Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eec0294f3e | ||
| 59a1c7956c | |||
|
|
e86949a1d7 | ||
|
|
7ca62bfc46 | ||
|
|
b60e4ae670 | ||
| ace52f8fc5 | |||
| 1ae9535516 | |||
|
|
b50cfb5049 | ||
|
|
a5227b9936 | ||
|
|
0d298db797 | ||
|
|
cbe71a1f32 | ||
|
|
a8fa8fd7e0 | ||
|
|
4aa2abd396 | ||
|
|
fa3326e99c | ||
|
|
21e050ce29 | ||
|
|
e480e2821b | ||
|
|
2d7e9b9226 | ||
|
|
93e0c4052c | ||
|
|
22373a0b87 | ||
|
|
d7968af525 | ||
| df2a48c20d | |||
| 7f1c02256b | |||
| fdc9b8b60d | |||
| 1025fed0d1 | |||
| 0331d94ca5 | |||
| 755c39a0f6 | |||
| 8f8eeddd91 | |||
| 548b101d82 | |||
|
|
e3149f8a27 |
224
.claude/commands/push-tickets-lesstime.md
Normal file
224
.claude/commands/push-tickets-lesstime.md
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
---
|
||||||
|
name: push-tickets-lesstime
|
||||||
|
description: Use after full-project-review to push TICKETS.md tickets into Lesstime project management via MCP. Triggers on "push tickets", "envoyer tickets", "creer les tickets dans lesstime", "sync tickets lesstime", "pousser les tickets".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Push Tickets to Lesstime
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Prend le fichier `TICKETS.md` genere par le skill `full-project-review` et cree les taches correspondantes dans Lesstime via son MCP server. Chaque ticket devient une tache avec la bonne priorite, le bon groupe, et la description complete.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Apres un `full-project-review` qui a genere un `TICKETS.md`
|
||||||
|
- L'utilisateur demande de "pousser", "sync", "envoyer" les tickets dans Lesstime
|
||||||
|
- L'utilisateur veut creer les taches dans son gestionnaire de projet
|
||||||
|
|
||||||
|
## Prerequis
|
||||||
|
|
||||||
|
- Un fichier `TICKETS.md` doit exister dans le repertoire courant (genere par `full-project-review`)
|
||||||
|
- L'API Lesstime doit etre accessible via HTTP
|
||||||
|
|
||||||
|
## Connexion a Lesstime
|
||||||
|
|
||||||
|
Lesstime est accessible via un serveur MCP HTTP (JSON-RPC 2.0). Il n'y a PAS de MCP natif configure dans Claude Code — il faut appeler l'API directement via `curl` dans le Bash tool.
|
||||||
|
|
||||||
|
### Parametres de connexion
|
||||||
|
|
||||||
|
```
|
||||||
|
URL: http://project.malio-dev.fr/_mcp
|
||||||
|
TOKEN: 7e8b410a5b79b5c0432951dcee3a3a81e0731e86d9f70d8784ec079a2b759c64
|
||||||
|
```
|
||||||
|
|
||||||
|
### Procedure de connexion (3 etapes)
|
||||||
|
|
||||||
|
**Etape 1 — Initialiser la session** (SANS header Mcp-Session-Id) :
|
||||||
|
```bash
|
||||||
|
curl -s -D /tmp/mcp_headers -X POST http://project.malio-dev.fr/_mcp \
|
||||||
|
-H "Authorization: Bearer 7e8b410a5b79b5c0432951dcee3a3a81e0731e86d9f70d8784ec079a2b759c64" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"claude","version":"1.0"}}}' > /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
**Etape 2 — Extraire le Session ID** depuis les headers de reponse :
|
||||||
|
```bash
|
||||||
|
SID=$(grep -i "mcp-session-id" /tmp/mcp_headers | awk '{print $2}' | tr -d '\r\n')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Etape 3 — Appeler les outils** avec le Session ID :
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://project.malio-dev.fr/_mcp \
|
||||||
|
-H "Authorization: Bearer 7e8b410a5b79b5c0432951dcee3a3a81e0731e86d9f70d8784ec079a2b759c64" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Mcp-Session-Id: $SID" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"list-projects","arguments":{}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Les reponses sont au format `{"jsonrpc":"2.0","id":X,"result":{"content":[{"type":"text","text":"[JSON_DATA]"}]}}`.
|
||||||
|
Extraire les donnees avec : `python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(json.loads(d['result']['content'][0]['text']))"`
|
||||||
|
|
||||||
|
### Approche recommandee : script Python
|
||||||
|
|
||||||
|
Pour pousser plusieurs tickets, generer un script Python temporaire qui :
|
||||||
|
1. Initialise la session via curl subprocess
|
||||||
|
2. Extrait le SID
|
||||||
|
3. Boucle sur les tickets et appelle create-task pour chacun
|
||||||
|
4. Affiche le resultat
|
||||||
|
|
||||||
|
Voir la memoire `reference_lesstime.md` pour les IDs connus (projets, users, statuts, priorites).
|
||||||
|
|
||||||
|
### IDs frequemment utilises
|
||||||
|
|
||||||
|
| Type | Label | ID |
|
||||||
|
|------|-------|----|
|
||||||
|
| Statut | A faire | 1 |
|
||||||
|
| Statut | En cours | 2 |
|
||||||
|
| Statut | Termine | 5 |
|
||||||
|
| Priorite | Basse | 1 |
|
||||||
|
| Priorite | Moyen | 2 |
|
||||||
|
| Priorite | Haute | 3 |
|
||||||
|
| User | matteo | 6 |
|
||||||
|
| User | Matthieu | 5 |
|
||||||
|
| Projet | Infrastructure | 13 |
|
||||||
|
| Projet | Lesstime | 5 |
|
||||||
|
| Projet | Inventory | 7 |
|
||||||
|
| Projet | Ferme | 8 |
|
||||||
|
| Projet | SIRH | 12 |
|
||||||
|
|
||||||
|
**IMPORTANT :** Toujours faire un appel `list-projects` / `list-users` / `list-priorities` en phase Discovery pour verifier que les IDs sont toujours valides. Les IDs ci-dessus sont un cache pour aller plus vite, pas une source de verite.
|
||||||
|
|
||||||
|
## Outils MCP Lesstime disponibles
|
||||||
|
|
||||||
|
Le MCP Lesstime expose 22 outils. Voici ceux utilises par ce skill :
|
||||||
|
|
||||||
|
### Discovery (appeler en premier pour mapper les IDs)
|
||||||
|
|
||||||
|
| Outil | Usage |
|
||||||
|
|-------|-------|
|
||||||
|
| `list-projects` | Trouver le projectId cible |
|
||||||
|
| `list-statuses` | Recuperer les statuts disponibles (label, id, color) |
|
||||||
|
| `list-priorities` | Recuperer les priorites disponibles (label, id, color) |
|
||||||
|
| `list-efforts` | Recuperer les niveaux d'effort (label, id) |
|
||||||
|
| `list-groups` | Lister les groupes d'un projet (par projectId) |
|
||||||
|
| `list-tags` | Lister les tags disponibles (label, id, color) |
|
||||||
|
| `list-users` | Lister les utilisateurs pour l'assignation |
|
||||||
|
|
||||||
|
### Creation
|
||||||
|
|
||||||
|
| Outil | Usage |
|
||||||
|
|-------|-------|
|
||||||
|
| `create-task` | Creer une tache (projectId, title, description, statusId, priorityId, effortId, assigneeId, groupId, tagIds) |
|
||||||
|
| `create-group` | Creer un groupe dans un projet (projectId, title) |
|
||||||
|
|
||||||
|
### Parametres de `create-task`
|
||||||
|
|
||||||
|
```
|
||||||
|
projectId: int (required) -- ID du projet cible
|
||||||
|
title: string (required) -- Titre du ticket (ex: "T-001 -- Supprimer le webhook hardcode")
|
||||||
|
description: string (optional) -- Corps complet du ticket (Pourquoi + A faire + Fichiers)
|
||||||
|
statusId: int (optional) -- ID du statut initial
|
||||||
|
priorityId: int (optional) -- ID de la priorite
|
||||||
|
effortId: int (optional) -- ID de l'effort estime
|
||||||
|
assigneeId: int (optional) -- ID de l'utilisateur assigne
|
||||||
|
groupId: int (optional) -- ID du groupe (utilise pour regrouper par priorite)
|
||||||
|
tagIds: int[] (optional) -- IDs des tags
|
||||||
|
```
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
```dot
|
||||||
|
digraph push_flow {
|
||||||
|
rankdir=TB;
|
||||||
|
"1. Lire TICKETS.md" -> "2. Discovery MCP (parallele)";
|
||||||
|
"2. Discovery MCP (parallele)" -> "3. Demander projet cible";
|
||||||
|
"3. Demander projet cible" -> "4. Mapper priorites";
|
||||||
|
"4. Mapper priorites" -> "5. Creer groupes si besoin";
|
||||||
|
"5. Creer groupes si besoin" -> "6. Creer les taches";
|
||||||
|
"6. Creer les taches" -> "7. Resume au user";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 1 -- Lire et parser TICKETS.md
|
||||||
|
|
||||||
|
Lire le fichier `TICKETS.md` du repertoire courant. Extraire :
|
||||||
|
- La liste des tickets avec leur ID (T-001, T-002, ...)
|
||||||
|
- Le titre de chaque ticket
|
||||||
|
- La priorite (P0, P1, P2, P3) -- derivee de la section dans laquelle se trouve le ticket
|
||||||
|
- Le corps complet (Pourquoi + A faire + Fichiers) -- sera la description de la tache
|
||||||
|
|
||||||
|
**Parsing :**
|
||||||
|
- Les sections `## P0`, `## P1`, `## P2`, `## P3` delimitent les groupes de priorite
|
||||||
|
- Chaque `### T-XXX -- {Titre}` est un ticket
|
||||||
|
- Tout le contenu entre deux `### T-XXX` constitue la description du ticket
|
||||||
|
|
||||||
|
### Phase 2 -- Discovery MCP (appels paralleles)
|
||||||
|
|
||||||
|
Appeler ces outils MCP **en parallele** pour recuperer les metadonnees :
|
||||||
|
|
||||||
|
1. `list-projects` -- pour afficher les projets disponibles
|
||||||
|
2. `list-statuses` -- pour mapper le statut initial des taches
|
||||||
|
3. `list-priorities` -- pour mapper P0/P1/P2/P3 aux priorites Lesstime
|
||||||
|
4. `list-efforts` -- pour estimer l'effort
|
||||||
|
5. `list-tags` -- pour les tags disponibles
|
||||||
|
|
||||||
|
### Phase 3 -- Demander le projet cible
|
||||||
|
|
||||||
|
Presenter a l'utilisateur la liste des projets Lesstime et lui demander :
|
||||||
|
1. **Quel projet ?** -- dans quel projet creer les taches
|
||||||
|
2. **Quel statut initial ?** -- ex: "To Do", "Backlog"
|
||||||
|
3. **Creer des groupes par priorite ?** -- ex: "P0 - Urgents", "P1 - Importants"
|
||||||
|
4. **Assigner a quelqu'un ?** -- optionnel
|
||||||
|
5. **Tags a ajouter ?** -- ex: "review", "tech-debt"
|
||||||
|
|
||||||
|
### Phase 4 -- Mapper les priorites
|
||||||
|
|
||||||
|
Mapper les priorites du TICKETS.md aux priorites Lesstime :
|
||||||
|
- P0 -> priorite la plus haute disponible (ex: "Urgent", "Critical")
|
||||||
|
- P1 -> priorite haute (ex: "High")
|
||||||
|
- P2 -> priorite moyenne (ex: "Medium")
|
||||||
|
- P3 -> priorite basse (ex: "Low")
|
||||||
|
|
||||||
|
Si le mapping n'est pas evident, demander confirmation a l'utilisateur.
|
||||||
|
|
||||||
|
### Phase 5 -- Creer les groupes (si demande)
|
||||||
|
|
||||||
|
Si l'utilisateur veut des groupes par priorite :
|
||||||
|
1. Creer le groupe "P0 - Urgents (securite)" via `create-group`
|
||||||
|
2. Creer le groupe "P1 - Importants" via `create-group`
|
||||||
|
3. Creer le groupe "P2 - Documentation" via `create-group`
|
||||||
|
4. Creer le groupe "P3 - Nice to have" via `create-group`
|
||||||
|
|
||||||
|
### Phase 6 -- Creer les taches
|
||||||
|
|
||||||
|
Pour chaque ticket dans TICKETS.md :
|
||||||
|
1. Construire le titre : `"T-XXX -- {titre}"`
|
||||||
|
2. Construire la description : le corps complet du ticket (Pourquoi + A faire + Fichiers)
|
||||||
|
3. Appeler `create-task` avec tous les parametres mappes
|
||||||
|
|
||||||
|
**Optimisation :** Creer les taches en parallele par batch de 5 pour eviter de surcharger l'API.
|
||||||
|
|
||||||
|
### Phase 7 -- Resume
|
||||||
|
|
||||||
|
Afficher un resume au user :
|
||||||
|
- Nombre de taches creees
|
||||||
|
- Repartition par priorite
|
||||||
|
- Lien vers le projet Lesstime (si disponible)
|
||||||
|
- Taches echouees (si applicable) avec raison
|
||||||
|
|
||||||
|
## Mapping par defaut
|
||||||
|
|
||||||
|
| TICKETS.md | Lesstime Priority | Lesstime Group |
|
||||||
|
|------------|-------------------|----------------|
|
||||||
|
| P0 | Urgent/Critical | "P0 - Urgents (securite)" |
|
||||||
|
| P1 | High | "P1 - Importants" |
|
||||||
|
| P2 | Medium | "P2 - Documentation" |
|
||||||
|
| P3 | Low | "P3 - Nice to have" |
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
- **Oublier la phase Discovery** -- les IDs de priorites/statuts varient par workspace Lesstime
|
||||||
|
- **Ne pas demander confirmation** -- toujours valider le projet cible et le mapping avant de creer
|
||||||
|
- **Creer sans groupes** -- les groupes rendent la vue Lesstime beaucoup plus lisible
|
||||||
|
- **Description trop courte** -- inclure le corps complet du ticket, pas juste le titre
|
||||||
|
- **Ne pas gerer les erreurs** -- si une tache echoue, continuer avec les suivantes et reporter a la fin
|
||||||
61
.claude/skills/ticket-executor/LEARNINGS.md
Normal file
61
.claude/skills/ticket-executor/LEARNINGS.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Ticket Executor - Learnings
|
||||||
|
|
||||||
|
## Session 2026-03-17 (26 tickets)
|
||||||
|
|
||||||
|
### T-001 — Secrets .env
|
||||||
|
- **Pattern**: Replace secrets with `change_me_in_env_local` placeholder, move real values to `.env.local`
|
||||||
|
- **Gotcha**: `.env.local` must contain ALL overridden secrets
|
||||||
|
|
||||||
|
### T-002 — Security API Gitea
|
||||||
|
- **Pattern**: Ajouter `security: "is_granted('ROLE_USER')"` sur les opérations ApiResource
|
||||||
|
- **Learning**: Vérifier d'abord les ressources déjà sécurisées pour ne pas dupliquer
|
||||||
|
|
||||||
|
### T-003 — SVG Upload
|
||||||
|
- **Pattern**: Double protection - bloquer à l'upload (retirer du MIME allowlist) + defense-in-depth (Content-Disposition: attachment au download)
|
||||||
|
- **Learning**: Toujours vérifier upload ET download controllers
|
||||||
|
|
||||||
|
### T-004 — MCP create-task / Repos numérotation
|
||||||
|
- **Gotcha critique**: PostgreSQL n'autorise PAS `FOR UPDATE` avec des fonctions d'agrégation (`MAX`)
|
||||||
|
- **Fix**: Utiliser `pg_advisory_xact_lock()` au lieu de `FOR UPDATE` pour les queries avec agrégation
|
||||||
|
- **Pattern**: Offset les lock keys (+1000000) pour éviter collisions entre Task et ClientTicket
|
||||||
|
|
||||||
|
### T-005 — Filter ROLE_CLIENT projects
|
||||||
|
- **Pattern**: Créer une Doctrine Extension (`QueryCollectionExtensionInterface` + `QueryItemExtensionInterface`) pour filtrer par relation
|
||||||
|
- **Learning**: Symfony autoconfigure enregistre l'extension automatiquement
|
||||||
|
|
||||||
|
### T-006 — Block client doc upload
|
||||||
|
- **Pattern**: Vérifier le rôle dans le Processor AVANT de résoudre l'IRI de la tâche
|
||||||
|
- **Learning**: Le portail client envoie un `clientTicket` IRI (pas de `task` IRI), donc le check sur `taskIri` non-vide suffit
|
||||||
|
|
||||||
|
### T-007 — MCP role checks
|
||||||
|
- **Pattern**: Injecter `Security` dans chaque Tool, vérifier au début de `__invoke()`
|
||||||
|
- **Learning**: 22 tools à modifier - bien séparer ROLE_ADMIN (users/clients) vs ROLE_USER (le reste)
|
||||||
|
|
||||||
|
### T-009 — Password hashing
|
||||||
|
- **Pattern**: Champ `plainPassword` non-persisté, writable uniquement, hashé dans le Processor
|
||||||
|
- **Learning**: Modifier aussi le frontend (DTO + composant) quand on renomme un champ API
|
||||||
|
|
||||||
|
### T-010 — Rate limiting
|
||||||
|
- **Gotcha**: `login_throttling` nécessite `symfony/rate-limiter` installé, pas juste dans composer.json
|
||||||
|
- **Learning**: Toujours vérifier que les packages sont installés, pas juste déclarés
|
||||||
|
|
||||||
|
### T-012 — Harmoniser repos numérotation
|
||||||
|
- **Pattern**: Aligner les contrats (retourner le max, pas le next) et mettre le +1 côté appelant
|
||||||
|
- **Learning**: Vérifier TOUS les appelants d'une méthode renommée
|
||||||
|
|
||||||
|
### T-015 — useAvatarService
|
||||||
|
- **Learning**: Quand on migre vers `useApi()`, ajouter la détection FormData pour ne pas écraser le Content-Type multipart
|
||||||
|
|
||||||
|
### T-020 — i18n
|
||||||
|
- **Pattern**: Ajouter `useI18n()` dans le setup script avant de pouvoir utiliser `t()` dans le JS
|
||||||
|
- **Learning**: Les templates peuvent utiliser `$t()` directement sans import
|
||||||
|
|
||||||
|
### T-022 — Retirer twig-bundle
|
||||||
|
- **Pattern**: Retirer de composer.json + bundles.php + supprimer config YAML + templates
|
||||||
|
- **Learning**: API Platform ne requiert PAS twig, c'est juste suggéré pour Swagger UI
|
||||||
|
|
||||||
|
## Meta-learnings
|
||||||
|
- **Parallélisation**: Les tickets touchant des fichiers indépendants peuvent tourner en parallèle sans problème
|
||||||
|
- **MCP status**: Toujours mettre "En cours" AVANT de commencer, "Terminé" APRÈS validation
|
||||||
|
- **PostgreSQL gotchas**: Tester les queries SQL avec agrégation + locking sur PostgreSQL, pas MySQL
|
||||||
|
- **Agents**: Les agents simples (1-3 fichiers) terminent en ~30s, les complexes (22 fichiers) en ~8min
|
||||||
78
.claude/skills/ticket-executor/SKILL.md
Normal file
78
.claude/skills/ticket-executor/SKILL.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: ticket-executor
|
||||||
|
description: Execute Lesstime project tickets systematically - updates MCP statuses, follows project conventions, and logs learnings for self-improvement
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ticket Executor Skill
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Execute Lesstime project tickets end-to-end: read the ticket, implement the fix, update MCP status, and log learnings.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Receive Ticket
|
||||||
|
- Get ticket ID, title, description, tags (Backend/Frontend), priority, and current status
|
||||||
|
- Understand the scope from the title and description
|
||||||
|
|
||||||
|
### 2. Set Status to "En cours" (ID: 2)
|
||||||
|
- Use MCP `update-task` with `statusId: 2` before starting work
|
||||||
|
- MCP endpoint: `http://project.malio-dev.fr/_mcp`
|
||||||
|
- Auth: `Bearer 7e8b410a5b79b5c0432951dcee3a3a81e0731e86d9f70d8784ec079a2b759c64`
|
||||||
|
|
||||||
|
### 3. Analyze & Implement
|
||||||
|
Based on tag:
|
||||||
|
- **Backend**: Check `src/Entity/`, `src/State/`, `src/Controller/`, `src/Security/`, `config/`
|
||||||
|
- **Frontend**: Check `frontend/components/`, `frontend/composables/`, `frontend/pages/`, `frontend/services/`
|
||||||
|
|
||||||
|
Conventions to follow:
|
||||||
|
- PHP: `declare(strict_types=1)`, Symfony + PSR-12, API Platform patterns
|
||||||
|
- Frontend: TypeScript strict, `useApi()` composable, 4 spaces indent
|
||||||
|
- See CLAUDE.md for full conventions
|
||||||
|
|
||||||
|
### 4. Verify
|
||||||
|
- For Backend: `make php-cs-fixer-allow-risky` if PHP changed
|
||||||
|
- For Frontend: check TypeScript types, no `any`
|
||||||
|
- Read modified files to confirm correctness
|
||||||
|
|
||||||
|
### 5. Set Status to "Terminé" (ID: 5)
|
||||||
|
- Use MCP `update-task` with `statusId: 5` after successful implementation
|
||||||
|
|
||||||
|
### 6. Log Learnings
|
||||||
|
Append to `.claude/skills/ticket-executor/LEARNINGS.md`:
|
||||||
|
- What worked well
|
||||||
|
- Patterns discovered
|
||||||
|
- Gotchas encountered
|
||||||
|
- Time-saving shortcuts found
|
||||||
|
|
||||||
|
## MCP Session Management
|
||||||
|
The MCP HTTP transport requires a session. To call tools:
|
||||||
|
```bash
|
||||||
|
# Initialize session (get Mcp-Session-Id from response header)
|
||||||
|
curl -si -X POST http://project.malio-dev.fr/_mcp \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude","version":"1.0"}}}'
|
||||||
|
|
||||||
|
# Call tool (use Mcp-Session-Id from init response)
|
||||||
|
curl -s -X POST http://project.malio-dev.fr/_mcp \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Mcp-Session-Id: <session-id>" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"update-task","arguments":{"id":<taskId>,"statusId":<statusId>}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status IDs
|
||||||
|
- 1 = A faire
|
||||||
|
- 2 = En cours
|
||||||
|
- 3 = Bloqué
|
||||||
|
- 4 = En attente de validation
|
||||||
|
- 5 = Terminé
|
||||||
|
|
||||||
|
## Learnings Integration
|
||||||
|
Before each ticket, read `LEARNINGS.md` to apply previous insights.
|
||||||
|
After each ticket, append new learnings. This creates a feedback loop that improves execution quality over time.
|
||||||
|
|
||||||
|
## Parallel Execution Rules
|
||||||
|
- Independent tickets (no shared files) can run in parallel via worktree agents
|
||||||
|
- Tickets modifying the same files must run sequentially
|
||||||
|
- Always verify no merge conflicts after parallel execution
|
||||||
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.env.local
|
||||||
|
.env.test
|
||||||
|
infra/dev/
|
||||||
|
infra/prod/docker-compose.yml
|
||||||
|
infra/prod/deploy.sh
|
||||||
|
infra/prod/deploy-release.sh
|
||||||
|
infra/prod/.env.example
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/.nuxt
|
||||||
|
frontend/.output
|
||||||
|
var/
|
||||||
|
vendor/
|
||||||
|
LOG/
|
||||||
|
docs/
|
||||||
|
tests/
|
||||||
|
*.sql
|
||||||
|
*.xlsx
|
||||||
|
*.png
|
||||||
|
*.md
|
||||||
|
!composer.lock
|
||||||
|
!symfony.lock
|
||||||
|
!frontend/package-lock.json
|
||||||
@@ -60,7 +60,7 @@ JWT_COOKIE_TTL=86400
|
|||||||
# Base de donnees (Doctrine / PostgreSQL)
|
# Base de donnees (Doctrine / PostgreSQL)
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
# Les variables POSTGRES_* sont definies dans docker/.env.docker
|
# Les variables POSTGRES_* sont definies dans infra/dev/.env.docker
|
||||||
# et injectees automatiquement par Docker Compose.
|
# et injectees automatiquement par Docker Compose.
|
||||||
# DATABASE_URL est construite a partir de ces variables.
|
# DATABASE_URL est construite a partir de ces variables.
|
||||||
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
@@ -74,10 +74,10 @@ DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_P
|
|||||||
ENCRYPTION_KEY=change_me_in_env_local
|
ENCRYPTION_KEY=change_me_in_env_local
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Docker (docker/.env.docker)
|
# Docker (infra/dev/.env.docker)
|
||||||
#
|
#
|
||||||
# Ces variables sont lues par Docker Compose. Voir docker/.env.docker
|
# Ces variables sont lues par Docker Compose. Voir infra/dev/.env.docker
|
||||||
# pour les valeurs par defaut. Creez docker/.env.docker.local pour
|
# pour les valeurs par defaut. Creez infra/dev/.env.docker.local pour
|
||||||
# surcharger localement.
|
# surcharger localement.
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
||||||
|
|||||||
30
.gitea/workflows/build-docker.yml
Normal file
30
.gitea/workflows/build-docker.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Build & Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Gitea Registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.malio.fr -u "${{ gitea.repository_owner }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
-f infra/prod/Dockerfile \
|
||||||
|
-t gitea.malio.fr/malio-dev/lesstime:${{ gitea.ref_name }} \
|
||||||
|
-t gitea.malio.fr/malio-dev/lesstime:latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
run: |
|
||||||
|
docker push gitea.malio.fr/malio-dev/lesstime:${{ gitea.ref_name }}
|
||||||
|
docker push gitea.malio.fr/malio-dev/lesstime:latest
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
name: Build Release Artefact
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: "8.4"
|
|
||||||
extensions: mbstring, intl, pdo_pgsql, xml, curl, zip, gd
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Install backend deps (prod)
|
|
||||||
env:
|
|
||||||
APP_ENV: prod
|
|
||||||
APP_DEBUG: "0"
|
|
||||||
run: composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
|
|
||||||
|
|
||||||
- name: Build frontend (static)
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
npm ci
|
|
||||||
CI=1 NUXT_TELEMETRY_DISABLED=1 NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ npm run generate
|
|
||||||
test -f .output/public/index.html
|
|
||||||
|
|
||||||
- name: Build artefact
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
mkdir -p release
|
|
||||||
tar -czf "release/lesstime-${GITHUB_REF_NAME}.tar.gz" \
|
|
||||||
.env \
|
|
||||||
bin \
|
|
||||||
config \
|
|
||||||
migrations \
|
|
||||||
public \
|
|
||||||
src \
|
|
||||||
vendor \
|
|
||||||
composer.json \
|
|
||||||
composer.lock \
|
|
||||||
symfony.lock \
|
|
||||||
frontend/.output
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: release/lesstime-${{ github.ref_name }}.tar.gz
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -28,5 +28,5 @@
|
|||||||
###< ide ###
|
###< ide ###
|
||||||
|
|
||||||
###> docker local ###
|
###> docker local ###
|
||||||
docker/.env.docker.local
|
infra/dev/.env.docker.local
|
||||||
###< docker local ###
|
###< docker local ###
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Exemples : `feat : add login page`, `fix(auth) : prevent null token crash`
|
|||||||
- Container PHP : `php-lesstime-fpm`
|
- Container PHP : `php-lesstime-fpm`
|
||||||
- Container Nginx : `nginx-lesstime`
|
- Container Nginx : `nginx-lesstime`
|
||||||
- Container DB : PostgreSQL sur port **5435** (interne et externe)
|
- Container DB : PostgreSQL sur port **5435** (interne et externe)
|
||||||
- Config Docker : `docker/.env.docker` (override local : `docker/.env.docker.local`)
|
- Config Docker : `infra/dev/.env.docker` (override local : `infra/dev/.env.docker.local`)
|
||||||
- Après modif nginx : `docker restart nginx-lesstime`
|
- Après modif nginx : `docker restart nginx-lesstime`
|
||||||
|
|
||||||
## Fixtures
|
## Fixtures
|
||||||
|
|||||||
0
LOG/xdebug.log
Normal file
0
LOG/xdebug.log
Normal file
@@ -156,7 +156,7 @@ docker/ # Dockerfiles et config Nginx
|
|||||||
| `nginx-lesstime` | 8082 | Nginx reverse proxy |
|
| `nginx-lesstime` | 8082 | Nginx reverse proxy |
|
||||||
| PostgreSQL | 5435 | Base de données |
|
| PostgreSQL | 5435 | Base de données |
|
||||||
|
|
||||||
Configuration : `docker/.env.docker` (override local : `docker/.env.docker.local`)
|
Configuration : `infra/dev/.env.docker` (override local : `infra/dev/.env.docker.local`)
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.3.9'
|
app.version: '0.3.20'
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name project.malio-dev.fr;
|
|
||||||
|
|
||||||
root /var/www/lesstime/frontend/.output/public;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
client_max_body_size 55m;
|
|
||||||
|
|
||||||
location ^~ /api/ {
|
|
||||||
root /var/www/lesstime/public;
|
|
||||||
try_files $uri /index.php?$query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ^~ /bundles/ {
|
|
||||||
root /var/www/lesstime/public;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /api/login_check {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/lesstime/public/index.php;
|
|
||||||
fastcgi_param DOCUMENT_ROOT /var/www/lesstime/public;
|
|
||||||
fastcgi_param SCRIPT_NAME /index.php;
|
|
||||||
fastcgi_param PATH_INFO /login_check;
|
|
||||||
fastcgi_param REQUEST_URI /login_check;
|
|
||||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ^~ /_mcp {
|
|
||||||
root /var/www/lesstime/public;
|
|
||||||
try_files $uri /index.php?$query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/index\.php(/|$) {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/lesstime/public/index.php;
|
|
||||||
fastcgi_param DOCUMENT_ROOT /var/www/lesstime/public;
|
|
||||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ \.php$ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
335
doc/deployment-docker.md
Normal file
335
doc/deployment-docker.md
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
# Deploiement Docker — Lesstime
|
||||||
|
|
||||||
|
## Pre-requis
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y ca-certificates curl gnupg
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Se deconnecter/reconnecter pour que le groupe `docker` prenne effet.
|
||||||
|
|
||||||
|
### Nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y nginx
|
||||||
|
sudo systemctl enable nginx
|
||||||
|
sudo systemctl start nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
PostgreSQL tourne dans un conteneur Docker separe (voir le repo `infra-postgres`).
|
||||||
|
Il doit etre installe et accessible avant de deployer Lesstime.
|
||||||
|
|
||||||
|
Creer la base de donnees pour Lesstime :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/postgres
|
||||||
|
docker compose exec postgres psql -U admin
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Si le user n'existe pas encore
|
||||||
|
CREATE USER malio WITH PASSWORD 'motdepasse';
|
||||||
|
|
||||||
|
-- Creer la base
|
||||||
|
CREATE DATABASE lesstime_prod OWNER malio;
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Premiere installation (nouvelle machine)
|
||||||
|
|
||||||
|
Guide complet pour mettre en ligne Lesstime sur une machine vierge. Inclut les pre-requis, la BDD et l'app.
|
||||||
|
|
||||||
|
### 1. Installer les pre-requis
|
||||||
|
|
||||||
|
Installer Docker, Nginx et PostgreSQL (voir section Pre-requis ci-dessus).
|
||||||
|
|
||||||
|
### 2. Creer le dossier de deploiement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /var/www/lesstime
|
||||||
|
sudo chown -R $(whoami):$(whoami) /var/www/lesstime
|
||||||
|
cd /var/www/lesstime
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Se connecter au registry Docker de Gitea
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login gitea.malio.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Username** : le nom d'utilisateur du compte organisation Gitea `MALIO`
|
||||||
|
- **Password** : le token REGISTRY_TOKEN dispo dans le bitwarden
|
||||||
|
|
||||||
|
Le login est sauvegarde dans `~/.docker/config.json`, pas besoin de le refaire a chaque deploiement.
|
||||||
|
|
||||||
|
### 4. Creer les fichiers de deploiement
|
||||||
|
|
||||||
|
Creer `docker-compose.yml` :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.malio.fr/malio-dev/lesstime:${LESSTIME_IMAGE_TAG:-latest}
|
||||||
|
container_name: lesstime-app
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./config/jwt:/var/www/html/config/jwt:ro
|
||||||
|
- ./uploads:/var/www/html/var/uploads
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
Creer `deploy.sh` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
TAG="${1:-latest}"
|
||||||
|
export LESSTIME_IMAGE_TAG="$TAG"
|
||||||
|
|
||||||
|
echo "==> Deploying lesstime:${TAG}..."
|
||||||
|
|
||||||
|
echo "==> Pulling image..."
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
echo "==> Starting container..."
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo "==> Waiting for container to be ready..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "==> Running migrations..."
|
||||||
|
docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
echo "==> Clearing cache..."
|
||||||
|
docker compose exec -T -u www-data app php bin/console cache:clear --env=prod
|
||||||
|
docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
|
||||||
|
|
||||||
|
VERSION=$(docker compose exec -T app cat config/version.yaml | grep 'app.version' | awk -F"'" '{print $2}')
|
||||||
|
echo "==> Deployed v${VERSION}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Rendre executable :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Configurer l'environnement
|
||||||
|
|
||||||
|
Creer `.env` avec les variables suivantes :
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Symfony
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_DEBUG=0
|
||||||
|
APP_SECRET=<generer avec: openssl rand -hex 32>
|
||||||
|
|
||||||
|
# Database (host.docker.internal = la machine hote, ou le PG tourne en Docker)
|
||||||
|
DATABASE_URL="postgresql://malio:password@host.docker.internal:5432/lesstime_prod?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=<generer avec: openssl rand -hex 32>
|
||||||
|
JWT_COOKIE_SECURE=1
|
||||||
|
JWT_COOKIE_SAMESITE=lax
|
||||||
|
JWT_TOKEN_TTL=86400
|
||||||
|
JWT_COOKIE_TTL=86400
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://project\.malio-dev\.fr$'
|
||||||
|
|
||||||
|
# App
|
||||||
|
DEFAULT_URI=https://project.malio-dev.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Generer les cles JWT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p config/jwt
|
||||||
|
openssl genpkey -algorithm RSA -out config/jwt/private.pem -pkeyopt rsa_keygen_bits:4096
|
||||||
|
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
Rendre les cles lisibles par le conteneur (www-data = uid 33) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chown 33:33 config/jwt/private.pem config/jwt/public.pem
|
||||||
|
sudo chmod 644 config/jwt/private.pem config/jwt/public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Creer le dossier uploads
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Configurer Nginx systeme
|
||||||
|
|
||||||
|
Creer `/etc/nginx/sites-available/lesstime.conf` :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name project.malio-dev.fr;
|
||||||
|
|
||||||
|
client_max_body_size 55m;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Activer le site :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/lesstime.conf /etc/nginx/sites-enabled/lesstime.conf
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Deployer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Importer les donnees (optionnel)
|
||||||
|
|
||||||
|
Si tu as un dump SQL a importer :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis ton PC, envoyer le dump vers le serveur
|
||||||
|
scp lesstime.sql user@serveur:/tmp/lesstime.sql
|
||||||
|
|
||||||
|
# Sur le serveur, vider la base puis importer
|
||||||
|
cd /var/www/postgres
|
||||||
|
docker compose exec -T postgres psql -U malio lesstime_prod -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
|
||||||
|
docker compose exec -T postgres psql -U malio lesstime_prod < /tmp/lesstime.sql
|
||||||
|
|
||||||
|
# Creer les tables manquantes (si le dump a des erreurs de syntaxe)
|
||||||
|
cd /var/www/lesstime
|
||||||
|
docker compose exec -u www-data app php bin/console doctrine:schema:update --force --env=prod
|
||||||
|
|
||||||
|
# Nettoyer
|
||||||
|
rm /tmp/lesstime.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structure finale du dossier
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/www/lesstime/
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── deploy.sh
|
||||||
|
├── .env
|
||||||
|
├── config/jwt/
|
||||||
|
│ ├── private.pem
|
||||||
|
│ └── public.pem
|
||||||
|
└── uploads/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployer une nouvelle version
|
||||||
|
|
||||||
|
Quand l'app est deja installee, deployer une mise a jour :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/lesstime
|
||||||
|
./deploy.sh # deploie la derniere version (latest)
|
||||||
|
./deploy.sh v0.3.13 # deploie une version specifique
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est tout. Le script pull l'image, redemarre le conteneur, lance les migrations et vide le cache.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
### Image seule (pas de changement de schema BDD)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh v0.3.12
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avec rollback de migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Rollback schema (pendant que la version actuelle tourne encore)
|
||||||
|
docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate prev --no-interaction
|
||||||
|
# 2. Deployer l'ancienne version
|
||||||
|
./deploy.sh v0.3.12
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
Le workflow `.gitea/workflows/build-docker.yml` se declenche automatiquement sur push de tag `v*` :
|
||||||
|
1. Build l'image multi-stage
|
||||||
|
2. Push vers `gitea.malio.fr/malio-dev/lesstime:<tag>` et `:latest`
|
||||||
|
|
||||||
|
Combine avec `auto-tag-develop.yml`, chaque push sur `develop` cree automatiquement un tag → build → image disponible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voir les logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/lesstime
|
||||||
|
docker compose logs -f # tous les logs
|
||||||
|
docker compose logs -f --tail=100 # 100 dernieres lignes
|
||||||
|
```
|
||||||
|
|
||||||
|
Logs Symfony :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec app cat var/log/prod.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration depuis l'ancien deploiement (bare-metal)
|
||||||
|
|
||||||
|
Si l'application tourne deja en bare metal :
|
||||||
|
|
||||||
|
1. Installer Docker (voir pre-requis)
|
||||||
|
2. Creer le dossier `/var/www/lesstime-docker/` (ne pas ecraser l'ancien)
|
||||||
|
3. Copier les fichiers existants :
|
||||||
|
```bash
|
||||||
|
cp /var/www/lesstime/.env /var/www/lesstime-docker/.env
|
||||||
|
cp -a /var/www/lesstime/config/jwt /var/www/lesstime-docker/config/jwt
|
||||||
|
cp -a /var/www/lesstime/var/uploads /var/www/lesstime-docker/uploads
|
||||||
|
```
|
||||||
|
4. Creer `docker-compose.yml` et `deploy.sh` dans `/var/www/lesstime-docker/` (voir etape 4 ci-dessus)
|
||||||
|
5. Editer `/var/www/lesstime-docker/.env` : changer `DATABASE_URL` pour utiliser `host.docker.internal` au lieu de `127.0.0.1`
|
||||||
|
6. Se connecter au registry Gitea (voir etape 3 ci-dessus)
|
||||||
|
7. Mettre a jour Nginx systeme avec la conf reverse proxy (voir etape 8 ci-dessus)
|
||||||
|
8. Arreter l'ancien PHP-FPM : `sudo systemctl stop php8.4-fpm`
|
||||||
|
9. Deployer : `cd /var/www/lesstime-docker && ./deploy.sh`
|
||||||
|
10. Verifier que tout marche, puis renommer le dossier : `mv /var/www/lesstime-docker /var/www/lesstime`
|
||||||
@@ -2,7 +2,7 @@ services:
|
|||||||
php:
|
php:
|
||||||
container_name: php-${DOCKER_APP_NAME}-fpm
|
container_name: php-${DOCKER_APP_NAME}-fpm
|
||||||
build:
|
build:
|
||||||
context: ./docker/php
|
context: ./infra/dev
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
DOCKER_PHP_VERSION: ${DOCKER_PHP_VERSION}
|
DOCKER_PHP_VERSION: ${DOCKER_PHP_VERSION}
|
||||||
@@ -21,8 +21,8 @@ services:
|
|||||||
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
||||||
- ~/.config:/var/www/.config # Pour la config de yarn
|
- ~/.config:/var/www/.config # Pour la config de yarn
|
||||||
- ~/.composer:/var/www/.composer # Pour la config de composer
|
- ~/.composer:/var/www/.composer # Pour la config de composer
|
||||||
- ./docker/php/config/php.ini:/usr/local/etc/php/php.ini
|
- ./infra/dev/php.ini:/usr/local/etc/php/php.ini
|
||||||
- ./docker/php/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
- ./infra/dev/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||||
- ./LOG:/var/www/html/LOG
|
- ./LOG:/var/www/html/LOG
|
||||||
- uploads_data:/var/www/html/var/uploads
|
- uploads_data:/var/www/html/var/uploads
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
@@ -41,7 +41,7 @@ services:
|
|||||||
- "8082:80"
|
- "8082:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/var/www/html:ro
|
- ./:/var/www/html:ro
|
||||||
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
- ./infra/dev/nginx.conf:/etc/nginx/conf.d/lesstime.conf:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|||||||
87
docs/claude-time-tracking-rule.md
Normal file
87
docs/claude-time-tracking-rule.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Règle Claude : Time Tracking automatique via Lesstime
|
||||||
|
|
||||||
|
> Ajouter ce contenu dans le CLAUDE.md de chaque projet ou dans `~/.claude/CLAUDE.md` pour l'appliquer globalement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Time Tracking obligatoire
|
||||||
|
|
||||||
|
Claude DOIT créer une time entry dans Lesstime au démarrage de chaque tâche de développement, ou sur demande explicite de l'utilisateur ("lance le chrono", "start timer", "track le temps").
|
||||||
|
|
||||||
|
### Déclencheurs
|
||||||
|
|
||||||
|
1. **Début d'une tâche de dev** : feature, bugfix, refactoring, infra, review
|
||||||
|
2. **Demande explicite** : "lance le chrono", "start timer", "track le temps"
|
||||||
|
3. **Depuis un ticket Lesstime** : lier directement au taskId du ticket référencé
|
||||||
|
|
||||||
|
### Méthode
|
||||||
|
|
||||||
|
Créer la time entry via **curl** sur l'API REST Lesstime :
|
||||||
|
|
||||||
|
1. **Login** : `POST http://project.malio-dev.fr/api/login_check`
|
||||||
|
- Body : `{"username":"admin","password":"admin"}`
|
||||||
|
- Réponse : 204 avec cookie `Set-Cookie: BEARER=<jwt>`
|
||||||
|
|
||||||
|
2. **Créer le timer** : `POST http://project.malio-dev.fr/api/time_entries`
|
||||||
|
- Headers : `Cookie: BEARER=<jwt>`, `Content-Type: application/ld+json`, `Accept: application/ld+json`
|
||||||
|
- Body :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": "/api/users/5",
|
||||||
|
"startedAt": "<ISO8601 avec timezone>",
|
||||||
|
"title": "<description courte de la tâche>",
|
||||||
|
"project": "/api/projects/<projectId>",
|
||||||
|
"tags": ["/api/task_tags/<tagId>"],
|
||||||
|
"task": "/api/tasks/<taskId>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Stopper le timer** : `PATCH http://project.malio-dev.fr/api/time_entries/<id>`
|
||||||
|
- Headers : `Cookie: BEARER=<jwt>`, `Content-Type: application/merge-patch+json`, `Accept: application/ld+json`
|
||||||
|
- Body : `{"stoppedAt": "<ISO8601>"}`
|
||||||
|
|
||||||
|
### Paramètres obligatoires
|
||||||
|
|
||||||
|
- **user** : TOUJOURS `/api/users/5` (Matthieu)
|
||||||
|
- **startedAt** : ISO 8601 avec timezone (ex: `2026-04-01T14:30:00+02:00`)
|
||||||
|
- **title** : description courte de la tâche en cours
|
||||||
|
- **project** : selon le projet (voir mapping ci-dessous)
|
||||||
|
|
||||||
|
### Tags (choisir selon le type de travail)
|
||||||
|
|
||||||
|
| Tag | ID | IRI |
|
||||||
|
|-----|----|-----|
|
||||||
|
| Backend | 3 | `/api/task_tags/3` |
|
||||||
|
| Frontend | 2 | `/api/task_tags/2` |
|
||||||
|
| IA | 7 | `/api/task_tags/7` |
|
||||||
|
| Infra | 5 | `/api/task_tags/5` |
|
||||||
|
| UI/UX | 4 | `/api/task_tags/4` |
|
||||||
|
| Maintenance | 6 | `/api/task_tags/6` |
|
||||||
|
| RDV | 1 | `/api/task_tags/1` |
|
||||||
|
| Réunion | 8 | `/api/task_tags/8` |
|
||||||
|
| Formation | 10 | `/api/task_tags/10` |
|
||||||
|
| Gestion projet | 9 | `/api/task_tags/9` |
|
||||||
|
|
||||||
|
### Mapping projets
|
||||||
|
|
||||||
|
| Projet | ID | IRI |
|
||||||
|
|--------|----|-----|
|
||||||
|
| Lesstime | 5 | `/api/projects/5` |
|
||||||
|
| Inventory | 7 | `/api/projects/7` |
|
||||||
|
| SIRH | 12 | `/api/projects/12` |
|
||||||
|
| Infrastructure | 13 | `/api/projects/13` |
|
||||||
|
| Malio UI | 11 | `/api/projects/11` |
|
||||||
|
| ERP Liot | 6 | `/api/projects/6` |
|
||||||
|
| Ferme | 8 | `/api/projects/8` |
|
||||||
|
| ADMIN | 16 | `/api/projects/16` |
|
||||||
|
| Maintenance-LIOT | 17 | `/api/projects/17` |
|
||||||
|
| Qualiopi | 14 | `/api/projects/14` |
|
||||||
|
| Vaultwarden | 18 | `/api/projects/18` |
|
||||||
|
|
||||||
|
### Règles
|
||||||
|
|
||||||
|
- **Un seul timer actif à la fois** (contrainte DB) — stopper l'actif avant d'en créer un nouveau
|
||||||
|
- **Toujours stopper le timer** en fin de tâche ou sur demande
|
||||||
|
- **Informer l'utilisateur** quand un timer est lancé/stoppé (numéro, titre, projet, tags)
|
||||||
|
- **Lier au ticket Lesstime** si un ticket est référencé (champ `task`)
|
||||||
|
- **Choisir les tags intelligemment** selon le type de travail effectué
|
||||||
@@ -61,7 +61,7 @@ ENCRYPTION_KEY=<random-hex-32>
|
|||||||
## 4. Installer le script de deploy
|
## 4. Installer le script de deploy
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cp script/deploy-release.sh /usr/local/bin/deploy-lesstime
|
sudo cp infra/prod/deploy-release.sh /usr/local/bin/deploy-lesstime
|
||||||
sudo chmod +x /usr/local/bin/deploy-lesstime
|
sudo chmod +x /usr/local/bin/deploy-lesstime
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ sudo -u www-data php bin/console lexik:jwt:generate-keypair --skip-if-exists --e
|
|||||||
## 7. Configurer Nginx
|
## 7. Configurer Nginx
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cp deploy/nginx/lesstime.conf /etc/nginx/sites-available/lesstime
|
sudo cp infra/prod/nginx-baremetal.conf /etc/nginx/sites-available/lesstime
|
||||||
sudo ln -sf /etc/nginx/sites-available/lesstime /etc/nginx/sites-enabled/
|
sudo ln -sf /etc/nginx/sites-available/lesstime /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -32,21 +32,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
:label="$t('bookstack.settings.save')"
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:opacity-50"
|
button-class="w-auto px-4"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
>
|
@click="handleSave"
|
||||||
{{ $t('bookstack.settings.save') }}
|
/>
|
||||||
</button>
|
<MalioButton
|
||||||
<button
|
variant="tertiary"
|
||||||
type="button"
|
:label="$t('bookstack.settings.testConnection')"
|
||||||
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50 disabled:opacity-50"
|
button-class="w-auto px-4"
|
||||||
:disabled="isTesting"
|
:disabled="isTesting"
|
||||||
@click="handleTest"
|
@click="handleTest"
|
||||||
>
|
/>
|
||||||
{{ $t('bookstack.settings.testConnection') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="testResult !== null" class="text-sm font-medium" :class="testResult ? 'text-green-600' : 'text-red-600'">
|
<p v-if="testResult !== null" class="text-sm font-medium" :class="testResult ? 'text-green-600' : 'text-red-600'">
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">Clients</h2>
|
<h2 class="text-lg font-bold text-neutral-900">Clients</h2>
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
|
label="Ajouter un client"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
/>
|
||||||
+ Ajouter un client
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@@ -92,19 +92,21 @@
|
|||||||
<td class="px-3 py-3 text-neutral-400">{{ formatDate(ticket.createdAt) }}</td>
|
<td class="px-3 py-3 text-neutral-400">{{ formatDate(ticket.createdAt) }}</td>
|
||||||
<td class="px-3 py-3">
|
<td class="px-3 py-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-primary-500"
|
icon="mdi:swap-horizontal"
|
||||||
:title="$t('clientTicket.changeStatus')"
|
:aria-label="$t('clientTicket.changeStatus')"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="18"
|
||||||
@click.stop="openStatusChange(ticket)"
|
@click.stop="openStatusChange(ticket)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:swap-horizontal" size="18" />
|
<MalioButtonIcon
|
||||||
</button>
|
icon="mdi:delete-outline"
|
||||||
<button
|
aria-label="Supprimer"
|
||||||
class="rounded p-1 text-neutral-400 transition-colors hover:bg-red-50 hover:text-red-500"
|
variant="ghost"
|
||||||
|
icon-size="18"
|
||||||
|
button-class="text-neutral-400 hover:bg-red-50 hover:text-red-500"
|
||||||
@click.stop="openDeleteConfirm(ticket)"
|
@click.stop="openDeleteConfirm(ticket)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:delete-outline" size="18" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -155,19 +157,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50"
|
variant="tertiary"
|
||||||
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="statusModalOpen = false"
|
@click="statusModalOpen = false"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
label="Confirmer"
|
||||||
<button
|
button-class="w-auto px-6"
|
||||||
class="rounded-lg bg-primary-500 px-6 py-2 text-sm font-semibold text-white transition-colors hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="isUpdatingStatus"
|
:disabled="isUpdatingStatus"
|
||||||
@click="confirmStatusChange"
|
@click="confirmStatusChange"
|
||||||
>
|
/>
|
||||||
Confirmer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,19 +187,19 @@
|
|||||||
<h3 class="text-lg font-bold text-neutral-900">{{ $t('clientTicket.confirmDelete') }}</h3>
|
<h3 class="text-lg font-bold text-neutral-900">{{ $t('clientTicket.confirmDelete') }}</h3>
|
||||||
<p class="mt-2 text-sm text-neutral-600">{{ $t('clientTicket.confirmDeleteMessage') }}</p>
|
<p class="mt-2 text-sm text-neutral-600">{{ $t('clientTicket.confirmDeleteMessage') }}</p>
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50"
|
variant="tertiary"
|
||||||
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="deleteModalOpen = false"
|
@click="deleteModalOpen = false"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
variant="danger"
|
||||||
<button
|
label="Supprimer"
|
||||||
class="rounded-lg bg-red-500 px-6 py-2 text-sm font-semibold text-white transition-colors hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isDeleting"
|
:disabled="isDeleting"
|
||||||
@click="confirmDelete"
|
@click="confirmDelete"
|
||||||
>
|
/>
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">Efforts</h2>
|
<h2 class="text-lg font-bold text-neutral-900">Efforts</h2>
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
|
label="Ajouter un effort"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
/>
|
||||||
+ Ajouter un effort
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@@ -24,21 +24,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
:label="$t('gitea.settings.save')"
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:opacity-50"
|
button-class="w-auto px-4"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
>
|
@click="handleSave"
|
||||||
{{ $t('gitea.settings.save') }}
|
/>
|
||||||
</button>
|
<MalioButton
|
||||||
<button
|
variant="tertiary"
|
||||||
type="button"
|
:label="$t('gitea.settings.testConnection')"
|
||||||
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50 disabled:opacity-50"
|
button-class="w-auto px-4"
|
||||||
:disabled="isTesting"
|
:disabled="isTesting"
|
||||||
@click="handleTest"
|
@click="handleTest"
|
||||||
>
|
/>
|
||||||
{{ $t('gitea.settings.testConnection') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="testResult !== null" class="text-sm font-medium" :class="testResult ? 'text-green-600' : 'text-red-600'">
|
<p v-if="testResult !== null" class="text-sm font-medium" :class="testResult ? 'text-green-600' : 'text-red-600'">
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">Priorités</h2>
|
<h2 class="text-lg font-bold text-neutral-900">Priorités</h2>
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
|
label="Ajouter une priorité"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
/>
|
||||||
+ Ajouter une priorité
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">Statuts</h2>
|
<h2 class="text-lg font-bold text-neutral-900">Statuts</h2>
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
|
label="Ajouter un statut"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
/>
|
||||||
+ Ajouter un statut
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">Tags</h2>
|
<h2 class="text-lg font-bold text-neutral-900">Tags</h2>
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
|
label="Ajouter un tag"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
/>
|
||||||
+ Ajouter un tag
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">Utilisateurs</h2>
|
<h2 class="text-lg font-bold text-neutral-900">Utilisateurs</h2>
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
|
label="Ajouter un utilisateur"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
/>
|
||||||
+ Ajouter un utilisateur
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@@ -37,21 +37,19 @@
|
|||||||
<span class="text-sm">{{ $t('zimbra.settings.enabled') }}</span>
|
<span class="text-sm">{{ $t('zimbra.settings.enabled') }}</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
:label="$t('zimbra.settings.save')"
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:opacity-50"
|
button-class="w-auto px-4"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
>
|
@click="handleSave"
|
||||||
{{ $t('zimbra.settings.save') }}
|
/>
|
||||||
</button>
|
<MalioButton
|
||||||
<button
|
variant="tertiary"
|
||||||
type="button"
|
:label="$t('zimbra.settings.testConnection')"
|
||||||
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50 disabled:opacity-50"
|
button-class="w-auto px-4"
|
||||||
:disabled="isTesting"
|
:disabled="isTesting"
|
||||||
@click="handleTest"
|
@click="handleTest"
|
||||||
>
|
/>
|
||||||
{{ $t('zimbra.settings.testConnection') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p v-if="testResult !== null" class="text-sm font-medium" :class="testResult ? 'text-green-600' : 'text-red-600'">
|
<p v-if="testResult !== null" class="text-sm font-medium" :class="testResult ? 'text-green-600' : 'text-red-600'">
|
||||||
{{ testResult ? $t('zimbra.settings.testSuccess') : $t('zimbra.settings.testFailed') }}
|
{{ testResult ? $t('zimbra.settings.testSuccess') : $t('zimbra.settings.testFailed') }}
|
||||||
|
|||||||
@@ -29,22 +29,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Edit button (only for open tickets submitted by current user) -->
|
<!-- Edit button (only for open tickets submitted by current user) -->
|
||||||
<button
|
<MalioButton
|
||||||
v-if="canEdit && !isEditing"
|
v-if="canEdit && !isEditing"
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="flex h-8 items-center gap-1.5 rounded-lg px-3 text-sm font-medium text-primary-500 transition-colors hover:bg-primary-50"
|
icon-name="mdi:pencil-outline"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-3"
|
||||||
|
:label="$t('common.edit')"
|
||||||
@click="startEdit"
|
@click="startEdit"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:pencil-outline" size="16" />
|
<MalioButtonIcon
|
||||||
{{ $t('common.edit') }}
|
icon="mdi:close"
|
||||||
</button>
|
aria-label="Fermer"
|
||||||
<button
|
variant="ghost"
|
||||||
type="button"
|
icon-size="20"
|
||||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-neutral-400 transition-colors hover:bg-neutral-200/60 hover:text-neutral-600"
|
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:close" size="20" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,21 +90,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50"
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="cancelEdit"
|
@click="cancelEdit"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
:label="$t('common.save')"
|
||||||
<button
|
button-class="w-auto px-6"
|
||||||
type="button"
|
|
||||||
class="rounded-lg bg-primary-500 px-6 py-2 text-sm font-semibold text-white transition-colors hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
@click="saveEdit"
|
@click="saveEdit"
|
||||||
>
|
/>
|
||||||
{{ $t('common.save') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Trigger button -->
|
<!-- Trigger button -->
|
||||||
<button
|
<MalioButton
|
||||||
class="relative flex shrink-0 items-center gap-2 rounded-md bg-neutral-100 px-3 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-200 sm:px-4"
|
variant="tertiary"
|
||||||
|
icon-name="mdi:ticket-outline"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-3 sm:px-4 shrink-0"
|
||||||
@click="open"
|
@click="open"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:ticket-outline" class="size-4 sm:size-5" />
|
|
||||||
<span class="hidden sm:inline">{{ $t('clientTicket.adminTab') }}</span>
|
<span class="hidden sm:inline">{{ $t('clientTicket.adminTab') }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="totalCount > 0"
|
v-if="totalCount > 0"
|
||||||
@@ -13,7 +15,7 @@
|
|||||||
>
|
>
|
||||||
{{ totalCount }}
|
{{ totalCount }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</MalioButton>
|
||||||
|
|
||||||
<!-- Panel -->
|
<!-- Panel -->
|
||||||
<Teleport v-if="isOpen" to="body">
|
<Teleport v-if="isOpen" to="body">
|
||||||
@@ -33,13 +35,13 @@
|
|||||||
<h2 class="text-base font-bold text-neutral-900">{{ $t('clientTicket.adminTab') }}</h2>
|
<h2 class="text-base font-bold text-neutral-900">{{ $t('clientTicket.adminTab') }}</h2>
|
||||||
<p class="mt-0.5 text-xs text-neutral-400">{{ projectName }}</p>
|
<p class="mt-0.5 text-xs text-neutral-400">{{ projectName }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<MalioButtonIcon
|
||||||
type="button"
|
icon="mdi:close"
|
||||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
|
aria-label="Fermer"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="20"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:close" size="20" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@@ -97,13 +99,13 @@
|
|||||||
<p class="mt-0.5 text-xs text-neutral-400">{{ formatDate(ticket.createdAt) }}</p>
|
<p class="mt-0.5 text-xs text-neutral-400">{{ formatDate(ticket.createdAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-primary-500"
|
icon="mdi:swap-horizontal"
|
||||||
:title="$t('clientTicket.changeStatus')"
|
:aria-label="$t('clientTicket.changeStatus')"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="16"
|
||||||
@click.stop="openStatusChange(ticket)"
|
@click.stop="openStatusChange(ticket)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:swap-horizontal" size="16" />
|
|
||||||
</button>
|
|
||||||
<Icon
|
<Icon
|
||||||
:name="expandedId === ticket.id ? 'mdi:chevron-up' : 'mdi:chevron-down'"
|
:name="expandedId === ticket.id ? 'mdi:chevron-up' : 'mdi:chevron-down'"
|
||||||
size="18"
|
size="18"
|
||||||
@@ -179,19 +181,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50"
|
variant="tertiary"
|
||||||
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="statusModalOpen = false"
|
@click="statusModalOpen = false"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
label="Confirmer"
|
||||||
<button
|
button-class="w-auto px-6"
|
||||||
class="rounded-lg bg-primary-500 px-6 py-2 text-sm font-semibold text-white transition-colors hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="isUpdatingStatus"
|
:disabled="isUpdatingStatus"
|
||||||
@click="confirmStatusChange"
|
@click="confirmStatusChange"
|
||||||
>
|
/>
|
||||||
Confirmer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('clients.editClient') : $t('clients.addClient')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('clients.editClient') : $t('clients.addClient')">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
@@ -35,16 +35,15 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="mt-6 flex justify-end">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Enregistrer"
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="bellRef" class="relative">
|
<div ref="bellRef" class="relative">
|
||||||
<button
|
<div class="relative">
|
||||||
type="button"
|
<MalioButtonIcon
|
||||||
class="relative rounded-md p-2 text-white hover:bg-primary-600 transition-colors"
|
icon="mdi:bell-outline"
|
||||||
@click="toggleDropdown"
|
aria-label="Notifications"
|
||||||
>
|
variant="ghost"
|
||||||
<Icon name="mdi:bell-outline" size="24" />
|
icon-size="24"
|
||||||
|
button-class="text-white hover:bg-primary-600"
|
||||||
|
@click="toggleDropdown"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="unreadCount > 0"
|
v-if="unreadCount > 0"
|
||||||
class="absolute -right-0.5 -top-0.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white"
|
class="absolute -right-0.5 -top-0.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white pointer-events-none"
|
||||||
>
|
>
|
||||||
{{ unreadCount > 99 ? '99+' : unreadCount }}
|
{{ unreadCount > 99 ? '99+' : unreadCount }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<Transition name="dropdown">
|
<Transition name="dropdown">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('projects.editProject') : $t('projects.addProject')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('projects.editProject') : $t('projects.addProject')">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.code"
|
v-model="form.code"
|
||||||
@@ -54,41 +54,44 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="mt-6 flex justify-end">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Enregistrer"
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div v-if="isEditing && project" class="mt-6 border-t border-neutral-200 pt-4 flex items-center justify-between">
|
<div v-if="isEditing && project" class="mt-6 border-t border-neutral-200 pt-4 flex items-center justify-between">
|
||||||
<button
|
<MalioButton
|
||||||
class="flex items-center gap-2 text-sm text-neutral-500 hover:text-amber-600"
|
variant="tertiary"
|
||||||
|
:icon-name="project.archived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-arrow-down-outline'"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
@click="handleArchiveToggle"
|
@click="handleArchiveToggle"
|
||||||
>
|
>
|
||||||
<Icon :name="project.archived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-arrow-down-outline'" size="18" />
|
|
||||||
{{ project.archived ? 'Désarchiver' : 'Archiver' }}
|
{{ project.archived ? 'Désarchiver' : 'Archiver' }}
|
||||||
</button>
|
</MalioButton>
|
||||||
<button
|
<MalioButton
|
||||||
v-if="project.taskCount === 0"
|
v-if="project.taskCount === 0"
|
||||||
class="flex items-center gap-2 text-sm text-neutral-500 hover:text-red-600"
|
variant="danger"
|
||||||
|
icon-name="mdi:delete-outline"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
@click="confirmDeleteOpen = true"
|
@click="confirmDeleteOpen = true"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:delete-outline" size="18" />
|
|
||||||
{{ $t('common.delete') }}
|
{{ $t('common.delete') }}
|
||||||
</button>
|
</MalioButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDeleteProjectModal
|
<ConfirmDeleteProjectModal
|
||||||
v-model="confirmDeleteOpen"
|
v-model="confirmDeleteOpen"
|
||||||
@confirm="handleDelete"
|
@confirm="handleDelete"
|
||||||
/>
|
/>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -3,20 +3,20 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">Groupes</h2>
|
<h2 class="text-lg font-bold text-neutral-900">Groupes</h2>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="text-sm font-medium text-neutral-500 hover:text-neutral-700"
|
button-class="w-auto px-3"
|
||||||
|
:label="showArchived ? $t('archive.hideArchived') : $t('archive.showArchived')"
|
||||||
@click="showArchived = !showArchived"
|
@click="showArchived = !showArchived"
|
||||||
>
|
/>
|
||||||
{{ showArchived ? $t('archive.hideArchived') : $t('archive.showArchived') }}
|
<MalioButton
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="!showArchived"
|
v-if="!showArchived"
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-4"
|
||||||
|
label="Ajouter un groupe"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
/>
|
||||||
+ Ajouter un groupe
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -39,22 +39,20 @@
|
|||||||
{{ item.description ?? '—' }}
|
{{ item.description ?? '—' }}
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ item }">
|
<template #actions="{ item }">
|
||||||
<button
|
<MalioButton
|
||||||
v-if="!showArchived && canArchiveGroup(item)"
|
v-if="!showArchived && canArchiveGroup(item)"
|
||||||
type="button"
|
variant="secondary"
|
||||||
class="rounded-md bg-neutral-500 px-3 py-1 text-xs font-semibold text-white hover:bg-neutral-600"
|
:label="$t('archive.archiveButton')"
|
||||||
|
button-class="w-auto px-3"
|
||||||
@click.stop="handleArchive(item)"
|
@click.stop="handleArchive(item)"
|
||||||
>
|
/>
|
||||||
{{ $t('archive.archiveButton') }}
|
<MalioButton
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showArchived"
|
v-if="showArchived"
|
||||||
type="button"
|
variant="secondary"
|
||||||
class="rounded-md bg-neutral-500 px-3 py-1 text-xs font-semibold text-white hover:bg-neutral-600"
|
:label="$t('archive.unarchiveButton')"
|
||||||
|
button-class="w-auto px-3"
|
||||||
@click.stop="handleUnarchive(item)"
|
@click.stop="handleUnarchive(item)"
|
||||||
>
|
/>
|
||||||
{{ $t('archive.unarchiveButton') }}
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
|
|||||||
@@ -57,13 +57,14 @@
|
|||||||
>
|
>
|
||||||
{{ link.title }}
|
{{ link.title }}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<MalioButtonIcon
|
||||||
type="button"
|
icon="mdi:close"
|
||||||
class="ml-auto shrink-0 text-neutral-300 hover:text-red-500"
|
aria-label="Supprimer le lien"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="16"
|
||||||
|
button-class="ml-auto shrink-0 text-neutral-300 hover:text-red-500"
|
||||||
@click="handleRemove(link.id)"
|
@click="handleRemove(link.id)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:close" size="16" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -72,13 +72,14 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="flex h-9 w-9 shrink-0 items-center justify-center self-end rounded-md text-neutral-500 transition-colors hover:bg-red-50 hover:text-red-500"
|
icon="mdi:delete-outline"
|
||||||
title="Supprimer"
|
aria-label="Supprimer"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="22"
|
||||||
|
button-class="self-end text-neutral-500 hover:bg-red-50 hover:text-red-500"
|
||||||
@click="emit('bulk-delete')"
|
@click="emit('bulk-delete')"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:delete-outline" size="22" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -29,13 +29,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<h4 class="text-sm font-semibold text-neutral-900">{{ task.title }}</h4>
|
<h4 class="text-sm font-semibold text-neutral-900">{{ task.title }}</h4>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="shrink-0 transition-colors"
|
:icon="isTimerOnTask ? 'mdi:stop-circle-outline' : 'mdi:play-circle-outline'"
|
||||||
:class="isTimerOnTask ? 'text-[#F18619] hover:text-[#d97314]' : 'text-neutral-400 hover:text-primary-500'"
|
:aria-label="isTimerOnTask ? 'Arrêter le timer' : 'Démarrer le timer'"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="20"
|
||||||
|
:button-class="isTimerOnTask ? 'shrink-0 text-[#F18619] hover:text-[#d97314]' : 'shrink-0 text-neutral-400 hover:text-primary-500'"
|
||||||
@click.stop="isTimerOnTask ? timerStore.stop() : onPlay()"
|
@click.stop="isTimerOnTask ? timerStore.stop() : onPlay()"
|
||||||
>
|
/>
|
||||||
<Icon :name="isTimerOnTask ? 'mdi:stop-circle-outline' : 'mdi:play-circle-outline'" size="20" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 flex items-center gap-1.5">
|
<div class="mt-2 flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -32,14 +32,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete button -->
|
<!-- Delete button -->
|
||||||
<button
|
<MalioButtonIcon
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
type="button"
|
icon="heroicons:x-mark"
|
||||||
class="absolute right-1 top-1 hidden rounded p-0.5 text-neutral-400 transition-colors hover:bg-red-50 hover:text-red-500 group-hover:block"
|
aria-label="Supprimer"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="16"
|
||||||
|
button-class="absolute right-1 top-1 hidden text-neutral-400 hover:bg-red-50 hover:text-red-500 group-hover:block"
|
||||||
@click.stop="$emit('delete', doc)"
|
@click.stop="$emit('delete', doc)"
|
||||||
>
|
/>
|
||||||
<Icon name="heroicons:x-mark" class="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,28 +12,34 @@
|
|||||||
ref="overlayRef"
|
ref="overlayRef"
|
||||||
>
|
>
|
||||||
<!-- Close button -->
|
<!-- Close button -->
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="absolute right-4 top-4 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70"
|
icon="heroicons:x-mark"
|
||||||
|
aria-label="Fermer"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="24"
|
||||||
|
button-class="absolute right-4 top-4 rounded-full bg-black/50 text-white hover:bg-black/70"
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
>
|
/>
|
||||||
<Icon name="heroicons:x-mark" class="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Navigation arrows -->
|
<!-- Navigation arrows -->
|
||||||
<button
|
<MalioButtonIcon
|
||||||
v-if="hasPrev"
|
v-if="hasPrev"
|
||||||
class="absolute left-4 top-1/2 -translate-y-1/2 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70"
|
icon="heroicons:chevron-left"
|
||||||
|
aria-label="Précédent"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="24"
|
||||||
|
button-class="absolute left-4 top-1/2 -translate-y-1/2 rounded-full bg-black/50 text-white hover:bg-black/70"
|
||||||
@click="$emit('prev')"
|
@click="$emit('prev')"
|
||||||
>
|
/>
|
||||||
<Icon name="heroicons:chevron-left" class="h-6 w-6" />
|
<MalioButtonIcon
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="hasNext"
|
v-if="hasNext"
|
||||||
class="absolute right-4 top-1/2 -translate-y-1/2 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70"
|
icon="heroicons:chevron-right"
|
||||||
|
aria-label="Suivant"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="24"
|
||||||
|
button-class="absolute right-4 top-1/2 -translate-y-1/2 rounded-full bg-black/50 text-white hover:bg-black/70"
|
||||||
@click="$emit('next')"
|
@click="$emit('next')"
|
||||||
>
|
/>
|
||||||
<Icon name="heroicons:chevron-right" class="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex max-h-[90vh] max-w-[90vw] flex-col items-center">
|
<div class="flex max-h-[90vh] max-w-[90vw] flex-col items-center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('taskEfforts.editEffort') : $t('taskEfforts.addEffort')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('taskEfforts.editEffort') : $t('taskEfforts.addEffort')">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
@@ -10,16 +10,15 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="mt-6 flex justify-end">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Enregistrer"
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -38,24 +38,22 @@
|
|||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
<MalioButtonIcon
|
||||||
v-if="activeTab === 'branches'"
|
v-if="activeTab === 'branches'"
|
||||||
type="button"
|
icon="mdi:content-copy"
|
||||||
class="rounded-md px-2.5 py-1.5 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-200/60 hover:text-neutral-700"
|
:aria-label="$t('gitea.branch.copy')"
|
||||||
:title="$t('gitea.branch.copy')"
|
variant="ghost"
|
||||||
|
icon-size="14"
|
||||||
@click="handleCopy"
|
@click="handleCopy"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:content-copy" size="14" />
|
<MalioButton
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="activeTab === 'branches'"
|
v-if="activeTab === 'branches'"
|
||||||
type="button"
|
icon-name="mdi:plus"
|
||||||
class="rounded-md bg-primary-500 px-2.5 py-1.5 text-xs font-semibold text-white transition-colors hover:bg-secondary-500"
|
icon-position="left"
|
||||||
|
button-class="w-auto px-2.5 py-1.5 text-xs"
|
||||||
|
:label="$t('gitea.branch.create')"
|
||||||
@click="showCreateForm = !showCreateForm"
|
@click="showCreateForm = !showCreateForm"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:plus" size="14" class="mr-0.5 inline-block align-[-2px]" />
|
|
||||||
{{ $t('gitea.branch.create') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -79,14 +77,12 @@
|
|||||||
:label="$t('gitea.branch.baseBranch')"
|
:label="$t('gitea.branch.baseBranch')"
|
||||||
input-class="w-full"
|
input-class="w-full"
|
||||||
/>
|
/>
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
:label="isCreating ? '...' : $t('gitea.branch.create')"
|
||||||
class="mb-[2px] rounded-md bg-primary-500 px-4 py-2 text-xs font-semibold text-white transition-colors hover:bg-secondary-500 disabled:opacity-50"
|
button-class="w-auto px-4 mb-[2px] text-xs"
|
||||||
:disabled="isCreating"
|
:disabled="isCreating"
|
||||||
@click="handleCreate"
|
@click="handleCreate"
|
||||||
>
|
/>
|
||||||
{{ isCreating ? '...' : $t('gitea.branch.create') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<code class="mt-2 block rounded bg-neutral-50 px-2 py-1 text-[11px] text-neutral-500">
|
<code class="mt-2 block rounded bg-neutral-50 px-2 py-1 text-[11px] text-neutral-500">
|
||||||
{{ branchPreview }}
|
{{ branchPreview }}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('taskGroups.editGroup') : $t('taskGroups.addGroup')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('taskGroups.editGroup') : $t('taskGroups.addGroup')">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.title"
|
v-model="form.title"
|
||||||
@@ -25,34 +25,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex items-center" :class="isEditing ? 'justify-between' : 'justify-end'">
|
<div class="mt-6 flex items-center" :class="isEditing ? 'justify-between' : 'justify-end'">
|
||||||
<button
|
<MalioButton
|
||||||
v-if="canArchive"
|
v-if="canArchive"
|
||||||
type="button"
|
variant="secondary"
|
||||||
class="rounded-md bg-neutral-500 px-4 py-2 text-sm font-semibold text-white hover:bg-neutral-600 disabled:cursor-not-allowed disabled:opacity-50"
|
:label="$t('archive.archiveButton')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
@click="handleArchive"
|
@click="handleArchive"
|
||||||
>
|
/>
|
||||||
{{ $t('archive.archiveButton') }}
|
<MalioButton
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="canUnarchive"
|
v-if="canUnarchive"
|
||||||
type="button"
|
variant="secondary"
|
||||||
class="rounded-md bg-neutral-500 px-4 py-2 text-sm font-semibold text-white hover:bg-neutral-600 disabled:cursor-not-allowed disabled:opacity-50"
|
:label="$t('archive.unarchiveButton')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
@click="handleUnarchive"
|
@click="handleUnarchive"
|
||||||
>
|
/>
|
||||||
{{ $t('archive.unarchiveButton') }}
|
<MalioButton
|
||||||
</button>
|
label="Enregistrer"
|
||||||
<button
|
button-class="w-auto px-6"
|
||||||
type="submit"
|
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -78,13 +78,14 @@
|
|||||||
|
|
||||||
<!-- Right: timer top, avatar bottom -->
|
<!-- Right: timer top, avatar bottom -->
|
||||||
<div class="flex shrink-0 flex-col items-end justify-between self-stretch gap-1">
|
<div class="flex shrink-0 flex-col items-end justify-between self-stretch gap-1">
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="shrink-0 transition-colors"
|
:icon="isTimerOnTask ? 'mdi:stop-circle-outline' : 'mdi:play-circle-outline'"
|
||||||
:class="isTimerOnTask ? 'text-[#F18619] hover:text-[#d97314]' : 'text-neutral-400 hover:text-primary-500'"
|
:aria-label="isTimerOnTask ? 'Arrêter le timer' : 'Démarrer le timer'"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="20"
|
||||||
|
:button-class="isTimerOnTask ? 'shrink-0 text-[#F18619] hover:text-[#d97314]' : 'shrink-0 text-neutral-400 hover:text-primary-500'"
|
||||||
@click.stop="isTimerOnTask ? timerStore.stop() : timerStore.startFromTask(task)"
|
@click.stop="isTimerOnTask ? timerStore.stop() : timerStore.startFromTask(task)"
|
||||||
>
|
/>
|
||||||
<Icon :name="isTimerOnTask ? 'mdi:stop-circle-outline' : 'mdi:play-circle-outline'" size="20" />
|
|
||||||
</button>
|
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
v-if="task.assignee"
|
v-if="task.assignee"
|
||||||
:user="task.assignee"
|
:user="task.assignee"
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
{{ isEditing ? $t('tasks.editTask') : $t('tasks.addTask') }}
|
{{ isEditing ? $t('tasks.editTask') : $t('tasks.addTask') }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<MalioButtonIcon
|
||||||
type="button"
|
icon="mdi:close"
|
||||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-neutral-400 transition-colors hover:bg-neutral-200/60 hover:text-neutral-600"
|
aria-label="Fermer"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="20"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:close" size="20" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Client ticket link -->
|
<!-- Client ticket link -->
|
||||||
@@ -417,48 +417,43 @@
|
|||||||
class="mt-6 flex items-center border-t border-neutral-100 pt-5"
|
class="mt-6 flex items-center border-t border-neutral-100 pt-5"
|
||||||
:class="isEditing ? 'justify-between' : 'justify-end'"
|
:class="isEditing ? 'justify-between' : 'justify-end'"
|
||||||
>
|
>
|
||||||
<button
|
<MalioButton
|
||||||
v-if="isEditing"
|
v-if="isEditing"
|
||||||
type="button"
|
variant="danger"
|
||||||
class="rounded-lg bg-red-50 px-4 py-2 text-sm font-semibold text-red-600 transition-colors hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50"
|
label="Supprimer"
|
||||||
|
button-class="w-auto px-4"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
@click="confirmDeleteOpen = true"
|
@click="confirmDeleteOpen = true"
|
||||||
>
|
/>
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
v-if="canArchive"
|
v-if="canArchive"
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-50"
|
:label="$t('archive.archiveButton')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
@click="handleArchive"
|
@click="handleArchive"
|
||||||
>
|
/>
|
||||||
{{ $t('archive.archiveButton') }}
|
<MalioButton
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="canUnarchive"
|
v-if="canUnarchive"
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-50"
|
:label="$t('archive.unarchiveButton')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
@click="handleUnarchive"
|
@click="handleUnarchive"
|
||||||
>
|
/>
|
||||||
{{ $t('archive.unarchiveButton') }}
|
<MalioButton
|
||||||
</button>
|
variant="tertiary"
|
||||||
<button
|
label="Annuler"
|
||||||
type="button"
|
button-class="w-auto px-4"
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50"
|
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
/>
|
||||||
Annuler
|
<MalioButton
|
||||||
</button>
|
label="Enregistrer"
|
||||||
<button
|
button-class="w-auto px-6"
|
||||||
type="submit"
|
|
||||||
class="rounded-lg bg-primary-500 px-6 py-2 text-sm font-semibold text-white transition-colors hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('taskPriorities.editPriority') : $t('taskPriorities.addPriority')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('taskPriorities.editPriority') : $t('taskPriorities.addPriority')">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
@@ -13,16 +13,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="mt-6 flex justify-end">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Enregistrer"
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('taskStatuses.editStatus') : $t('taskStatuses.addStatus')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('taskStatuses.editStatus') : $t('taskStatuses.addStatus')">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
@@ -31,16 +31,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="mt-6 flex justify-end">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Enregistrer"
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('taskTags.editTag') : $t('taskTags.addTag')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('taskTags.editTag') : $t('taskTags.addTag')">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
@@ -13,16 +13,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="mt-6 flex justify-end">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Enregistrer"
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('timeEntries.editEntry') : $t('timeEntries.addEntry')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('timeEntries.editEntry') : $t('timeEntries.addEntry')">
|
||||||
<form class="space-y-4" @submit.prevent="onSubmit">
|
<form class="space-y-4" @submit.prevent="onSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-semibold text-neutral-700">Titre</label>
|
<label class="mb-1 block text-sm font-semibold text-neutral-700">Titre</label>
|
||||||
@@ -97,33 +97,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center" :class="isEditing ? 'justify-between' : 'justify-end'">
|
<div class="flex items-center" :class="isEditing ? 'justify-between' : 'justify-end'">
|
||||||
<button
|
<MalioButton
|
||||||
v-if="isEditing"
|
v-if="isEditing"
|
||||||
type="button"
|
variant="danger"
|
||||||
class="rounded-md bg-red-500 px-4 py-2 text-sm font-semibold text-white hover:bg-red-600 transition"
|
label="Supprimer"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="onDelete"
|
@click="onDelete"
|
||||||
>
|
/>
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<MalioButton
|
||||||
v-if="isEditing"
|
v-if="isEditing"
|
||||||
type="button"
|
variant="secondary"
|
||||||
class="rounded-md bg-blue-500 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-600 transition"
|
label="Dupliquer"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="onDuplicate"
|
@click="onDuplicate"
|
||||||
>
|
/>
|
||||||
Dupliquer
|
<MalioButton
|
||||||
</button>
|
label="Enregistrer"
|
||||||
<button
|
button-class="w-auto px-4"
|
||||||
type="submit"
|
@click="onSubmit"
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-600 transition"
|
/>
|
||||||
>
|
|
||||||
Enregistrer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -54,13 +54,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete action -->
|
<!-- Delete action -->
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="shrink-0 rounded-md p-1.5 text-neutral-300 opacity-0 transition hover:bg-red-50 hover:text-red-500 group-hover:opacity-100"
|
icon="mdi:delete-outline"
|
||||||
:title="$t('common.delete')"
|
:aria-label="$t('common.delete')"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="18"
|
||||||
|
button-class="shrink-0 text-neutral-300 opacity-0 hover:bg-red-50 hover:text-red-500 group-hover:opacity-100"
|
||||||
@click.stop="emit('deleteEntry', entry)"
|
@click.stop="emit('deleteEntry', entry)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:delete-outline" size="18" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
205
frontend/components/time-tracking/TimeTrackingExportDrawer.vue
Normal file
205
frontend/components/time-tracking/TimeTrackingExportDrawer.vue
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<MalioDrawer v-model="isOpen" :title="$t('timeEntries.exportTitle')" drawer-class="max-w-lg">
|
||||||
|
<div class="flex flex-col gap-6 p-4">
|
||||||
|
<!-- Period presets -->
|
||||||
|
<div>
|
||||||
|
<p class="mb-2 text-sm font-semibold text-neutral-700">Période</p>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<MalioRadioButton
|
||||||
|
v-model="periodMode"
|
||||||
|
name="exportPeriod"
|
||||||
|
value="currentMonth"
|
||||||
|
:label="$t('timeEntries.exportCurrentMonth')"
|
||||||
|
/>
|
||||||
|
<MalioRadioButton
|
||||||
|
v-model="periodMode"
|
||||||
|
name="exportPeriod"
|
||||||
|
value="lastMonth"
|
||||||
|
:label="$t('timeEntries.exportLastMonth')"
|
||||||
|
/>
|
||||||
|
<MalioRadioButton
|
||||||
|
v-model="periodMode"
|
||||||
|
name="exportPeriod"
|
||||||
|
value="custom"
|
||||||
|
:label="$t('timeEntries.exportCustomPeriod')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="periodMode === 'custom'" class="mt-3 flex items-center gap-3">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="mb-1 block text-xs text-neutral-500">{{ $t('timeEntries.exportFrom') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="customFrom"
|
||||||
|
type="date"
|
||||||
|
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="mb-1 block text-xs text-neutral-500">{{ $t('timeEntries.exportTo') }}</label>
|
||||||
|
<input
|
||||||
|
v-model="customTo"
|
||||||
|
type="date"
|
||||||
|
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User filter (admin only) -->
|
||||||
|
<div v-if="isAdmin" class="[&>div]:!mt-0">
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
v-model="selectedUserIds"
|
||||||
|
:options="userOptions"
|
||||||
|
:label="$t('timeEntries.exportUsers')"
|
||||||
|
:display-tag="true"
|
||||||
|
:display-select-all="true"
|
||||||
|
min-width="!w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Client filter -->
|
||||||
|
<div class="[&>div]:!mt-0">
|
||||||
|
<MalioSelect
|
||||||
|
v-model="selectedClientId"
|
||||||
|
:options="clientOptions"
|
||||||
|
:label="$t('timeEntries.exportClient')"
|
||||||
|
:empty-option-label="$t('timeEntries.exportAllClients')"
|
||||||
|
min-width="!w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Project filter -->
|
||||||
|
<div class="[&>div]:!mt-0">
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
v-model="selectedProjectIds"
|
||||||
|
:options="filteredProjectOptions"
|
||||||
|
:label="$t('timeEntries.exportProjects')"
|
||||||
|
:display-tag="true"
|
||||||
|
:display-select-all="true"
|
||||||
|
min-width="!w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tag filter -->
|
||||||
|
<div class="[&>div]:!mt-0">
|
||||||
|
<MalioSelectCheckbox
|
||||||
|
v-model="selectedTagIds"
|
||||||
|
:options="tagOptions"
|
||||||
|
:label="$t('timeEntries.exportTags')"
|
||||||
|
:display-tag="true"
|
||||||
|
:display-select-all="true"
|
||||||
|
min-width="!w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export button -->
|
||||||
|
<MalioButton
|
||||||
|
:label="$t('timeEntries.export')"
|
||||||
|
icon-name="mdi:download"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-full"
|
||||||
|
@click="doExport"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</MalioDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type { Project } from '~/services/dto/project'
|
||||||
|
import type { TaskTag } from '~/services/dto/task-tag'
|
||||||
|
import type { Client } from '~/services/dto/client'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
users: UserData[]
|
||||||
|
projects: Project[]
|
||||||
|
tags: TaskTag[]
|
||||||
|
clients: Client[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isOpen = defineModel<boolean>({ default: false })
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'export', params: {
|
||||||
|
after: string
|
||||||
|
before: string
|
||||||
|
users?: number[]
|
||||||
|
projects?: number[]
|
||||||
|
client?: number
|
||||||
|
tags?: number[]
|
||||||
|
}): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const isAdmin = computed(() => authStore.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
||||||
|
|
||||||
|
const periodMode = ref<'currentMonth' | 'lastMonth' | 'custom'>('currentMonth')
|
||||||
|
const customFrom = ref('')
|
||||||
|
const customTo = ref('')
|
||||||
|
const selectedUserIds = ref<number[]>([])
|
||||||
|
const selectedClientId = ref<number | null>(null)
|
||||||
|
const selectedProjectIds = ref<number[]>([])
|
||||||
|
const selectedTagIds = ref<number[]>([])
|
||||||
|
|
||||||
|
const userOptions = computed(() =>
|
||||||
|
props.users.map(u => ({ label: u.username, value: u.id }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const clientOptions = computed(() =>
|
||||||
|
props.clients.map(c => ({ label: c.name, value: c.id }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredProjectOptions = computed(() => {
|
||||||
|
let list = props.projects
|
||||||
|
if (selectedClientId.value) {
|
||||||
|
list = list.filter(p => p.client?.id === selectedClientId.value)
|
||||||
|
}
|
||||||
|
return list.map(p => ({ label: p.name, value: p.id }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const tagOptions = computed(() =>
|
||||||
|
props.tags.map(t => ({ label: t.label, value: t.id }))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reset project selection when client changes
|
||||||
|
watch(selectedClientId, () => {
|
||||||
|
selectedProjectIds.value = []
|
||||||
|
})
|
||||||
|
|
||||||
|
function getDateRange(): { after: string; before: string } {
|
||||||
|
const now = new Date()
|
||||||
|
if (periodMode.value === 'currentMonth') {
|
||||||
|
const first = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||||
|
const last = new Date(now.getFullYear(), now.getMonth() + 1, 1)
|
||||||
|
return {
|
||||||
|
after: first.toISOString().slice(0, 10),
|
||||||
|
before: last.toISOString().slice(0, 10),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (periodMode.value === 'lastMonth') {
|
||||||
|
const first = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||||||
|
const last = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||||
|
return {
|
||||||
|
after: first.toISOString().slice(0, 10),
|
||||||
|
before: last.toISOString().slice(0, 10),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
after: customFrom.value,
|
||||||
|
before: customTo.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doExport() {
|
||||||
|
const { after, before } = getDateRange()
|
||||||
|
if (!after || !before) return
|
||||||
|
|
||||||
|
emit('export', {
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
users: selectedUserIds.value.length ? selectedUserIds.value : undefined,
|
||||||
|
projects: selectedProjectIds.value.length ? selectedProjectIds.value : undefined,
|
||||||
|
client: selectedClientId.value ?? undefined,
|
||||||
|
tags: selectedTagIds.value.length ? selectedTagIds.value : undefined,
|
||||||
|
})
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center justify-between border-b border-neutral-200 px-6 py-4">
|
<div class="flex items-center justify-between border-b border-neutral-200 px-6 py-4">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">{{ title }}</h2>
|
<h2 class="text-lg font-bold text-neutral-900">{{ title }}</h2>
|
||||||
<button
|
<MalioButtonIcon
|
||||||
type="button"
|
icon="mdi:close"
|
||||||
class="rounded p-1 text-neutral-400 hover:text-neutral-600"
|
aria-label="Fermer"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="24"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:close" size="24" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-y-auto px-6 py-4">
|
<div class="flex-1 overflow-y-auto px-6 py-4">
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,32 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="border-b border-neutral-200 bg-primary-500 px-3 py-2 text-white sm:px-5 sm:py-2 max-h-[60px]">
|
<header class="border-b border-neutral-200 bg-primary-500 px-3 py-2 text-white sm:px-5 sm:py-2 max-h-[60px]">
|
||||||
<div class="flex h-full items-center justify-between">
|
<div class="flex h-full items-center justify-between">
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="rounded-md p-2 text-white hover:bg-primary-600 transition-colors lg:hidden"
|
icon="mdi:menu"
|
||||||
|
aria-label="Menu"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="24"
|
||||||
|
button-class="lg:hidden text-white hover:bg-primary-600"
|
||||||
@click="ui.openMobileSidebar()"
|
@click="ui.openMobileSidebar()"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:menu" size="24" />
|
|
||||||
</button>
|
|
||||||
<div class="hidden items-center gap-2 lg:flex">
|
<div class="hidden items-center gap-2 lg:flex">
|
||||||
<h1 class="text-lg font-bold tracking-tight">{{ appTitle }}</h1>
|
<h1 class="text-lg font-bold tracking-tight">{{ appTitle }}</h1>
|
||||||
<button
|
<MalioButtonIcon
|
||||||
type="button"
|
icon="mdi:swap-horizontal"
|
||||||
class="rounded-md p-1 text-white/60 transition-colors hover:bg-primary-600 hover:text-white"
|
:aria-label="appTitle === 'NeauTime' ? 'Switch to Lesstime' : 'Switch to NeauTime'"
|
||||||
:title="appTitle === 'NeauTime' ? 'Switch to Lesstime' : 'Switch to NeauTime'"
|
variant="ghost"
|
||||||
|
icon-size="18"
|
||||||
|
button-class="text-white/60 hover:bg-primary-600 hover:text-white"
|
||||||
@click="toggleTitle"
|
@click="toggleTitle"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:swap-horizontal" size="18" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto flex items-center gap-4 text-xl text-white sm:gap-8">
|
<div class="ml-auto flex items-center gap-4 text-xl text-white sm:gap-8">
|
||||||
<button
|
<MalioButtonIcon
|
||||||
type="button"
|
:icon="ui.darkMode ? 'mdi:weather-sunny' : 'mdi:weather-night'"
|
||||||
class="rounded-md p-1.5 text-white/70 transition-colors hover:bg-primary-600 hover:text-white"
|
:aria-label="ui.darkMode ? 'Mode clair' : 'Mode sombre'"
|
||||||
:title="ui.darkMode ? 'Mode clair' : 'Mode sombre'"
|
variant="ghost"
|
||||||
|
icon-size="22"
|
||||||
|
button-class="text-white/70 hover:bg-primary-600 hover:text-white"
|
||||||
@click="ui.toggleDarkMode()"
|
@click="ui.toggleDarkMode()"
|
||||||
>
|
/>
|
||||||
<Icon :name="ui.darkMode ? 'mdi:weather-sunny' : 'mdi:weather-night'" size="22" />
|
|
||||||
</button>
|
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
<div class="group relative flex gap-2 sm:gap-4">
|
<div class="group relative flex gap-2 sm:gap-4">
|
||||||
<UserAvatar v-if="user" :user="user" size="md" class="cursor-pointer" />
|
<UserAvatar v-if="user" :user="user" size="md" class="cursor-pointer" />
|
||||||
|
|||||||
@@ -9,20 +9,18 @@
|
|||||||
{{ $t('taskDocuments.confirmDeleteMessage') }}
|
{{ $t('taskDocuments.confirmDeleteMessage') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
variant="danger"
|
||||||
<button
|
label="Supprimer"
|
||||||
type="button"
|
button-class="w-auto px-4"
|
||||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700"
|
|
||||||
@click="$emit('confirm')"
|
@click="$emit('confirm')"
|
||||||
>
|
/>
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,20 +9,18 @@
|
|||||||
{{ $t('projects.deleteConfirmMessage') }}
|
{{ $t('projects.deleteConfirmMessage') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
variant="danger"
|
||||||
<button
|
:label="$t('common.delete')"
|
||||||
type="button"
|
button-class="w-auto px-4"
|
||||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700"
|
|
||||||
@click="$emit('confirm')"
|
@click="$emit('confirm')"
|
||||||
>
|
/>
|
||||||
{{ $t('common.delete') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,21 +21,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
variant="danger"
|
||||||
<button
|
:label="$t('common.delete')"
|
||||||
type="button"
|
button-class="w-auto px-4"
|
||||||
class="rounded-md bg-[red-600] px-4 py-2 text-sm font-semibold text-white hover:bg-[red-700] disabled:opacity-50"
|
|
||||||
:disabled="isProcessing"
|
:disabled="isProcessing"
|
||||||
@click="confirm"
|
@click="confirm"
|
||||||
>
|
/>
|
||||||
{{ $t('common.delete') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,20 +9,18 @@
|
|||||||
{{ $t('tasks.deleteConfirmMessage') }}
|
{{ $t('tasks.deleteConfirmMessage') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
|
label="Annuler"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
>
|
/>
|
||||||
Annuler
|
<MalioButton
|
||||||
</button>
|
variant="danger"
|
||||||
<button
|
label="Supprimer"
|
||||||
type="button"
|
button-class="w-auto px-4"
|
||||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700"
|
|
||||||
@click="$emit('confirm')"
|
@click="$emit('confirm')"
|
||||||
>
|
/>
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,13 +35,15 @@
|
|||||||
<td v-if="deletable || $slots.actions" class="px-4 py-3">
|
<td v-if="deletable || $slots.actions" class="px-4 py-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<slot name="actions" :item="item" />
|
<slot name="actions" :item="item" />
|
||||||
<button
|
<MalioButtonIcon
|
||||||
v-if="deletable"
|
v-if="deletable"
|
||||||
class="text-neutral-400 transition-colors hover:text-red-500"
|
icon="mdi:delete-outline"
|
||||||
|
aria-label="Supprimer"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="20"
|
||||||
|
button-class="text-neutral-400 hover:text-red-500"
|
||||||
@click.stop="$emit('delete', item)"
|
@click.stop="$emit('delete', item)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:delete-outline" size="20" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -18,21 +18,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="emit('cancel')"
|
@click="emit('cancel')"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
:label="$t('common.confirm')"
|
||||||
<button
|
button-class="w-auto px-4"
|
||||||
type="button"
|
|
||||||
class="rounded-lg bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
|
||||||
:disabled="cropping"
|
:disabled="cropping"
|
||||||
@click="onConfirm"
|
@click="onConfirm"
|
||||||
>
|
/>
|
||||||
{{ $t('common.confirm') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? $t('users.editUser') : $t('users.addUser')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('users.editUser') : $t('users.addUser')">
|
||||||
<form class="flex flex-col gap-2" @submit.prevent="handleSubmit">
|
<form class="flex flex-col gap-2" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
@@ -70,16 +70,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="mt-6 flex justify-end">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Enregistrer"
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Enregistrer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -162,7 +162,21 @@
|
|||||||
"noEntries": "Aucune activité pour cette période",
|
"noEntries": "Aucune activité pour cette période",
|
||||||
"addEntry": "Ajouter une Activité",
|
"addEntry": "Ajouter une Activité",
|
||||||
"editEntry": "Modifier un temps",
|
"editEntry": "Modifier un temps",
|
||||||
"export": "Exporter"
|
"export": "Exporter",
|
||||||
|
"exportTitle": "Exporter les temps",
|
||||||
|
"exportCurrentMonth": "Mois en cours",
|
||||||
|
"exportLastMonth": "Mois dernier",
|
||||||
|
"exportCustomPeriod": "Période personnalisée",
|
||||||
|
"exportFrom": "Du",
|
||||||
|
"exportTo": "Au",
|
||||||
|
"exportUsers": "Utilisateurs",
|
||||||
|
"exportClient": "Client",
|
||||||
|
"exportProjects": "Projets",
|
||||||
|
"exportTags": "Tags",
|
||||||
|
"exportAllClients": "Tous les clients",
|
||||||
|
"exportLoading": "Export en cours...",
|
||||||
|
"exportSuccess": "Export terminé !",
|
||||||
|
"exportError": "Erreur lors de l'export."
|
||||||
},
|
},
|
||||||
"archive": {
|
"archive": {
|
||||||
"title": "Archives",
|
"title": "Archives",
|
||||||
|
|||||||
101
frontend/package-lock.json
generated
101
frontend/package-lock.json
generated
@@ -7,7 +7,7 @@
|
|||||||
"name": "nuxt-app",
|
"name": "nuxt-app",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@malio/layer-ui": "^1.1.0",
|
"@malio/layer-ui": "^1.2.0",
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@nuxtjs/i18n": "^10.2.3",
|
"@nuxtjs/i18n": "^10.2.3",
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
@@ -76,7 +76,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -1038,6 +1037,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
@@ -1047,6 +1047,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz",
|
||||||
"integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==",
|
"integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/object-schema": "^3.0.3",
|
"@eslint/object-schema": "^3.0.3",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
@@ -1061,6 +1062,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
|
||||||
"integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
|
"integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^1.1.1"
|
"@eslint/core": "^1.1.1"
|
||||||
},
|
},
|
||||||
@@ -1073,6 +1075,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
|
||||||
"integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
|
"integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.15"
|
"@types/json-schema": "^7.0.15"
|
||||||
},
|
},
|
||||||
@@ -1085,6 +1088,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
|
||||||
"integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
|
"integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
}
|
}
|
||||||
@@ -1094,6 +1098,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
|
||||||
"integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
|
"integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^1.1.1",
|
"@eslint/core": "^1.1.1",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
@@ -1169,6 +1174,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.0"
|
"node": ">=18.18.0"
|
||||||
}
|
}
|
||||||
@@ -1178,6 +1184,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
||||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@humanfs/core": "^0.19.1",
|
"@humanfs/core": "^0.19.1",
|
||||||
"@humanwhocodes/retry": "^0.4.0"
|
"@humanwhocodes/retry": "^0.4.0"
|
||||||
@@ -1191,6 +1198,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.22"
|
"node": ">=12.22"
|
||||||
},
|
},
|
||||||
@@ -1204,6 +1212,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
||||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18"
|
"node": ">=18.18"
|
||||||
},
|
},
|
||||||
@@ -2203,9 +2212,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@malio/layer-ui": {
|
"node_modules/@malio/layer-ui": {
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.1.0/layer-ui-1.1.0.tgz",
|
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.2.0/layer-ui-1.2.0.tgz",
|
||||||
"integrity": "sha512-mc+kOK+EDfo6ZZcE0/FaVnvDyIDJrigkgOzvL8rxnpljXEiRlKj5673e5e6ZIoOyKFqktzbJXzFr4V6UBD0wPg==",
|
"integrity": "sha512-/D/p7Tz5t8xsZ+qL4kwBs2XXA/yNJpwF5C8pbSrz06Z8Je/Yut2J4KT1YpPHcfyFFE3TB8TpV0Okg/29aN6Ggg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
@@ -2483,7 +2492,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.3.1.tgz",
|
||||||
"integrity": "sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA==",
|
"integrity": "sha512-UjBFt72dnpc+83BV3OIbCT0YHLevJtgJCHpxMX0YRKWLDhhbcDdUse87GtsQBrjvOzK7WUNUYLDS/hQLYev5rA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"c12": "^3.3.3",
|
"c12": "^3.3.3",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
@@ -2556,7 +2564,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.3.1.tgz",
|
||||||
"integrity": "sha512-S+wHJdYDuyk9I43Ej27y5BeWMZgi7R/UVql3b3qtT35d0fbpXW7fUenzhLRCCDC6O10sjguc6fcMcR9sMKvV8g==",
|
"integrity": "sha512-S+wHJdYDuyk9I43Ej27y5BeWMZgi7R/UVql3b3qtT35d0fbpXW7fUenzhLRCCDC6O10sjguc6fcMcR9sMKvV8g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "^3.5.27",
|
"@vue/shared": "^3.5.27",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
@@ -3203,7 +3210,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.95.0.tgz",
|
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.95.0.tgz",
|
||||||
"integrity": "sha512-Te8fE/SmiiKWIrwBwxz5Dod87uYvsbcZ9JAL5ylPg1DevyKgTkxCXnPEaewk1Su2qpfNmry5RHoN+NywWFCG+A==",
|
"integrity": "sha512-Te8fE/SmiiKWIrwBwxz5Dod87uYvsbcZ9JAL5ylPg1DevyKgTkxCXnPEaewk1Su2qpfNmry5RHoN+NywWFCG+A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "^0.95.0"
|
"@oxc-project/types": "^0.95.0"
|
||||||
},
|
},
|
||||||
@@ -5309,7 +5315,8 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||||
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
@@ -5321,7 +5328,8 @@
|
|||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
@@ -5662,7 +5670,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz",
|
||||||
"integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==",
|
"integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.29.0",
|
||||||
"@vue/compiler-core": "3.5.29",
|
"@vue/compiler-core": "3.5.29",
|
||||||
@@ -5912,7 +5919,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5952,6 +5958,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -6283,7 +6290,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"bare-abort-controller": "*"
|
"bare-abort-controller": "*"
|
||||||
},
|
},
|
||||||
@@ -6477,7 +6483,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -6606,7 +6611,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -6748,7 +6752,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
},
|
},
|
||||||
@@ -6785,7 +6788,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"consola": "^3.2.3"
|
"consola": "^3.2.3"
|
||||||
}
|
}
|
||||||
@@ -7341,7 +7343,8 @@
|
|||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
@@ -7847,6 +7850,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||||
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/esrecurse": "^4.3.1",
|
"@types/esrecurse": "^4.3.1",
|
||||||
"@types/estree": "^1.0.8",
|
"@types/estree": "^1.0.8",
|
||||||
@@ -7877,6 +7881,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -7889,6 +7894,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
},
|
},
|
||||||
@@ -7901,6 +7907,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
},
|
},
|
||||||
@@ -7913,6 +7920,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
@@ -7922,6 +7930,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
||||||
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.16.0",
|
"acorn": "^8.16.0",
|
||||||
"acorn-jsx": "^5.3.2",
|
"acorn-jsx": "^5.3.2",
|
||||||
@@ -7939,6 +7948,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
},
|
},
|
||||||
@@ -7964,6 +7974,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||||
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.1.0"
|
"estraverse": "^5.1.0"
|
||||||
},
|
},
|
||||||
@@ -7976,6 +7987,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.2.0"
|
"estraverse": "^5.2.0"
|
||||||
},
|
},
|
||||||
@@ -8076,7 +8088,8 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-fifo": {
|
"node_modules/fast-fifo": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
@@ -8104,13 +8117,15 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-levenshtein": {
|
"node_modules/fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-npm-meta": {
|
"node_modules/fast-npm-meta": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
@@ -8155,6 +8170,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flat-cache": "^4.0.0"
|
"flat-cache": "^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -8185,6 +8201,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"locate-path": "^6.0.0",
|
"locate-path": "^6.0.0",
|
||||||
"path-exists": "^4.0.0"
|
"path-exists": "^4.0.0"
|
||||||
@@ -8201,6 +8218,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||||
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flatted": "^3.2.9",
|
"flatted": "^3.2.9",
|
||||||
"keyv": "^4.5.4"
|
"keyv": "^4.5.4"
|
||||||
@@ -8213,7 +8231,8 @@
|
|||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.0.tgz",
|
||||||
"integrity": "sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==",
|
"integrity": "sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==",
|
||||||
"license": "ISC"
|
"license": "ISC",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
@@ -8746,6 +8765,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
@@ -9122,19 +9142,22 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/json-stable-stringify-without-jsonify": {
|
"node_modules/json-stable-stringify-without-jsonify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
@@ -9212,6 +9235,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
@@ -9472,6 +9496,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prelude-ls": "^1.2.1",
|
"prelude-ls": "^1.2.1",
|
||||||
"type-check": "~0.4.0"
|
"type-check": "~0.4.0"
|
||||||
@@ -9556,6 +9581,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p-locate": "^5.0.0"
|
"p-locate": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -9947,7 +9973,8 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
@@ -10183,7 +10210,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.3.1.tgz",
|
||||||
"integrity": "sha512-bl+0rFcT5Ax16aiWFBFPyWcsTob19NTZaDL5P6t0MQdK63AtgS6fN6fwvwdbXtnTk6/YdCzlmuLzXhSM22h0OA==",
|
"integrity": "sha512-bl+0rFcT5Ax16aiWFBFPyWcsTob19NTZaDL5P6t0MQdK63AtgS6fN6fwvwdbXtnTk6/YdCzlmuLzXhSM22h0OA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dxup/nuxt": "^0.3.2",
|
"@dxup/nuxt": "^0.3.2",
|
||||||
"@nuxt/cli": "^3.33.0",
|
"@nuxt/cli": "^3.33.0",
|
||||||
@@ -10454,6 +10480,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-is": "^0.1.3",
|
"deep-is": "^0.1.3",
|
||||||
"fast-levenshtein": "^2.0.6",
|
"fast-levenshtein": "^2.0.6",
|
||||||
@@ -10505,7 +10532,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.112.0.tgz",
|
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.112.0.tgz",
|
||||||
"integrity": "sha512-7rQ3QdJwobMQLMZwQaPuPYMEF2fDRZwf51lZ//V+bA37nejjKW5ifMHbbCwvA889Y4RLhT+/wLJpPRhAoBaZYw==",
|
"integrity": "sha512-7rQ3QdJwobMQLMZwQaPuPYMEF2fDRZwf51lZ//V+bA37nejjKW5ifMHbbCwvA889Y4RLhT+/wLJpPRhAoBaZYw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "^0.112.0"
|
"@oxc-project/types": "^0.112.0"
|
||||||
},
|
},
|
||||||
@@ -10589,6 +10615,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yocto-queue": "^0.1.0"
|
"yocto-queue": "^0.1.0"
|
||||||
},
|
},
|
||||||
@@ -10604,6 +10631,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p-limit": "^3.0.2"
|
"p-limit": "^3.0.2"
|
||||||
},
|
},
|
||||||
@@ -10646,6 +10674,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -10749,7 +10778,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-api": "^7.7.7"
|
"@vue/devtools-api": "^7.7.7"
|
||||||
},
|
},
|
||||||
@@ -10866,7 +10894,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -11410,7 +11437,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"util-deprecate": "^1.0.2"
|
"util-deprecate": "^1.0.2"
|
||||||
@@ -11461,6 +11487,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
@@ -11497,6 +11524,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -11858,7 +11886,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -12641,7 +12668,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -12982,6 +13008,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prelude-ls": "^1.2.1"
|
"prelude-ls": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -13049,7 +13076,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -13485,6 +13511,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -13509,7 +13536,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -13871,7 +13897,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
|
||||||
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
|
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.29",
|
"@vue/compiler-dom": "3.5.29",
|
||||||
"@vue/compiler-sfc": "3.5.29",
|
"@vue/compiler-sfc": "3.5.29",
|
||||||
@@ -13936,7 +13961,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
|
||||||
"integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
|
"integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "11.3.0",
|
"@intlify/core-base": "11.3.0",
|
||||||
"@intlify/devtools-types": "11.3.0",
|
"@intlify/devtools-types": "11.3.0",
|
||||||
@@ -13958,7 +13982,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-api": "^6.6.4"
|
"@vue/devtools-api": "^6.6.4"
|
||||||
},
|
},
|
||||||
@@ -14011,6 +14034,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -14179,6 +14203,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@malio/layer-ui": "^1.1.0",
|
"@malio/layer-ui": "^1.2.0",
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@nuxtjs/i18n": "^10.2.3",
|
"@nuxtjs/i18n": "^10.2.3",
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -30,13 +30,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<MalioButton
|
||||||
|
label="Se connecter"
|
||||||
|
button-class="w-full"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
/>
|
||||||
Se connecter
|
|
||||||
</button>
|
|
||||||
<p class="font-bold">v{{ version }}</p>
|
<p class="font-bold">v{{ version }}</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -324,15 +324,16 @@ onMounted(async () => {
|
|||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('myTasks.title') }}</h1>
|
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('myTasks.title') }}</h1>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<MalioButton
|
||||||
class="flex items-center gap-1.5 rounded-lg bg-primary-500 px-3 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-secondary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-3"
|
||||||
@click="openTaskCreate"
|
@click="openTaskCreate"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:plus" size="18" />
|
|
||||||
{{ $t('myTasks.createTask') }}
|
{{ $t('myTasks.createTask') }}
|
||||||
</button>
|
</MalioButton>
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-center rounded-md border p-1.5 transition-colors"
|
class="flex h-[40px] w-[40px] items-center justify-center rounded-md border transition-colors"
|
||||||
:class="viewMode === 'list'
|
:class="viewMode === 'list'
|
||||||
? 'border-primary-500 bg-primary-500 text-white'
|
? 'border-primary-500 bg-primary-500 text-white'
|
||||||
: 'border-primary-500 text-primary-500 hover:bg-primary-50'"
|
: 'border-primary-500 text-primary-500 hover:bg-primary-50'"
|
||||||
|
|||||||
@@ -104,21 +104,19 @@
|
|||||||
:placeholder="$t('clientTicket.rejectComment')"
|
:placeholder="$t('clientTicket.rejectComment')"
|
||||||
/>
|
/>
|
||||||
<div class="mt-4 flex justify-end gap-3">
|
<div class="mt-4 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
variant="tertiary"
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="cancelReject"
|
@click="cancelReject"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
variant="danger"
|
||||||
<button
|
:label="$t('clientTicket.status.rejected')"
|
||||||
type="button"
|
button-class="w-auto px-4"
|
||||||
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-50"
|
|
||||||
:disabled="!rejectComment.trim()"
|
:disabled="!rejectComment.trim()"
|
||||||
@click="confirmReject"
|
@click="confirmReject"
|
||||||
>
|
/>
|
||||||
{{ $t('clientTicket.status.rejected') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,13 +74,12 @@
|
|||||||
>
|
>
|
||||||
{{ $t('common.cancel') }}
|
{{ $t('common.cancel') }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
:label="$t('portal.submitTicket')"
|
||||||
class="rounded-lg bg-primary-500 px-6 py-2 text-sm font-semibold text-white transition-colors hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-auto px-6"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
@click="handleSubmit"
|
||||||
{{ $t('portal.submitTicket') }}
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,15 +26,14 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button
|
<MalioButton
|
||||||
v-if="auth.user?.avatarUrl"
|
v-if="auth.user?.avatarUrl"
|
||||||
type="button"
|
variant="danger"
|
||||||
class="rounded-lg border border-red-300 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50"
|
button-class="w-auto px-4"
|
||||||
:disabled="removing"
|
:disabled="removing"
|
||||||
|
:label="$t('profile.removeAvatar')"
|
||||||
@click="onRemove"
|
@click="onRemove"
|
||||||
>
|
/>
|
||||||
{{ $t('profile.removeAvatar') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -60,20 +60,20 @@
|
|||||||
<p class="mt-0.5 text-xs text-neutral-400">{{ formatDate(ticket.createdAt) }}</p>
|
<p class="mt-0.5 text-xs text-neutral-400">{{ formatDate(ticket.createdAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="rounded p-1.5 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-primary-500"
|
icon="mdi:swap-horizontal"
|
||||||
:title="$t('clientTicket.changeStatus')"
|
:aria-label="$t('clientTicket.changeStatus')"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="18"
|
||||||
@click.stop="openStatusChange(ticket)"
|
@click.stop="openStatusChange(ticket)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:swap-horizontal" size="18" />
|
<MalioButtonIcon
|
||||||
</button>
|
icon="mdi:delete-outline"
|
||||||
<button
|
aria-label="Supprimer"
|
||||||
class="rounded p-1.5 text-neutral-400 transition-colors hover:bg-red-50 hover:text-red-500"
|
variant="ghost"
|
||||||
title="Supprimer"
|
icon-size="18"
|
||||||
@click.stop="onDelete(ticket)"
|
@click.stop="onDelete(ticket)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:delete-outline" size="18" />
|
|
||||||
</button>
|
|
||||||
<Icon
|
<Icon
|
||||||
:name="expandedId === ticket.id ? 'mdi:chevron-up' : 'mdi:chevron-down'"
|
:name="expandedId === ticket.id ? 'mdi:chevron-up' : 'mdi:chevron-down'"
|
||||||
size="20"
|
size="20"
|
||||||
@@ -143,19 +143,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
class="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 transition-colors hover:bg-neutral-50"
|
variant="tertiary"
|
||||||
|
:label="$t('common.cancel')"
|
||||||
|
button-class="w-auto px-4"
|
||||||
@click="statusModalOpen = false"
|
@click="statusModalOpen = false"
|
||||||
>
|
/>
|
||||||
{{ $t('common.cancel') }}
|
<MalioButton
|
||||||
</button>
|
label="Confirmer"
|
||||||
<button
|
button-class="w-auto px-6"
|
||||||
class="rounded-lg bg-primary-500 px-6 py-2 text-sm font-semibold text-white transition-colors hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="isUpdatingStatus"
|
:disabled="isUpdatingStatus"
|
||||||
@click="confirmStatusChange"
|
@click="confirmStatusChange"
|
||||||
>
|
/>
|
||||||
Confirmer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,15 +4,17 @@
|
|||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ project?.name ?? '' }}</h1>
|
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ project?.name ?? '' }}</h1>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<MalioButton
|
||||||
class="shrink-0 rounded-md bg-primary-500 px-3 py-2 text-xs font-semibold text-white hover:bg-secondary-500 sm:px-4 sm:text-sm"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-3 shrink-0"
|
||||||
@click="openTaskCreate"
|
@click="openTaskCreate"
|
||||||
>
|
>
|
||||||
<span class="hidden sm:inline">+ Ajouter un ticket</span>
|
<span class="hidden sm:inline">Ajouter un ticket</span>
|
||||||
<span class="sm:hidden">+ Ticket</span>
|
<span class="sm:hidden">Ticket</span>
|
||||||
</button>
|
</MalioButton>
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-center rounded-md border p-1.5 transition-colors"
|
class="flex h-[40px] w-[40px] items-center justify-center rounded-md border transition-colors"
|
||||||
:class="viewMode === 'list'
|
:class="viewMode === 'list'
|
||||||
? 'border-primary-500 bg-primary-500 text-white'
|
? 'border-primary-500 bg-primary-500 text-white'
|
||||||
: 'border-primary-500 text-primary-500 hover:bg-primary-50'"
|
: 'border-primary-500 text-primary-500 hover:bg-primary-50'"
|
||||||
@@ -21,13 +23,12 @@
|
|||||||
>
|
>
|
||||||
<Icon name="mdi:format-list-bulleted" size="20" />
|
<Icon name="mdi:format-list-bulleted" size="20" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="flex shrink-0 items-center rounded-md bg-neutral-200 px-3 py-2 text-neutral-600 hover:bg-neutral-300 sm:px-4"
|
icon="heroicons:cog-6-tooth"
|
||||||
title="Paramètres du projet"
|
aria-label="Paramètres du projet"
|
||||||
|
variant="ghost"
|
||||||
@click="projectDrawerOpen = true"
|
@click="projectDrawerOpen = true"
|
||||||
>
|
/>
|
||||||
<Icon name="heroicons:cog-6-tooth" class="size-4 sm:size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,24 @@
|
|||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('projects.title') }}</h1>
|
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('projects.title') }}</h1>
|
||||||
<div class="flex items-center gap-2 sm:gap-3">
|
<div class="flex items-center gap-2 sm:gap-3">
|
||||||
<button
|
<MalioButton
|
||||||
class="flex items-center gap-1.5 rounded-md px-2 py-2 text-sm font-medium transition sm:px-3"
|
variant="tertiary"
|
||||||
:class="showArchived
|
:icon-name="showArchived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-outline'"
|
||||||
? 'bg-amber-100 text-amber-700 hover:bg-amber-200'
|
icon-position="left"
|
||||||
: 'text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700'"
|
button-class="w-auto px-3"
|
||||||
@click="toggleArchived"
|
@click="toggleArchived"
|
||||||
>
|
>
|
||||||
<Icon :name="showArchived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-outline'" size="18" />
|
|
||||||
<span class="hidden sm:inline">{{ showArchived ? $t('projects.hideArchived') : $t('projects.showArchived') }}</span>
|
<span class="hidden sm:inline">{{ showArchived ? $t('projects.hideArchived') : $t('projects.showArchived') }}</span>
|
||||||
</button>
|
</MalioButton>
|
||||||
<button
|
<MalioButton
|
||||||
class="shrink-0 rounded-md bg-primary-500 px-3 py-2 text-xs font-semibold text-white hover:bg-secondary-500 sm:px-4 sm:text-sm"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-auto px-3 shrink-0"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
>
|
||||||
<span class="hidden sm:inline">+ {{ $t('projects.addProject') }}</span>
|
<span class="hidden sm:inline">{{ $t('projects.addProject') }}</span>
|
||||||
<span class="sm:hidden">+ {{ $t('projects.addProjectShort') }}</span>
|
<span class="sm:hidden">{{ $t('projects.addProjectShort') }}</span>
|
||||||
</button>
|
</MalioButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,12 +45,13 @@
|
|||||||
{{ $t('common.archived') }}
|
{{ $t('common.archived') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<MalioButtonIcon
|
||||||
class="p-1 text-neutral-400 hover:text-primary-500"
|
icon="mdi:pencil-outline"
|
||||||
|
aria-label="Modifier le projet"
|
||||||
|
variant="ghost"
|
||||||
|
icon-size="16"
|
||||||
@click.stop="openEdit(project)"
|
@click.stop="openEdit(project)"
|
||||||
>
|
/>
|
||||||
<Icon name="mdi:pencil-outline" size="16" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-sm text-neutral-600 line-clamp-4">
|
<p class="mt-2 text-sm text-neutral-600 line-clamp-4">
|
||||||
{{ project.description ?? '' }}
|
{{ project.description ?? '' }}
|
||||||
|
|||||||
@@ -3,27 +3,35 @@
|
|||||||
<div ref="pageHeaderEl" class="sticky top-8 z-20 flex-shrink-0 bg-white pb-4 sm:top-12">
|
<div ref="pageHeaderEl" class="sticky top-8 z-20 flex-shrink-0 bg-white pb-4 sm:top-12">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">Suivi des temps</h1>
|
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">Suivi des temps</h1>
|
||||||
<button
|
<MalioButton
|
||||||
class="shrink-0 rounded-md bg-primary-500 px-3 py-2 text-xs font-semibold text-white hover:bg-primary-600 transition sm:px-4 sm:text-sm"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="shrink-0"
|
||||||
@click="openCreateDrawer()"
|
@click="openCreateDrawer()"
|
||||||
>
|
>
|
||||||
<span class="hidden sm:inline">+ Ajouter une Activité</span>
|
<span class="hidden sm:inline">Ajouter une Activité</span>
|
||||||
<span class="sm:hidden">+ Activité</span>
|
<span class="sm:hidden">Activité</span>
|
||||||
</button>
|
</MalioButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative z-30 mt-4 flex flex-wrap items-center gap-3 sm:gap-4">
|
<div class="relative z-30 mt-4 flex flex-wrap items-center gap-3 sm:gap-4">
|
||||||
<div class="flex shrink-0 items-center gap-1 h-8">
|
<div class="flex shrink-0 items-center gap-1 h-8">
|
||||||
<button class="flex h-8 w-8 items-center justify-center rounded-full text-neutral-400 hover:text-neutral-700 transition" @click="navigatePrev">
|
<MalioButtonIcon
|
||||||
<Icon name="mdi:chevron-left" size="20" />
|
icon="mdi:chevron-left"
|
||||||
</button>
|
aria-label="Précédent"
|
||||||
|
variant="ghost"
|
||||||
|
@click="navigatePrev"
|
||||||
|
/>
|
||||||
<DateFilter v-model="selectedDateFilter" :picker-mode="viewMode === 'day' ? 'day' : 'week'" />
|
<DateFilter v-model="selectedDateFilter" :picker-mode="viewMode === 'day' ? 'day' : 'week'" />
|
||||||
<h2 class="shrink-0 whitespace-nowrap text-lg font-bold leading-8 text-orange-500">
|
<h2 class="shrink-0 whitespace-nowrap text-lg font-bold leading-8 text-orange-500">
|
||||||
{{ currentMonthLabel }}
|
{{ currentMonthLabel }}
|
||||||
</h2>
|
</h2>
|
||||||
<button class="flex h-8 w-8 items-center justify-center rounded-full text-neutral-400 hover:text-neutral-700 transition" @click="navigateNext">
|
<MalioButtonIcon
|
||||||
<Icon name="mdi:chevron-right" size="20" />
|
icon="mdi:chevron-right"
|
||||||
</button>
|
aria-label="Suivant"
|
||||||
|
variant="ghost"
|
||||||
|
@click="navigateNext"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center rounded-full bg-neutral-100 p-1">
|
<div class="flex items-center rounded-full bg-neutral-100 p-1">
|
||||||
@@ -76,13 +84,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<MalioButton
|
||||||
class="flex shrink-0 items-center gap-2 rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 transition"
|
:label="$t('timeEntries.export')"
|
||||||
@click="exportTimeEntries"
|
variant="secondary"
|
||||||
>
|
icon-name="mdi:download"
|
||||||
<Icon name="mdi:download" size="18" />
|
icon-position="left"
|
||||||
{{ $t('timeEntries.export') }}
|
button-class="w-auto px-4"
|
||||||
</button>
|
@click="exportDrawerOpen = true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -128,6 +137,15 @@
|
|||||||
@paste="onPaste"
|
@paste="onPaste"
|
||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TimeTrackingExportDrawer
|
||||||
|
v-model="exportDrawerOpen"
|
||||||
|
:users="users"
|
||||||
|
:projects="projects"
|
||||||
|
:tags="tags"
|
||||||
|
:clients="clients"
|
||||||
|
@export="onExport"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -136,6 +154,7 @@ import type { TimeEntry } from '~/services/dto/time-entry'
|
|||||||
import type { UserData } from '~/services/dto/user-data'
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
import type { Project } from '~/services/dto/project'
|
import type { Project } from '~/services/dto/project'
|
||||||
import type { TaskTag } from '~/services/dto/task-tag'
|
import type { TaskTag } from '~/services/dto/task-tag'
|
||||||
|
import type { Client } from '~/services/dto/client'
|
||||||
import { useTimeEntryService } from '~/services/time-entries'
|
import { useTimeEntryService } from '~/services/time-entries'
|
||||||
import type { HydraCollection } from '~/utils/api'
|
import type { HydraCollection } from '~/utils/api'
|
||||||
import { extractHydraMembers } from '~/utils/api'
|
import { extractHydraMembers } from '~/utils/api'
|
||||||
@@ -156,6 +175,8 @@ const entries = ref<TimeEntry[]>([])
|
|||||||
const users = ref<UserData[]>([])
|
const users = ref<UserData[]>([])
|
||||||
const projects = ref<Project[]>([])
|
const projects = ref<Project[]>([])
|
||||||
const tags = ref<TaskTag[]>([])
|
const tags = ref<TaskTag[]>([])
|
||||||
|
const clients = ref<Client[]>([])
|
||||||
|
const exportDrawerOpen = ref(false)
|
||||||
|
|
||||||
const drawerOpen = ref(false)
|
const drawerOpen = ref(false)
|
||||||
const editingEntry = ref<TimeEntry | null>(null)
|
const editingEntry = ref<TimeEntry | null>(null)
|
||||||
@@ -305,38 +326,35 @@ async function onDelete(entry: TimeEntry) {
|
|||||||
await loadEntries()
|
await loadEntries()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExportDateRange(): { after: string, before: string } {
|
async function onExport(params: {
|
||||||
if (Array.isArray(selectedDateFilter.value) && selectedDateFilter.value.length === 2) {
|
after: string
|
||||||
return {
|
before: string
|
||||||
after: selectedDateFilter.value[0].toISOString().slice(0, 10),
|
users?: number[]
|
||||||
before: selectedDateFilter.value[1].toISOString().slice(0, 10),
|
projects?: number[]
|
||||||
}
|
client?: number
|
||||||
|
tags?: number[]
|
||||||
|
}) {
|
||||||
|
const toast = useToast()
|
||||||
|
const { t } = useNuxtApp().$i18n as { t: (key: string) => string }
|
||||||
|
|
||||||
|
toast.info({ message: t('timeEntries.exportLoading') })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await timeEntryService.downloadExport(params)
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(result.blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = result.filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
toast.success({ message: t('timeEntries.exportSuccess') })
|
||||||
|
} catch {
|
||||||
|
toast.error({ message: t('timeEntries.exportError') })
|
||||||
}
|
}
|
||||||
const end = new Date(startDate.value)
|
|
||||||
end.setDate(end.getDate() + (viewMode.value === 'day' ? 1 : 7))
|
|
||||||
return {
|
|
||||||
after: startDate.value.toISOString().slice(0, 10),
|
|
||||||
before: end.toISOString().slice(0, 10),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportTimeEntries() {
|
|
||||||
const { after, before } = getExportDateRange()
|
|
||||||
|
|
||||||
const url = timeEntryService.getExportUrl({
|
|
||||||
after,
|
|
||||||
before,
|
|
||||||
user: selectedUserId.value ?? undefined,
|
|
||||||
project: selectedProjectId.value ?? undefined,
|
|
||||||
tags: selectedTagId.value ? [selectedTagId.value] : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = ''
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
document.body.removeChild(a)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadEntries() {
|
async function loadEntries() {
|
||||||
@@ -353,15 +371,17 @@ async function loadEntries() {
|
|||||||
async function loadReferenceData() {
|
async function loadReferenceData() {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const [usersData, projectsData, typesData] = await Promise.all([
|
const [usersData, projectsData, typesData, clientsData] = await Promise.all([
|
||||||
api.get<HydraCollection<UserData>>('/users'),
|
api.get<HydraCollection<UserData>>('/users'),
|
||||||
api.get<HydraCollection<Project>>('/projects'),
|
api.get<HydraCollection<Project>>('/projects'),
|
||||||
api.get<HydraCollection<TaskTag>>('/task_tags'),
|
api.get<HydraCollection<TaskTag>>('/task_tags'),
|
||||||
|
api.get<HydraCollection<Client>>('/clients'),
|
||||||
])
|
])
|
||||||
|
|
||||||
users.value = extractHydraMembers(usersData)
|
users.value = extractHydraMembers(usersData)
|
||||||
projects.value = extractHydraMembers(projectsData)
|
projects.value = extractHydraMembers(projectsData)
|
||||||
tags.value = extractHydraMembers(typesData)
|
tags.value = extractHydraMembers(typesData)
|
||||||
|
clients.value = extractHydraMembers(clientsData)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -53,20 +53,42 @@ export function useTimeEntryService() {
|
|||||||
function getExportUrl(params: {
|
function getExportUrl(params: {
|
||||||
after: string
|
after: string
|
||||||
before: string
|
before: string
|
||||||
user?: number
|
users?: number[]
|
||||||
project?: number
|
projects?: number[]
|
||||||
|
client?: number
|
||||||
tags?: number[]
|
tags?: number[]
|
||||||
}): string {
|
}): string {
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams()
|
||||||
query.set('after', params.after)
|
query.set('after', params.after)
|
||||||
query.set('before', params.before)
|
query.set('before', params.before)
|
||||||
if (params.user) query.set('user', String(params.user))
|
if (params.users?.length) {
|
||||||
if (params.project) query.set('project', String(params.project))
|
params.users.forEach(id => query.append('users[]', String(id)))
|
||||||
|
}
|
||||||
|
if (params.client) query.set('client', String(params.client))
|
||||||
|
if (params.projects?.length) {
|
||||||
|
params.projects.forEach(id => query.append('projects[]', String(id)))
|
||||||
|
}
|
||||||
if (params.tags?.length) {
|
if (params.tags?.length) {
|
||||||
params.tags.forEach(id => query.append('tags[]', String(id)))
|
params.tags.forEach(id => query.append('tags[]', String(id)))
|
||||||
}
|
}
|
||||||
return `/api/time_entries/export?${query.toString()}`
|
return `/time_entries/export?${query.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return { getByDateRange, getActive, create, update, remove, getExportUrl }
|
async function downloadExport(params: {
|
||||||
|
after: string
|
||||||
|
before: string
|
||||||
|
users?: number[]
|
||||||
|
projects?: number[]
|
||||||
|
client?: number
|
||||||
|
tags?: number[]
|
||||||
|
}): Promise<{ blob: Blob; filename: string }> {
|
||||||
|
const url = getExportUrl(params)
|
||||||
|
const response = await api.getBlob(url)
|
||||||
|
const disposition = response.headers.get('content-disposition') ?? ''
|
||||||
|
const filenameMatch = disposition.match(/filename="?([^";\n]+)"?/)
|
||||||
|
const filename = filenameMatch?.[1] ?? `export-temps-${params.after}_${params.before}.xlsx`
|
||||||
|
return { blob: response.data, filename }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getByDateRange, getActive, create, update, remove, getExportUrl, downloadExport }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,28 @@ export default <Partial<Config>>{
|
|||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
500: '#056CF2'
|
500: '#056CF2'
|
||||||
|
},
|
||||||
|
m: {
|
||||||
|
primary: 'rgb(var(--m-primary) / <alpha-value>)',
|
||||||
|
secondary: 'rgb(var(--m-secondary, 75 77 237) / <alpha-value>)',
|
||||||
|
tertiary: 'rgb(var(--m-tertiary, 243 244 248) / <alpha-value>)',
|
||||||
|
border: 'rgb(var(--m-border) / <alpha-value>)',
|
||||||
|
text: 'rgb(var(--m-text) / <alpha-value>)',
|
||||||
|
muted: 'rgb(var(--m-muted) / <alpha-value>)',
|
||||||
|
bg: 'rgb(var(--m-bg) / <alpha-value>)',
|
||||||
|
surface: 'rgb(var(--m-surface) / <alpha-value>)',
|
||||||
|
disabled: 'rgb(var(--m-disabled) / <alpha-value>)',
|
||||||
|
danger: 'rgb(var(--m-danger) / <alpha-value>)',
|
||||||
|
success: 'rgb(var(--m-success) / <alpha-value>)',
|
||||||
|
'btn-primary': 'rgb(var(--m-btn-primary) / <alpha-value>)',
|
||||||
|
'btn-primary-hover': 'rgb(var(--m-btn-primary-hover) / <alpha-value>)',
|
||||||
|
'btn-primary-active': 'rgb(var(--m-btn-primary-active) / <alpha-value>)',
|
||||||
|
'btn-secondary': 'rgb(var(--m-btn-secondary) / <alpha-value>)',
|
||||||
|
'btn-secondary-hover': 'rgb(var(--m-btn-secondary-hover) / <alpha-value>)',
|
||||||
|
'btn-secondary-active': 'rgb(var(--m-btn-secondary-active) / <alpha-value>)',
|
||||||
|
'btn-danger': 'rgb(var(--m-btn-danger) / <alpha-value>)',
|
||||||
|
'btn-danger-hover': 'rgb(var(--m-btn-danger-hover) / <alpha-value>)',
|
||||||
|
'btn-danger-active': 'rgb(var(--m-btn-danger-active) / <alpha-value>)',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
infra/prod/.env.example
Normal file
22
infra/prod/.env.example
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Symfony
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_DEBUG=0
|
||||||
|
APP_SECRET=change-me
|
||||||
|
|
||||||
|
# Database (use host.docker.internal to reach bare-metal PostgreSQL)
|
||||||
|
DATABASE_URL="postgresql://lesstime_user:password@host.docker.internal:5432/lesstime_prod?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=change-me
|
||||||
|
JWT_COOKIE_SECURE=1
|
||||||
|
JWT_COOKIE_SAMESITE=lax
|
||||||
|
JWT_TOKEN_TTL=86400
|
||||||
|
JWT_COOKIE_TTL=86400
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ALLOW_ORIGIN='^https?://project\.malio-dev\.fr$'
|
||||||
|
|
||||||
|
# App
|
||||||
|
DEFAULT_URI=https://project.malio-dev.fr
|
||||||
81
infra/prod/Dockerfile
Normal file
81
infra/prod/Dockerfile
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# --- Stage 1: Build backend ---
|
||||||
|
FROM php:8.4-cli AS backend-build
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \
|
||||||
|
unzip curl git \
|
||||||
|
&& docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY composer.json composer.lock symfony.lock ./
|
||||||
|
RUN APP_ENV=prod APP_DEBUG=0 composer install --no-dev --no-scripts --no-interaction
|
||||||
|
|
||||||
|
COPY bin bin/
|
||||||
|
COPY config config/
|
||||||
|
COPY migrations migrations/
|
||||||
|
COPY public public/
|
||||||
|
COPY src src/
|
||||||
|
|
||||||
|
RUN composer dump-autoload --optimize --no-dev
|
||||||
|
|
||||||
|
# --- Stage 2: Build frontend ---
|
||||||
|
FROM node:lts-alpine AS frontend-build
|
||||||
|
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY frontend/ ./
|
||||||
|
ENV CI=1 \
|
||||||
|
NUXT_TELEMETRY_DISABLED=1 \
|
||||||
|
NUXT_PUBLIC_API_BASE=/api \
|
||||||
|
NUXT_PUBLIC_APP_BASE=/
|
||||||
|
RUN npm run generate
|
||||||
|
|
||||||
|
# --- Stage 3: Production image ---
|
||||||
|
FROM php:8.4-fpm AS production
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \
|
||||||
|
nginx supervisor \
|
||||||
|
&& docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# PHP production config
|
||||||
|
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||||
|
|
||||||
|
# PHP-FPM: forward worker output to stderr for docker logs
|
||||||
|
RUN echo "catch_workers_output = yes" >> /usr/local/etc/php-fpm.d/www.conf \
|
||||||
|
&& echo "decorate_workers_output = no" >> /usr/local/etc/php-fpm.d/www.conf
|
||||||
|
|
||||||
|
# Nginx: log to stdout/stderr
|
||||||
|
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||||
|
&& ln -sf /dev/stderr /var/log/nginx/error.log
|
||||||
|
|
||||||
|
# Remove default nginx site
|
||||||
|
RUN rm -f /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
|
# Configs
|
||||||
|
COPY infra/prod/supervisord.conf /etc/supervisor/conf.d/app.conf
|
||||||
|
COPY infra/prod/nginx.conf /etc/nginx/sites-enabled/lesstime.conf
|
||||||
|
|
||||||
|
# Backend from stage 1
|
||||||
|
COPY --from=backend-build /app /var/www/html
|
||||||
|
|
||||||
|
# Frontend from stage 2
|
||||||
|
COPY --from=frontend-build /app/frontend/.output/public /var/www/html/frontend/.output/public
|
||||||
|
|
||||||
|
# Symfony needs a .env file to boot (variables are overridden by env_file in docker-compose)
|
||||||
|
RUN echo "APP_ENV=prod" > /var/www/html/.env
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
RUN mkdir -p /var/www/html/var /var/www/html/var/uploads /var/www/html/var/mcp-sessions \
|
||||||
|
&& chown -R www-data:www-data /var/www/html/var
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["supervisord", "-n", "-c", "/etc/supervisor/conf.d/app.conf"]
|
||||||
28
infra/prod/deploy.sh
Executable file
28
infra/prod/deploy.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
TAG="${1:-latest}"
|
||||||
|
export LESSTIME_IMAGE_TAG="$TAG"
|
||||||
|
|
||||||
|
echo "==> Deploying lesstime:${TAG}..."
|
||||||
|
|
||||||
|
echo "==> Pulling image..."
|
||||||
|
sudo docker compose pull
|
||||||
|
|
||||||
|
echo "==> Starting container..."
|
||||||
|
sudo docker compose up -d
|
||||||
|
|
||||||
|
echo "==> Waiting for container to be ready..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "==> Running migrations..."
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
echo "==> Clearing cache..."
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console cache:clear --env=prod
|
||||||
|
sudo docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
|
||||||
|
|
||||||
|
VERSION=$(sudo docker compose exec -T app cat config/version.yaml | grep 'app.version' | awk -F"'" '{print $2}')
|
||||||
|
echo "==> Deployed v${VERSION}"
|
||||||
13
infra/prod/docker-compose.yml
Normal file
13
infra/prod/docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.malio.fr/malio-dev/lesstime:${LESSTIME_IMAGE_TAG:-latest}
|
||||||
|
container_name: lesstime-app
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
volumes:
|
||||||
|
- ./config/jwt:/var/www/html/config/jwt:ro
|
||||||
|
- ./uploads:/var/www/html/var/uploads
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
14
infra/prod/nginx-proxy.conf
Normal file
14
infra/prod/nginx-proxy.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name project.malio-dev.fr;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
client_max_body_size 55m;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
infra/prod/nginx.conf
Normal file
53
infra/prod/nginx.conf
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /var/www/html/frontend/.output/public;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
client_max_body_size 55m;
|
||||||
|
|
||||||
|
access_log /dev/stdout;
|
||||||
|
error_log /dev/stderr;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /bundles/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api/login_check {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_param SCRIPT_NAME /index.php;
|
||||||
|
fastcgi_param PATH_INFO /login_check;
|
||||||
|
fastcgi_param REQUEST_URI /login_check;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /_mcp {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
infra/prod/supervisord.conf
Normal file
28
infra/prod/supervisord.conf
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
|
||||||
|
[program:php-fpm]
|
||||||
|
command=php-fpm -F
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
stopasgroup=true
|
||||||
|
stopsignal=QUIT
|
||||||
|
|
||||||
|
[program:nginx]
|
||||||
|
command=nginx -g "daemon off;"
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
stopasgroup=true
|
||||||
|
stopsignal=QUIT
|
||||||
6
makefile
6
makefile
@@ -1,6 +1,6 @@
|
|||||||
# Permet d'utiliser un .env.docker.local pour override
|
# Permet d'utiliser un .env.docker.local pour override
|
||||||
ENV_DEFAULT = docker/.env.docker
|
ENV_DEFAULT = infra/dev/.env.docker
|
||||||
ENV_LOCAL = docker/.env.docker.local
|
ENV_LOCAL = infra/dev/.env.docker.local
|
||||||
ENV_FILE := $(if $(wildcard $(ENV_LOCAL)),$(ENV_LOCAL),$(ENV_DEFAULT))
|
ENV_FILE := $(if $(wildcard $(ENV_LOCAL)),$(ENV_LOCAL),$(ENV_DEFAULT))
|
||||||
|
|
||||||
# Permet d'avoir les variables du fichier .env.docker.local
|
# Permet d'avoir les variables du fichier .env.docker.local
|
||||||
@@ -23,13 +23,11 @@ FILES =
|
|||||||
#========================================================================================
|
#========================================================================================
|
||||||
|
|
||||||
env-init:
|
env-init:
|
||||||
@mkdir -p docker
|
|
||||||
@cp --update=none $(ENV_DEFAULT) $(ENV_LOCAL)
|
@cp --update=none $(ENV_DEFAULT) $(ENV_LOCAL)
|
||||||
|
|
||||||
# Lance le container
|
# Lance le container
|
||||||
start: env-init
|
start: env-init
|
||||||
@echo "**** START CONTAINERS ****"
|
@echo "**** START CONTAINERS ****"
|
||||||
@cp --update=none docker/.env.docker docker/.env.docker.local
|
|
||||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
# Éteint le container
|
# Éteint le container
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Usage: ./script/deploy-release.sh v0.1.0
|
|
||||||
# Requires: curl, tar, (optional) rsync
|
|
||||||
#
|
|
||||||
# Auth token: set RELEASE_TOKEN env var or create /etc/lesstime-release-token
|
|
||||||
umask 002
|
|
||||||
|
|
||||||
TAG="${1:-}"
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
echo "Usage: $0 v0.1.0" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
REPO_OWNER="MALIO-DEV"
|
|
||||||
REPO_NAME="Lesstime"
|
|
||||||
GITEA_API="https://gitea.malio.fr/api/v1"
|
|
||||||
DEPLOY_DIR="/var/www/lesstime"
|
|
||||||
|
|
||||||
if [ -f /etc/lesstime-release-token ] && [ -z "${RELEASE_TOKEN:-}" ]; then
|
|
||||||
RELEASE_TOKEN="$(cat /etc/lesstime-release-token)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmp_dir="$(mktemp -d)"
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$tmp_dir"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
release_json="$tmp_dir/release.json"
|
|
||||||
curl_opts=(-sS)
|
|
||||||
if [ -n "${RELEASE_TOKEN:-}" ]; then
|
|
||||||
curl_opts+=(-H "Authorization: token ${RELEASE_TOKEN}")
|
|
||||||
fi
|
|
||||||
curl "${curl_opts[@]}" \
|
|
||||||
"${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${TAG}" \
|
|
||||||
-o "$release_json"
|
|
||||||
|
|
||||||
asset_url="$(python3 - "$release_json" <<'PY'
|
|
||||||
import json, sys
|
|
||||||
data = json.load(open(sys.argv[1], 'r'))
|
|
||||||
assets = data.get("assets", [])
|
|
||||||
for a in assets:
|
|
||||||
name = a.get("name", "")
|
|
||||||
if name.startswith("lesstime-") and name.endswith(".tar.gz"):
|
|
||||||
print(a.get("browser_download_url", ""))
|
|
||||||
break
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [ -z "$asset_url" ]; then
|
|
||||||
echo "Release asset not found for tag ${TAG}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
archive="$tmp_dir/artefact.tar.gz"
|
|
||||||
curl "${curl_opts[@]}" -L "$asset_url" -o "$archive"
|
|
||||||
|
|
||||||
tar -xzf "$archive" -C "$tmp_dir"
|
|
||||||
|
|
||||||
if command -v rsync >/dev/null 2>&1; then
|
|
||||||
rsync -a --delete --no-perms --no-owner --no-group \
|
|
||||||
--exclude ".env" \
|
|
||||||
--exclude ".env.local" \
|
|
||||||
--exclude "config/jwt" \
|
|
||||||
--exclude "var" \
|
|
||||||
"$tmp_dir"/ "$DEPLOY_DIR"/
|
|
||||||
else
|
|
||||||
cp -a "$tmp_dir"/. "$DEPLOY_DIR"/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure Nginx can traverse the deploy path.
|
|
||||||
chmod o+rx "$(dirname "$DEPLOY_DIR")" "$DEPLOY_DIR" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Create frontend/dist symlink if needed (nginx serves from frontend/dist)
|
|
||||||
if [ -d "${DEPLOY_DIR}/frontend/.output/public" ] && [ ! -L "${DEPLOY_DIR}/frontend/dist" ]; then
|
|
||||||
ln -sfn "${DEPLOY_DIR}/frontend/.output/public" "${DEPLOY_DIR}/frontend/dist"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Release ${TAG} deployed to ${DEPLOY_DIR}"
|
|
||||||
|
|
||||||
# Ensure var/log exists and is writable by PHP (www-data)
|
|
||||||
mkdir -p "${DEPLOY_DIR}/var/log"
|
|
||||||
chown www-data:www-data "${DEPLOY_DIR}/var/log"
|
|
||||||
chmod 775 "${DEPLOY_DIR}/var/log"
|
|
||||||
|
|
||||||
if [ -f "${DEPLOY_DIR}/.env.local" ]; then
|
|
||||||
echo "Clearing cache..."
|
|
||||||
php "${DEPLOY_DIR}/bin/console" cache:clear --env=prod --no-debug
|
|
||||||
|
|
||||||
echo "Running migrations (if any)..."
|
|
||||||
php "${DEPLOY_DIR}/bin/console" doctrine:migrations:migrate --no-interaction --env=prod
|
|
||||||
else
|
|
||||||
echo "Skip post-deploy: ${DEPLOY_DIR}/.env.local not found" >&2
|
|
||||||
fi
|
|
||||||
@@ -47,27 +47,65 @@ class TimeEntryExportController extends AbstractController
|
|||||||
throw new BadRequestHttpException('Format de date invalide. Utilisez YYYY-MM-DD.');
|
throw new BadRequestHttpException('Format de date invalide. Utilisez YYYY-MM-DD.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max range: 12 months
|
|
||||||
if ($after->modify('+12 months') < $before) {
|
if ($after->modify('+12 months') < $before) {
|
||||||
throw new BadRequestHttpException('La plage de dates ne peut pas dépasser 12 mois.');
|
throw new BadRequestHttpException('La plage de dates ne peut pas dépasser 12 mois.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization: non-admin users can only export their own data
|
// --- Users ---
|
||||||
$user = null;
|
$users = null;
|
||||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||||
/** @var User $user */
|
/** @var User $currentUser */
|
||||||
$user = $this->security->getUser();
|
$currentUser = $this->security->getUser();
|
||||||
|
$users = [$currentUser];
|
||||||
} else {
|
} else {
|
||||||
$userId = $request->query->getInt('user');
|
/** @var int[] $userIds */
|
||||||
if ($userId > 0) {
|
$userIds = array_filter(
|
||||||
$user = $this->entityManager->getRepository(User::class)->find($userId);
|
array_map('intval', (array) $request->query->all('users')),
|
||||||
|
fn (int $id) => $id > 0,
|
||||||
|
);
|
||||||
|
if ([] !== $userIds) {
|
||||||
|
$users = $this->entityManager->getRepository(User::class)->findBy(['id' => $userIds]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$project = null;
|
// --- Client (filter projects by client) ---
|
||||||
$projectId = $request->query->getInt('project');
|
$clientId = $request->query->getInt('client');
|
||||||
if ($projectId > 0) {
|
$clientProjects = null;
|
||||||
$project = $this->entityManager->getRepository(Project::class)->find($projectId);
|
if ($clientId > 0) {
|
||||||
|
$clientProjects = $this->entityManager->getRepository(Project::class)->findBy(['client' => $clientId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Projects ---
|
||||||
|
$projects = null;
|
||||||
|
|
||||||
|
/** @var int[] $projectIds */
|
||||||
|
$projectIds = array_filter(
|
||||||
|
array_map('intval', (array) $request->query->all('projects')),
|
||||||
|
fn (int $id) => $id > 0,
|
||||||
|
);
|
||||||
|
if ([] !== $projectIds) {
|
||||||
|
$projects = $this->entityManager->getRepository(Project::class)->findBy(['id' => $projectIds]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge: if both client and projects are set, intersect; if only client, use client projects
|
||||||
|
if (null !== $clientProjects && null !== $projects) {
|
||||||
|
$clientProjectIds = array_map(fn (Project $p) => $p->getId(), $clientProjects);
|
||||||
|
$projects = array_values(array_filter($projects, fn (Project $p) => in_array($p->getId(), $clientProjectIds, true)));
|
||||||
|
if ([] === $projects) {
|
||||||
|
$projects = null;
|
||||||
|
// No matching projects — force empty result by using a dummy condition
|
||||||
|
$entries = [];
|
||||||
|
$tempFile = $this->exportService->generate($entries, $after, $before);
|
||||||
|
$filename = sprintf('export-temps-%s_%s.xlsx', $after->format('Y-m-d'), $before->format('Y-m-d'));
|
||||||
|
$response = new BinaryFileResponse($tempFile);
|
||||||
|
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||||
|
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
$response->deleteFileAfterSend(true);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
} elseif (null !== $clientProjects) {
|
||||||
|
$projects = $clientProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var int[] $tagIds */
|
/** @var int[] $tagIds */
|
||||||
@@ -79,8 +117,8 @@ class TimeEntryExportController extends AbstractController
|
|||||||
$entries = $this->timeEntryRepository->findForExport(
|
$entries = $this->timeEntryRepository->findForExport(
|
||||||
$after,
|
$after,
|
||||||
$before,
|
$before,
|
||||||
$user,
|
$users ?: null,
|
||||||
$project,
|
$projects ?: null,
|
||||||
$tagIds ?: null,
|
$tagIds ?: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -30,15 +30,17 @@ class TimeEntryRepository extends ServiceEntityRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null|int[] $tagIds
|
* @param null|User[] $users
|
||||||
|
* @param null|Project[] $projects
|
||||||
|
* @param null|int[] $tagIds
|
||||||
*
|
*
|
||||||
* @return TimeEntry[]
|
* @return TimeEntry[]
|
||||||
*/
|
*/
|
||||||
public function findForExport(
|
public function findForExport(
|
||||||
DateTimeImmutable $after,
|
DateTimeImmutable $after,
|
||||||
DateTimeImmutable $before,
|
DateTimeImmutable $before,
|
||||||
?User $user = null,
|
?array $users = null,
|
||||||
?Project $project = null,
|
?array $projects = null,
|
||||||
?array $tagIds = null,
|
?array $tagIds = null,
|
||||||
): array {
|
): array {
|
||||||
$qb = $this->createQueryBuilder('te')
|
$qb = $this->createQueryBuilder('te')
|
||||||
@@ -49,15 +51,15 @@ class TimeEntryRepository extends ServiceEntityRepository
|
|||||||
->orderBy('te.startedAt', 'ASC')
|
->orderBy('te.startedAt', 'ASC')
|
||||||
;
|
;
|
||||||
|
|
||||||
if (null !== $user) {
|
if (null !== $users && [] !== $users) {
|
||||||
$qb->andWhere('te.user = :user')
|
$qb->andWhere('te.user IN (:users)')
|
||||||
->setParameter('user', $user)
|
->setParameter('users', $users)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $project) {
|
if (null !== $projects && [] !== $projects) {
|
||||||
$qb->andWhere('te.project = :project')
|
$qb->andWhere('te.project IN (:projects)')
|
||||||
->setParameter('project', $project)
|
->setParameter('projects', $projects)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user