diff --git a/docs/superpowers/specs/2026-03-15-task-documents-design.md b/docs/superpowers/specs/2026-03-15-task-documents-design.md new file mode 100644 index 0000000..2515f15 --- /dev/null +++ b/docs/superpowers/specs/2026-03-15-task-documents-design.md @@ -0,0 +1,171 @@ +# Task Documents — Design Spec + +## Overview + +Ajout d'un système de documents attachés aux tickets (tasks). Les utilisateurs peuvent uploader des fichiers via drag & drop ou sélection, les visualiser (images, PDF) dans une modale plein écran, et les télécharger. + +## Contraintes + +- **Taille max par fichier** : 50 Mo +- **Types acceptés** : tous types de fichiers +- **Nombre par ticket** : illimité +- **Stockage** : filesystem local (`var/uploads/documents/`) +- **Permissions** : ROLE_ADMIN pour créer/supprimer, ROLE_USER pour lire + +## Backend + +### Entité `TaskDocument` + +| Champ | Type | Description | +|-------|------|-------------| +| `id` | int (auto) | Clé primaire | +| `task` | ManyToOne → Task | Ticket parent (CASCADE on delete) | +| `originalName` | string (255) | Nom original du fichier uploadé | +| `fileName` | string (255) | Nom unique sur disque (`{uuid}.{extension}`) | +| `mimeType` | string (100) | Type MIME (ex: `image/png`, `application/pdf`) | +| `size` | int | Taille en octets | +| `createdAt` | DateTimeImmutable | Date d'upload | +| `uploadedBy` | ManyToOne → User | Utilisateur ayant uploadé (SET NULL on delete) | + +### Relation inverse sur Task + +- `Task.documents` : OneToMany → TaskDocument +- Sérialisé dans le groupe `task:read` pour charger les documents avec le ticket + +### Stockage filesystem + +- Répertoire : `var/uploads/documents/` +- Nommage : `{uuid}.{extension}` — évite les collisions et les caractères spéciaux +- Volume Docker dédié pour persister les uploads +- Servir les fichiers via Nginx (`/uploads/{fileName}`) ou un endpoint Symfony dédié pour contrôle d'accès + +### API Endpoints + +| Méthode | Route | Description | Accès | +|---------|-------|-------------|-------| +| `POST` | `/api/task_documents` | Upload multipart/form-data | ROLE_ADMIN | +| `GET` | `/api/task_documents?task=/api/tasks/{id}` | Liste documents d'un ticket | ROLE_USER | +| `GET` | `/api/task_documents/{id}` | Métadonnées d'un document | ROLE_USER | +| `DELETE` | `/api/task_documents/{id}` | Supprime document + fichier | ROLE_ADMIN | + +### State Processor — POST (`TaskDocumentProcessor`) + +1. Reçoit le fichier via multipart/form-data + IRI de la task +2. Valide : fichier non vide, taille ≤ 50 Mo +3. Génère un UUID v4, extrait l'extension du nom original +4. Déplace le fichier uploadé dans `var/uploads/documents/{uuid}.{ext}` +5. Crée et persiste l'entité `TaskDocument` avec toutes les métadonnées +6. Set `uploadedBy` depuis le token JWT courant + +### State Processor — DELETE + +1. Récupère le `fileName` de l'entité +2. Supprime le fichier du filesystem (`var/uploads/documents/{fileName}`) +3. Supprime l'entité de la base de données + +### Validation + +- Contrainte sur `originalName` : NotBlank +- Contrainte sur `task` : NotNull +- Validation dans le Processor : taille fichier ≤ 50 Mo, fichier présent dans la requête +- PHP `upload_max_filesize` et `post_max_size` à configurer ≥ 50 Mo + +### Configuration PHP/Nginx + +- `php.ini` : `upload_max_filesize = 50M`, `post_max_size = 55M` +- Nginx : `client_max_body_size 55m;` + +## Frontend + +### Placement dans l'UI + +La zone de documents est placée **sous la description** dans le `TaskModal`, visible en mode édition. + +### Composants à créer + +Tous dans `frontend/components/task/` : + +#### `TaskDocumentUpload.vue` + +- Zone drag & drop avec bordure pointillée +- Texte : "Glisser des fichiers ici ou cliquer pour sélectionner" +- Input file caché (`multiple`, `accept="*"`) +- Événements : `dragover`, `dragleave`, `drop` pour le feedback visuel +- Barre de progression par fichier pendant l'upload +- Upload séquentiel ou parallèle (un POST multipart par fichier) +- Émet un événement quand l'upload est terminé pour rafraîchir la liste + +#### `TaskDocumentList.vue` + +- Grille de cartes compactes pour chaque document +- **Images** (`image/*`) : miniature 64x64 en `object-fit: cover`, chargée depuis l'URL du fichier +- **Autres fichiers** : icône selon le type MIME : + - PDF → icône PDF + - Word/Excel → icônes Office + - Archives → icône archive + - Défaut → icône fichier générique +- Informations affichées : nom original (tronqué si > ~30 chars), taille formatée (Ko/Mo) +- Clic sur un document → ouvre `TaskDocumentPreview` +- Bouton supprimer (visible uniquement pour ROLE_ADMIN, avec confirmation) + +#### `TaskDocumentPreview.vue` + +- Modale plein écran (overlay sombre semi-transparent) +- Contenu selon le type : + - **Images** (`image/*`) : `` centré, taille adaptative + - **PDF** (`application/pdf`) : `