diff --git a/CLAUDE.md b/CLAUDE.md index 91bbed8..753e081 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,28 +5,29 @@ Application de gestion de projet. Monorepo Symfony 8 (API Platform 4) + Nuxt 4. ## Stack - **Backend** : PHP 8.4, Symfony 8.0, API Platform 4, Doctrine ORM, PostgreSQL 16 -- **Frontend** : Nuxt 4 (SSR off / SPA), Vue 3, Pinia, Tailwind CSS, nuxt-toast, @nuxtjs/i18n, @nuxt/icon +- **Frontend** : Nuxt 4 (SSR off / SPA), Vue 3, Pinia, Tailwind CSS, @malio/layer-ui, nuxt-toast, @nuxtjs/i18n, @nuxt/icon - **Auth** : JWT HTTP-only cookie (lexik/jwt-authentication-bundle), login à `/login_check`, cookie `BEARER` - **Docker** : PHP-FPM + Node 24, Nginx (port 8082), PostgreSQL (port 5435) ## Structure ``` -src/Entity/ # Entités Doctrine +src/Entity/ # Entités Doctrine (User, Client, Project, Task, TaskStatus, TaskEffort, TaskPriority, TaskType, TaskGroup) src/ApiResource/ # Ressources API Platform (si découplées des entités) -src/State/ # Providers et Processors API Platform +src/State/ # Providers et Processors API Platform (MeProvider, AppVersionProvider, UserPasswordHasherProcessor) src/Repository/ # Repositories Doctrine src/DataFixtures/ # Fixtures config/ # Config Symfony (security, api_platform, lexik_jwt, nelmio_cors, doctrine) config/jwt/ # Clés JWT (private.pem, public.pem) migrations/ # Migrations Doctrine +docs/plans/ # Plans d'implémentation frontend/ # App Nuxt 4 -frontend/pages/ # Pages +frontend/pages/ # Pages (index, login, clients, projects, projects/[id], admin) frontend/layouts/ # Layouts (pas "layout") -frontend/components/ # Composants Vue +frontend/components/ # Composants Vue (AppDrawer, ColorPicker, *Drawer, TaskCard, Admin*Tab, UserDrawer) frontend/composables/# Composables (useApi, etc.) frontend/stores/ # Stores Pinia -frontend/services/ # Services API (auth, etc.) +frontend/services/ # Services API (auth, clients, projects, tasks, task-statuses, etc.) frontend/services/dto/ # Types TypeScript frontend/i18n/locales/ # Fichiers de traduction (langDir résolu depuis i18n/) ``` diff --git a/docs/plans/2026-03-09-clients-projects-crud.md b/docs/plans/2026-03-09-clients-projects-crud.md new file mode 100644 index 0000000..edb8897 --- /dev/null +++ b/docs/plans/2026-03-09-clients-projects-crud.md @@ -0,0 +1,1354 @@ +# CRUD Clients & Projets - Plan d'implémentation + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implémenter le CRUD complet Client et Projet (backend API Platform + frontend Nuxt) avec pages dédiées, drawers latéraux pour ajout/modification, et fixtures de données. + +**Architecture:** Deux entités Doctrine (Client, Project) avec relation ManyToOne optionnelle Project→Client. API Platform gère le CRUD via les attributs sur les entités (pas de controllers). Frontend avec pages dédiées : tableau pour les clients, cartes colorées pour les projets, drawers droits pour les formulaires. + +**Tech Stack:** PHP 8.4 / Symfony 8 / API Platform 4 / Doctrine ORM / PostgreSQL (backend) — Nuxt 4 / Vue 3 / Pinia / Tailwind CSS / Malio UI Layer (frontend) + +--- + +## Task 1 : Entité Client (Backend) + +**Files:** +- Create: `src/Entity/Client.php` +- Create: `src/Repository/ClientRepository.php` + +**Step 1: Créer le repository Client** + +```php + ['client:read']], + denormalizationContext: ['groups' => ['client:write']], + order: ['name' => 'ASC'], +)] +#[ORM\Entity(repositoryClass: ClientRepository::class)] +class Client +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['client:read', 'project:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['client:read', 'client:write', 'project:read'])] + private ?string $name = null; + + #[ORM\Column(length: 255, nullable: true)] + #[Groups(['client:read', 'client:write'])] + private ?string $email = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['client:read', 'client:write'])] + private ?string $phone = null; + + #[ORM\Column(length: 255, nullable: true)] + #[Groups(['client:read', 'client:write'])] + private ?string $street = null; + + #[ORM\Column(length: 255, nullable: true)] + #[Groups(['client:read', 'client:write'])] + private ?string $city = null; + + #[ORM\Column(length: 20, nullable: true)] + #[Groups(['client:read', 'client:write'])] + private ?string $postalCode = null; + + /** @var Collection */ + #[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'client')] + private Collection $projects; + + public function __construct() + { + $this->projects = new ArrayCollection(); + } + + // Getters et setters pour chaque propriété (getId, getName/setName, getEmail/setEmail, + // getPhone/setPhone, getStreet/setStreet, getCity/setCity, getPostalCode/setPostalCode, + // getProjects) + // Suivre le même pattern fluent (return $this) que User.php +} +``` + +**Step 3: Générer et exécuter la migration** + +Run (dans le container PHP) : +```bash +php bin/console doctrine:migrations:diff +php bin/console doctrine:migrations:migrate --no-interaction +``` +Expected: Migration créée et exécutée, table `client` créée en base. + +**Step 4: Vérifier l'API** + +Run: `curl -s http://localhost:8082/api/clients | head -20` (après login) +Expected: Réponse Hydra vide `{"hydra:member":[],"hydra:totalItems":0}` + +**Step 5: Commit** + +```bash +git add src/Entity/Client.php src/Repository/ClientRepository.php migrations/ +git commit -m "feat : add Client entity with CRUD API" +``` + +--- + +## Task 2 : Entité Project (Backend) + +**Files:** +- Create: `src/Entity/Project.php` +- Create: `src/Repository/ProjectRepository.php` + +**Step 1: Créer le repository Project** + +```php + ['project:read']], + denormalizationContext: ['groups' => ['project:write']], + order: ['name' => 'ASC'], +)] +#[ORM\Entity(repositoryClass: ProjectRepository::class)] +class Project +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['project:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['project:read', 'project:write'])] + private ?string $name = null; + + #[ORM\Column(type: 'text', nullable: true)] + #[Groups(['project:read', 'project:write'])] + private ?string $description = null; + + #[ORM\Column(length: 7)] + #[Groups(['project:read', 'project:write'])] + private ?string $color = '#222783'; + + #[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'projects')] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['project:read', 'project:write'])] + private ?Client $client = null; + + // Getters et setters pour chaque propriété (getId, getName/setName, getDescription/setDescription, + // getColor/setColor, getClient/setClient) + // Suivre le même pattern fluent (return $this) que User.php +} +``` + +**Note importante pour API Platform :** Le champ `client` en écriture accepte un IRI (ex: `"/api/clients/1"`) ou `null`. + +**Step 3: Générer et exécuter la migration** + +Run (dans le container PHP) : +```bash +php bin/console doctrine:migrations:diff +php bin/console doctrine:migrations:migrate --no-interaction +``` +Expected: Migration créée, table `project` avec foreign key vers `client`. + +**Step 4: Commit** + +```bash +git add src/Entity/Project.php src/Repository/ProjectRepository.php migrations/ +git commit -m "feat : add Project entity with CRUD API and Client relation" +``` + +--- + +## Task 3 : Fixtures Clients & Projets + +**Files:** +- Modify: `src/DataFixtures/AppFixtures.php` + +**Step 1: Ajouter les fixtures** + +Modifier `AppFixtures.php` pour ajouter des clients et projets après la création du user admin : + +```php +setUsername('admin'); + $admin->setRoles(['ROLE_ADMIN']); + $admin->setPassword($this->passwordHasher->hashPassword($admin, 'admin')); + $manager->persist($admin); + + // Clients + $clientLiot = new Client(); + $clientLiot->setName('LIOT'); + $clientLiot->setEmail('contact@liot.fr'); + $clientLiot->setPhone('05 50 50 50 50'); + $clientLiot->setStreet('14 allée d\'argenson'); + $clientLiot->setCity('Poitiers'); + $clientLiot->setPostalCode('86100'); + $manager->persist($clientLiot); + + $clientAcme = new Client(); + $clientAcme->setName('ACME Corp'); + $clientAcme->setEmail('contact@acme.com'); + $clientAcme->setPhone('01 23 45 67 89'); + $clientAcme->setStreet('10 rue de la Paix'); + $clientAcme->setCity('Paris'); + $clientAcme->setPostalCode('75002'); + $manager->persist($clientAcme); + + $clientNova = new Client(); + $clientNova->setName('Nova Tech'); + $clientNova->setEmail('info@novatech.io'); + $clientNova->setPhone('04 56 78 90 12'); + $clientNova->setStreet('5 avenue Jean Jaurès'); + $clientNova->setCity('Lyon'); + $clientNova->setPostalCode('69007'); + $manager->persist($clientNova); + + // Projets + $colors = ['#222783', '#E91E63', '#4A90D9', '#7E57C2', '#26A69A', '#FDD835', '#8BC34A', '#FF7043']; + + $projectSirh = new Project(); + $projectSirh->setName('SIRH'); + $projectSirh->setDescription('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer ac blandit turpis.'); + $projectSirh->setColor($colors[0]); + $projectSirh->setClient($clientLiot); + $manager->persist($projectSirh); + + $projectCrm = new Project(); + $projectCrm->setName('CRM'); + $projectCrm->setDescription('Gestion de la relation client et suivi commercial.'); + $projectCrm->setColor($colors[1]); + $projectCrm->setClient($clientAcme); + $manager->persist($projectCrm); + + $projectErp = new Project(); + $projectErp->setName('ERP'); + $projectErp->setDescription('Planification des ressources et gestion des stocks.'); + $projectErp->setColor($colors[2]); + $projectErp->setClient($clientNova); + $manager->persist($projectErp); + + $projectInterne = new Project(); + $projectInterne->setName('Site vitrine'); + $projectInterne->setDescription('Refonte du site web corporate.'); + $projectInterne->setColor($colors[4]); + $projectInterne->setClient(null); + $manager->persist($projectInterne); + + $manager->flush(); + } +} +``` + +**Step 2: Recharger les fixtures** + +Run: `make fixtures` ou dans le container : `php bin/console doctrine:fixtures:load --no-interaction` +Expected: 1 user, 3 clients, 4 projets créés. + +**Step 3: Commit** + +```bash +git add src/DataFixtures/AppFixtures.php +git commit -m "feat : add Client and Project fixtures" +``` + +--- + +## Task 4 : DTOs & Service Client (Frontend) + +**Files:** +- Create: `frontend/services/dto/client.ts` +- Create: `frontend/services/clients.ts` +- Modify: `frontend/utils/api.ts` (créer si inexistant — utilitaire Hydra) + +**Step 1: Créer l'utilitaire Hydra** + +Créer `frontend/utils/api.ts` : + +```typescript +export type HydraCollection = { + 'hydra:member': T[] + 'hydra:totalItems': number +} + +export function extractHydraMembers(response: HydraCollection): T[] { + return response['hydra:member'] ?? [] +} +``` + +**Step 2: Créer le DTO Client** + +Créer `frontend/services/dto/client.ts` : + +```typescript +export type Client = { + id: number + '@id'?: string + name: string + email: string | null + phone: string | null + street: string | null + city: string | null + postalCode: string | null +} + +export type ClientWrite = { + name: string + email: string | null + phone: string | null + street: string | null + city: string | null + postalCode: string | null +} +``` + +**Step 3: Créer le service Client** + +Créer `frontend/services/clients.ts` : + +```typescript +import type { Client, ClientWrite } from './dto/client' +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export function useClientService() { + const api = useApi() + + async function getAll(): Promise { + const data = await api.get>('/clients') + return extractHydraMembers(data) + } + + async function create(payload: ClientWrite): Promise { + return api.post('/clients', payload as Record, { + toastSuccessKey: 'clients.created', + }) + } + + async function update(id: number, payload: Partial): Promise { + return api.patch(`/clients/${id}`, payload as Record, { + toastSuccessKey: 'clients.updated', + }) + } + + async function remove(id: number): Promise { + await api.delete(`/clients/${id}`, {}, { + toastSuccessKey: 'clients.deleted', + }) + } + + return { getAll, create, update, remove } +} +``` + +**Step 4: Commit** + +```bash +git add frontend/utils/api.ts frontend/services/dto/client.ts frontend/services/clients.ts +git commit -m "feat : add Client DTO, service and Hydra utils (frontend)" +``` + +--- + +## Task 5 : DTOs & Service Project (Frontend) + +**Files:** +- Create: `frontend/services/dto/project.ts` +- Create: `frontend/services/projects.ts` + +**Step 1: Créer le DTO Project** + +Créer `frontend/services/dto/project.ts` : + +```typescript +import type { Client } from './client' + +export type Project = { + id: number + '@id'?: string + name: string + description: string | null + color: string + client: Client | null +} + +export type ProjectWrite = { + name: string + description: string | null + color: string + client: string | null // IRI : "/api/clients/1" ou null +} +``` + +**Step 2: Créer le service Project** + +Créer `frontend/services/projects.ts` : + +```typescript +import type { Project, ProjectWrite } from './dto/project' +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export function useProjectService() { + const api = useApi() + + async function getAll(): Promise { + const data = await api.get>('/projects') + return extractHydraMembers(data) + } + + async function create(payload: ProjectWrite): Promise { + return api.post('/projects', payload as Record, { + toastSuccessKey: 'projects.created', + }) + } + + async function update(id: number, payload: Partial): Promise { + return api.patch(`/projects/${id}`, payload as Record, { + toastSuccessKey: 'projects.updated', + }) + } + + async function remove(id: number): Promise { + await api.delete(`/projects/${id}`, {}, { + toastSuccessKey: 'projects.deleted', + }) + } + + return { getAll, create, update, remove } +} +``` + +**Step 3: Commit** + +```bash +git add frontend/services/dto/project.ts frontend/services/projects.ts +git commit -m "feat : add Project DTO and service (frontend)" +``` + +--- + +## Task 6 : Composant Drawer réutilisable + +**Files:** +- Create: `frontend/components/AppDrawer.vue` + +**Step 1: Créer le composant Drawer** + +Créer `frontend/components/AppDrawer.vue` : + +```vue + + + + + +``` + +**Step 2: Vérifier le rendu** + +Tester en important temporairement dans une page existante (optionnel). + +**Step 3: Commit** + +```bash +git add frontend/components/AppDrawer.vue +git commit -m "feat : add reusable AppDrawer component" +``` + +--- + +## Task 7 : Page Clients (Frontend) + +**Files:** +- Create: `frontend/pages/clients.vue` +- Create: `frontend/components/ClientDrawer.vue` + +**Step 1: Créer le formulaire drawer Client** + +Créer `frontend/components/ClientDrawer.vue` : + +```vue + + + +``` + +**Step 2: Créer la page Clients** + +Créer `frontend/pages/clients.vue` : + +```vue + + + +``` + +**Step 3: Vérifier le rendu** + +Run: `make dev-nuxt` puis naviguer vers `/clients` +Expected: Tableau vide (ou avec données si fixtures chargées), bouton "+ Ajouter un client", drawer s'ouvre au clic. + +**Step 4: Commit** + +```bash +git add frontend/pages/clients.vue frontend/components/ClientDrawer.vue +git commit -m "feat : add Clients page with table and drawer form" +``` + +--- + +## Task 8 : Page Projets (Frontend) + +**Files:** +- Create: `frontend/pages/projects.vue` +- Create: `frontend/components/ProjectDrawer.vue` +- Create: `frontend/components/ColorPicker.vue` + +**Step 1: Créer le composant ColorPicker** + +Créer `frontend/components/ColorPicker.vue` : + +```vue + + + +``` + +**Step 2: Créer le formulaire drawer Project** + +Créer `frontend/components/ProjectDrawer.vue` : + +```vue + + + +``` + +**Step 3: Créer la page Projets** + +Créer `frontend/pages/projects.vue` : + +```vue + + + +``` + +**Step 4: Supprimer l'ancienne page project-list si elle existe** + +Vérifier si `frontend/pages/project-list.vue` existe. Si oui, la supprimer (c'était un placeholder). + +**Step 5: Commit** + +```bash +git add frontend/components/ColorPicker.vue frontend/components/ProjectDrawer.vue frontend/pages/projects.vue +git rm frontend/pages/project-list.vue 2>/dev/null || true +git commit -m "feat : add Projects page with cards and drawer form" +``` + +--- + +## Task 9 : Navigation Sidebar + Traductions i18n + +**Files:** +- Modify: `frontend/layouts/default.vue` +- Modify: `frontend/i18n/locales/fr.json` + +**Step 1: Mettre à jour la sidebar** + +Dans `frontend/layouts/default.vue`, ajouter le lien "Clients" sous "Projets" dans la nav : + +```html + + + + Clients + +``` + +Aussi, mettre à jour le lien Projets pour pointer vers `/projects` (au lieu de `/project-list`) : + +Changer `to="/project-list"` → `to="/projects"` + +**Step 2: Ajouter les traductions** + +Modifier `frontend/i18n/locales/fr.json` pour ajouter les clés de toast : + +```json +{ + "errors": { + "http": { + "get": "Impossible de récupérer les données.", + "post": "Impossible de créer la ressource.", + "put": "Impossible de mettre à jour la ressource.", + "patch": "Impossible de mettre à jour la ressource.", + "delete": "Impossible de supprimer la ressource." + }, + "auth": { + "login": "Identifiants invalides.", + "logout": "Impossible de se déconnecter.", + "session": "Session expirée" + } + }, + "success": { + "auth": { + "login": "Connexion réussie.", + "logout": "Déconnexion réussie." + } + }, + "clients": { + "created": "Client créé avec succès.", + "updated": "Client mis à jour avec succès.", + "deleted": "Client supprimé avec succès." + }, + "projects": { + "created": "Projet créé avec succès.", + "updated": "Projet mis à jour avec succès.", + "deleted": "Projet supprimé avec succès." + } +} +``` + +**Step 3: Commit** + +```bash +git add frontend/layouts/default.vue frontend/i18n/locales/fr.json +git commit -m "feat : add Clients nav link and i18n translations" +``` + +--- + +## Task 10 : Mise à jour CLAUDE.md + +**Files:** +- Modify: `CLAUDE.md` + +**Step 1: Mettre à jour CLAUDE.md** + +Ajouter dans la section structure les nouveaux fichiers et patterns. Points à ajouter/mettre à jour : + +- Ajouter `Client` et `Project` dans les entités documentées +- Ajouter les fixtures complètes (3 clients, 4 projets) +- Documenter le pattern Drawer (AppDrawer + formulaires spécifiques) +- Documenter les services frontend (useClientService, useProjectService) +- Documenter les DTOs (Client, ClientWrite, Project, ProjectWrite) +- Documenter l'utilitaire Hydra (`utils/api.ts`) +- Documenter le composant ColorPicker +- Mettre à jour la structure des dossiers + +**Step 2: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs : update CLAUDE.md with Client and Project architecture" +``` + +--- + +## Task 11 : Tests manuels de bout en bout + +**Step 1: Recharger les fixtures** + +Run: `make db-reset` ou `make fixtures` +Expected: BDD reset avec admin + 3 clients + 4 projets + +**Step 2: Tester l'API Client via curl** + +```bash +# Login +curl -c cookies.txt -X POST http://localhost:8082/login_check \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"admin"}' + +# GET clients +curl -b cookies.txt http://localhost:8082/api/clients +``` +Expected: Liste de 3 clients au format Hydra + +**Step 3: Tester l'API Project via curl** + +```bash +curl -b cookies.txt http://localhost:8082/api/projects +``` +Expected: Liste de 4 projets au format Hydra, chaque projet avec `client` imbriqué ou null + +**Step 4: Tester le frontend** + +1. Naviguer vers `http://localhost:8082/` +2. Se connecter avec `admin` / `admin` +3. Cliquer "Projets" dans la sidebar → page avec 4 cartes colorées +4. Cliquer "+ Ajouter un projet" → drawer s'ouvre, remplir et enregistrer +5. Cliquer "Clients" dans la sidebar → page avec tableau de 3 clients +6. Cliquer sur une ligne → drawer s'ouvre avec les données pré-remplies +7. Cliquer "+ Ajouter un client" → drawer vide s'ouvre + +**Step 5: Commit final si ajustements** + +```bash +git add -A +git commit -m "fix : adjustments after manual testing" +``` + +--- + +## Résumé des commits prévus + +| # | Message | Scope | +|---|---------|-------| +| 1 | `feat : add Client entity with CRUD API` | Backend | +| 2 | `feat : add Project entity with CRUD API and Client relation` | Backend | +| 3 | `feat : add Client and Project fixtures` | Backend | +| 4 | `feat : add Client DTO, service and Hydra utils (frontend)` | Frontend | +| 5 | `feat : add Project DTO and service (frontend)` | Frontend | +| 6 | `feat : add reusable AppDrawer component` | Frontend | +| 7 | `feat : add Clients page with table and drawer form` | Frontend | +| 8 | `feat : add Projects page with cards and drawer form` | Frontend | +| 9 | `feat : add Clients nav link and i18n translations` | Frontend | +| 10 | `docs : update CLAUDE.md with Client and Project architecture` | Docs | diff --git a/docs/plans/2026-03-09-task-management.md b/docs/plans/2026-03-09-task-management.md new file mode 100644 index 0000000..548e22f --- /dev/null +++ b/docs/plans/2026-03-09-task-management.md @@ -0,0 +1,2421 @@ +# Task Management (Tickets + Kanban) Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Ajouter un système de gestion de tâches (tickets) avec vue Kanban et Backlog par projet, incluant des entités configurables (Status, Effort, Priorité, Type, Groupe) administrables via un onglet admin. + +**Architecture:** 6 nouvelles entités Doctrine exposées via API Platform. Le frontend ajoute une page admin avec CRUD pour les entités de configuration, et une page projet avec Kanban + Backlog. Les patterns existants (Entity → Repository → ApiResource, Service → DTO → Drawer) sont réutilisés à l'identique. + +**Tech Stack:** Symfony 8 / API Platform 4 / Doctrine ORM (backend), Nuxt 4 / Vue 3 / Pinia / Tailwind CSS (frontend) + +--- + +## Modèle de données + +``` +TaskStatus { id, label, color, position } — ex: "A faire", "En cours", "Bloqué"… +TaskEffort { id, label } — ex: "S", "M", "L", "XL", "XXL" +TaskPriority { id, label, color } — ex: "Basse" (bleu), "Moyen" (orange), "Haute" (rouge) +TaskType { id, label, color } — ex: "Gestion mdp", "Connexion", "Calendrier" +TaskGroup { id, title, description, color, project } — lié à un projet, ex: groupe filtrable +Task { id, title, description, status, effort, priority, assignee(User), group, project, types(M2M) } +``` + +--- + +## Phase 1 : Backend — Entités de configuration + +### Task 1.1 : Entité TaskStatus + +**Files:** +- Create: `src/Entity/TaskStatus.php` +- Create: `src/Repository/TaskStatusRepository.php` + +**Step 1: Create TaskStatusRepository** + +```php + + */ +class TaskStatusRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, TaskStatus::class); + } +} +``` + +**Step 2: Create TaskStatus entity** + +```php + ['task_status:read']], + denormalizationContext: ['groups' => ['task_status:write']], + order: ['position' => 'ASC'], +)] +#[ORM\Entity(repositoryClass: TaskStatusRepository::class)] +class TaskStatus +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['task_status:read', 'task:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['task_status:read', 'task_status:write', 'task:read'])] + private ?string $label = null; + + #[ORM\Column(length: 7)] + #[Groups(['task_status:read', 'task_status:write', 'task:read'])] + private ?string $color = '#222783'; + + #[ORM\Column(type: 'integer')] + #[Groups(['task_status:read', 'task_status:write', 'task:read'])] + private int $position = 0; + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): static + { + $this->label = $label; + + return $this; + } + + public function getColor(): ?string + { + return $this->color; + } + + public function setColor(string $color): static + { + $this->color = $color; + + return $this; + } + + public function getPosition(): int + { + return $this->position; + } + + public function setPosition(int $position): static + { + $this->position = $position; + + return $this; + } +} +``` + +**Step 3: Commit** + +```bash +git add src/Entity/TaskStatus.php src/Repository/TaskStatusRepository.php +git commit -m "feat : add TaskStatus entity" +``` + +--- + +### Task 1.2 : Entité TaskEffort + +**Files:** +- Create: `src/Entity/TaskEffort.php` +- Create: `src/Repository/TaskEffortRepository.php` + +**Step 1: Create TaskEffortRepository** (même pattern que TaskStatusRepository) + +**Step 2: Create TaskEffort entity** + +```php + ['task_effort:read']], + denormalizationContext: ['groups' => ['task_effort:write']], + order: ['label' => 'ASC'], +)] +#[ORM\Entity(repositoryClass: TaskEffortRepository::class)] +class TaskEffort +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['task_effort:read', 'task:read'])] + private ?int $id = null; + + #[ORM\Column(length: 50)] + #[Groups(['task_effort:read', 'task_effort:write', 'task:read'])] + private ?string $label = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): static + { + $this->label = $label; + + return $this; + } +} +``` + +**Step 3: Commit** + +```bash +git add src/Entity/TaskEffort.php src/Repository/TaskEffortRepository.php +git commit -m "feat : add TaskEffort entity" +``` + +--- + +### Task 1.3 : Entité TaskPriority + +**Files:** +- Create: `src/Entity/TaskPriority.php` +- Create: `src/Repository/TaskPriorityRepository.php` + +**Step 1: Create TaskPriorityRepository** (même pattern) + +**Step 2: Create TaskPriority entity** + +```php + ['task_priority:read']], + denormalizationContext: ['groups' => ['task_priority:write']], + order: ['label' => 'ASC'], +)] +#[ORM\Entity(repositoryClass: TaskPriorityRepository::class)] +class TaskPriority +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['task_priority:read', 'task:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['task_priority:read', 'task_priority:write', 'task:read'])] + private ?string $label = null; + + #[ORM\Column(length: 7)] + #[Groups(['task_priority:read', 'task_priority:write', 'task:read'])] + private ?string $color = '#222783'; + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): static + { + $this->label = $label; + + return $this; + } + + public function getColor(): ?string + { + return $this->color; + } + + public function setColor(string $color): static + { + $this->color = $color; + + return $this; + } +} +``` + +**Step 3: Commit** + +```bash +git add src/Entity/TaskPriority.php src/Repository/TaskPriorityRepository.php +git commit -m "feat : add TaskPriority entity" +``` + +--- + +### Task 1.4 : Entité TaskType + +**Files:** +- Create: `src/Entity/TaskType.php` +- Create: `src/Repository/TaskTypeRepository.php` + +**Step 1: Create TaskTypeRepository** (même pattern) + +**Step 2: Create TaskType entity** + +Structure identique à TaskPriority : `id`, `label` (255), `color` (7, default `#222783`). + +```php + ['task_type:read']], + denormalizationContext: ['groups' => ['task_type:write']], + order: ['label' => 'ASC'], +)] +#[ORM\Entity(repositoryClass: TaskTypeRepository::class)] +class TaskType +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['task_type:read', 'task:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['task_type:read', 'task_type:write', 'task:read'])] + private ?string $label = null; + + #[ORM\Column(length: 7)] + #[Groups(['task_type:read', 'task_type:write', 'task:read'])] + private ?string $color = '#222783'; + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): static + { + $this->label = $label; + + return $this; + } + + public function getColor(): ?string + { + return $this->color; + } + + public function setColor(string $color): static + { + $this->color = $color; + + return $this; + } +} +``` + +**Step 3: Commit** + +```bash +git add src/Entity/TaskType.php src/Repository/TaskTypeRepository.php +git commit -m "feat : add TaskType entity" +``` + +--- + +### Task 1.5 : Entité TaskGroup + +**Files:** +- Create: `src/Entity/TaskGroup.php` +- Create: `src/Repository/TaskGroupRepository.php` + +**Step 1: Create TaskGroupRepository** (même pattern) + +**Step 2: Create TaskGroup entity** + +```php + ['task_group:read']], + denormalizationContext: ['groups' => ['task_group:write']], + order: ['title' => 'ASC'], +)] +#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact'])] +#[ORM\Entity(repositoryClass: TaskGroupRepository::class)] +class TaskGroup +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['task_group:read', 'task:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['task_group:read', 'task_group:write', 'task:read'])] + private ?string $title = null; + + #[ORM\Column(type: 'text', nullable: true)] + #[Groups(['task_group:read', 'task_group:write'])] + private ?string $description = null; + + #[ORM\Column(length: 7)] + #[Groups(['task_group:read', 'task_group:write', 'task:read'])] + private ?string $color = '#222783'; + + #[ORM\ManyToOne(targetEntity: Project::class)] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + #[Groups(['task_group:read', 'task_group:write'])] + private ?Project $project = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + + return $this; + } + + public function getColor(): ?string + { + return $this->color; + } + + public function setColor(string $color): static + { + $this->color = $color; + + return $this; + } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): static + { + $this->project = $project; + + return $this; + } +} +``` + +**Step 3: Commit** + +```bash +git add src/Entity/TaskGroup.php src/Repository/TaskGroupRepository.php +git commit -m "feat : add TaskGroup entity" +``` + +--- + +### Task 1.6 : Entité Task + +**Files:** +- Create: `src/Entity/Task.php` +- Create: `src/Repository/TaskRepository.php` + +**Step 1: Create TaskRepository** (même pattern) + +**Step 2: Create Task entity** + +```php + ['task:read']], + denormalizationContext: ['groups' => ['task:write']], + order: ['id' => 'DESC'], +)] +#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact', 'group' => 'exact'])] +#[ORM\Entity(repositoryClass: TaskRepository::class)] +class Task +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['task:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['task:read', 'task:write'])] + private ?string $title = null; + + #[ORM\Column(type: 'text', nullable: true)] + #[Groups(['task:read', 'task:write'])] + private ?string $description = null; + + #[ORM\ManyToOne(targetEntity: TaskStatus::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['task:read', 'task:write'])] + private ?TaskStatus $status = null; + + #[ORM\ManyToOne(targetEntity: TaskEffort::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['task:read', 'task:write'])] + private ?TaskEffort $effort = null; + + #[ORM\ManyToOne(targetEntity: TaskPriority::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['task:read', 'task:write'])] + private ?TaskPriority $priority = null; + + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['task:read', 'task:write'])] + private ?User $assignee = null; + + #[ORM\ManyToOne(targetEntity: TaskGroup::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['task:read', 'task:write'])] + private ?TaskGroup $group = null; + + #[ORM\ManyToOne(targetEntity: Project::class)] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + #[Groups(['task:read', 'task:write'])] + private ?Project $project = null; + + /** @var Collection */ + #[ORM\ManyToMany(targetEntity: TaskType::class)] + #[ORM\JoinTable(name: 'task_task_type')] + #[Groups(['task:read', 'task:write'])] + private Collection $types; + + public function __construct() + { + $this->types = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + + return $this; + } + + public function getStatus(): ?TaskStatus + { + return $this->status; + } + + public function setStatus(?TaskStatus $status): static + { + $this->status = $status; + + return $this; + } + + public function getEffort(): ?TaskEffort + { + return $this->effort; + } + + public function setEffort(?TaskEffort $effort): static + { + $this->effort = $effort; + + return $this; + } + + public function getPriority(): ?TaskPriority + { + return $this->priority; + } + + public function setPriority(?TaskPriority $priority): static + { + $this->priority = $priority; + + return $this; + } + + public function getAssignee(): ?User + { + return $this->assignee; + } + + public function setAssignee(?User $assignee): static + { + $this->assignee = $assignee; + + return $this; + } + + public function getGroup(): ?TaskGroup + { + return $this->group; + } + + public function setGroup(?TaskGroup $group): static + { + $this->group = $group; + + return $this; + } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): static + { + $this->project = $project; + + return $this; + } + + /** @return Collection */ + public function getTypes(): Collection + { + return $this->types; + } + + public function addType(TaskType $type): static + { + if (!$this->types->contains($type)) { + $this->types->add($type); + } + + return $this; + } + + public function removeType(TaskType $type): static + { + $this->types->removeElement($type); + + return $this; + } +} +``` + +Note : ajouter aussi `'task:read'` aux groups de `User.id` et `User.username` dans `src/Entity/User.php` pour que l'assignee soit sérialisé correctement : + +```php +// Dans User.php, ajouter 'task:read' aux groups de id et username +#[Groups(['me:read', 'task:read'])] +private ?int $id = null; + +#[Groups(['me:read', 'task:read'])] +private ?string $username = null; +``` + +**Step 3: Commit** + +```bash +git add src/Entity/Task.php src/Repository/TaskRepository.php src/Entity/User.php +git commit -m "feat : add Task entity with relations" +``` + +--- + +### Task 1.7 : Migration + Fixtures + +**Step 1: Générer et exécuter la migration** + +```bash +make shell +# Dans le container : +php bin/console doctrine:migrations:diff +php bin/console doctrine:migrations:migrate --no-interaction +exit +``` + +**Step 2: Mettre à jour les fixtures** dans `src/DataFixtures/AppFixtures.php` + +Ajouter après les projets existants : + +```php +// --- TaskStatus (ordonnés par position) --- +$statusTodo = new TaskStatus(); +$statusTodo->setLabel('A faire'); +$statusTodo->setColor('#1565C0'); +$statusTodo->setPosition(0); +$manager->persist($statusTodo); + +$statusInProgress = new TaskStatus(); +$statusInProgress->setLabel('En cours'); +$statusInProgress->setColor('#6A6A6A'); +$statusInProgress->setPosition(1); +$manager->persist($statusInProgress); + +$statusBlocked = new TaskStatus(); +$statusBlocked->setLabel('Bloqué'); +$statusBlocked->setColor('#1565C0'); +$statusBlocked->setPosition(2); +$manager->persist($statusBlocked); + +$statusReview = new TaskStatus(); +$statusReview->setLabel('En attente de validation'); +$statusReview->setColor('#1565C0'); +$statusReview->setPosition(3); +$manager->persist($statusReview); + +$statusDone = new TaskStatus(); +$statusDone->setLabel('Terminé'); +$statusDone->setColor('#1565C0'); +$statusDone->setPosition(4); +$manager->persist($statusDone); + +// --- TaskEffort --- +$effortS = new TaskEffort(); +$effortS->setLabel('S'); +$manager->persist($effortS); + +$effortM = new TaskEffort(); +$effortM->setLabel('M'); +$manager->persist($effortM); + +$effortL = new TaskEffort(); +$effortL->setLabel('L'); +$manager->persist($effortL); + +$effortXL = new TaskEffort(); +$effortXL->setLabel('XL'); +$manager->persist($effortXL); + +$effortXXL = new TaskEffort(); +$effortXXL->setLabel('XXL'); +$manager->persist($effortXXL); + +// --- TaskPriority --- +$priorityLow = new TaskPriority(); +$priorityLow->setLabel('Basse'); +$priorityLow->setColor('#1565C0'); +$manager->persist($priorityLow); + +$priorityMedium = new TaskPriority(); +$priorityMedium->setLabel('Moyen'); +$priorityMedium->setColor('#FF8F00'); +$manager->persist($priorityMedium); + +$priorityHigh = new TaskPriority(); +$priorityHigh->setLabel('Haute'); +$priorityHigh->setColor('#C62828'); +$manager->persist($priorityHigh); + +// --- TaskType --- +$typePassword = new TaskType(); +$typePassword->setLabel('Gestion mdp'); +$typePassword->setColor('#C62828'); +$manager->persist($typePassword); + +$typeAuth = new TaskType(); +$typeAuth->setLabel('Connexion'); +$typeAuth->setColor('#FF8F00'); +$manager->persist($typeAuth); + +$typeCalendar = new TaskType(); +$typeCalendar->setLabel('Calendrier'); +$typeCalendar->setColor('#1565C0'); +$manager->persist($typeCalendar); + +// --- TaskGroup (liés au projet SIRH) --- +$groupFrontend = new TaskGroup(); +$groupFrontend->setTitle('Frontend'); +$groupFrontend->setColor('#4A90D9'); +$groupFrontend->setProject($projectSirh); +$manager->persist($groupFrontend); + +$groupBackend = new TaskGroup(); +$groupBackend->setTitle('Backend'); +$groupBackend->setColor('#26A69A'); +$groupBackend->setProject($projectSirh); +$manager->persist($groupBackend); + +// --- Tasks (projet SIRH) --- +$task1 = new Task(); +$task1->setTitle('Création d\'une page de login'); +$task1->setDescription('Implémenter la page de connexion avec formulaire.'); +$task1->setStatus($statusTodo); +$task1->setEffort($effortXXL); +$task1->setPriority($priorityLow); +$task1->setAssignee($admin); +$task1->setGroup($groupFrontend); +$task1->setProject($projectSirh); +$task1->addType($typePassword); +$manager->persist($task1); + +$task2 = new Task(); +$task2->setTitle('Création d\'une page de login'); +$task2->setDescription('Gérer la connexion OAuth.'); +$task2->setStatus($statusTodo); +$task2->setEffort($effortL); +$task2->setPriority($priorityHigh); +$task2->setAssignee($admin); +$task2->setGroup($groupFrontend); +$task2->setProject($projectSirh); +$task2->addType($typeAuth); +$manager->persist($task2); + +$task3 = new Task(); +$task3->setTitle('Création d\'une page de login'); +$task3->setStatus($statusInProgress); +$task3->setEffort($effortXXL); +$task3->setPriority($priorityLow); +$task3->setAssignee($admin); +$task3->setGroup($groupBackend); +$task3->setProject($projectSirh); +$task3->addType($typePassword); +$manager->persist($task3); + +$task4 = new Task(); +$task4->setTitle('Création d\'une page de login'); +$task4->setStatus($statusBlocked); +$task4->setEffort($effortXXL); +$task4->setPriority($priorityLow); +$task4->setAssignee($admin); +$task4->setProject($projectSirh); +$task4->addType($typePassword); +$manager->persist($task4); + +$task5 = new Task(); +$task5->setTitle('Création d\'une page de login'); +$task5->setStatus($statusReview); +$task5->setEffort($effortXXL); +$task5->setPriority($priorityMedium); +$task5->setAssignee($admin); +$task5->setProject($projectSirh); +$task5->addType($typeCalendar); +$manager->persist($task5); + +$task6 = new Task(); +$task6->setTitle('Création d\'une page de login'); +$task6->setStatus($statusDone); +$task6->setEffort($effortXXL); +$task6->setPriority($priorityHigh); +$task6->setAssignee($admin); +$task6->setProject($projectSirh); +$task6->addType($typeAuth); +$manager->persist($task6); +``` + +**Step 3: Recharger les fixtures** + +```bash +make db-reset +``` + +**Step 4: Commit** + +```bash +git add src/DataFixtures/AppFixtures.php migrations/ +git commit -m "feat : add task fixtures and migration" +``` + +--- + +## Phase 2 : Frontend — Services & DTOs pour les entités de configuration + +### Task 2.1 : DTOs TypeScript + +**Files:** +- Create: `frontend/services/dto/task-status.ts` +- Create: `frontend/services/dto/task-effort.ts` +- Create: `frontend/services/dto/task-priority.ts` +- Create: `frontend/services/dto/task-type.ts` +- Create: `frontend/services/dto/task-group.ts` +- Create: `frontend/services/dto/task.ts` + +**Step 1: Créer tous les DTOs** + +```typescript +// frontend/services/dto/task-status.ts +export type TaskStatus = { + id: number + '@id'?: string + label: string + color: string + position: number +} + +export type TaskStatusWrite = { + label: string + color: string + position: number +} +``` + +```typescript +// frontend/services/dto/task-effort.ts +export type TaskEffort = { + id: number + '@id'?: string + label: string +} + +export type TaskEffortWrite = { + label: string +} +``` + +```typescript +// frontend/services/dto/task-priority.ts +export type TaskPriority = { + id: number + '@id'?: string + label: string + color: string +} + +export type TaskPriorityWrite = { + label: string + color: string +} +``` + +```typescript +// frontend/services/dto/task-type.ts +export type TaskType = { + id: number + '@id'?: string + label: string + color: string +} + +export type TaskTypeWrite = { + label: string + color: string +} +``` + +```typescript +// frontend/services/dto/task-group.ts +import type { Project } from './project' + +export type TaskGroup = { + id: number + '@id'?: string + title: string + description: string | null + color: string + project: Project | null +} + +export type TaskGroupWrite = { + title: string + description: string | null + color: string + project: string // IRI +} +``` + +```typescript +// frontend/services/dto/task.ts +import type { TaskStatus } from './task-status' +import type { TaskEffort } from './task-effort' +import type { TaskPriority } from './task-priority' +import type { TaskType } from './task-type' +import type { TaskGroup } from './task-group' +import type { UserData } from './user-data' + +export type Task = { + id: number + '@id'?: string + title: string + description: string | null + status: TaskStatus | null + effort: TaskEffort | null + priority: TaskPriority | null + assignee: UserData | null + group: TaskGroup | null + types: TaskType[] +} + +export type TaskWrite = { + title: string + description: string | null + status: string | null // IRI + effort: string | null // IRI + priority: string | null // IRI + assignee: string | null // IRI + group: string | null // IRI + project: string // IRI + types: string[] // IRIs +} +``` + +**Step 2: Commit** + +```bash +git add frontend/services/dto/task-status.ts frontend/services/dto/task-effort.ts \ + frontend/services/dto/task-priority.ts frontend/services/dto/task-type.ts \ + frontend/services/dto/task-group.ts frontend/services/dto/task.ts +git commit -m "feat : add task-related DTOs" +``` + +--- + +### Task 2.2 : Services API + +**Files:** +- Create: `frontend/services/task-statuses.ts` +- Create: `frontend/services/task-efforts.ts` +- Create: `frontend/services/task-priorities.ts` +- Create: `frontend/services/task-types.ts` +- Create: `frontend/services/task-groups.ts` +- Create: `frontend/services/tasks.ts` + +Chaque service suit le pattern de `useClientService()` : + +```typescript +// frontend/services/task-statuses.ts +import type { TaskStatus, TaskStatusWrite } from './dto/task-status' +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export function useTaskStatusService() { + const api = useApi() + + async function getAll(): Promise { + const data = await api.get>('/task_statuses') + return extractHydraMembers(data) + } + + async function create(payload: TaskStatusWrite): Promise { + return api.post('/task_statuses', payload as Record, { + toastSuccessKey: 'taskStatuses.created', + }) + } + + async function update(id: number, payload: Partial): Promise { + return api.patch(`/task_statuses/${id}`, payload as Record, { + toastSuccessKey: 'taskStatuses.updated', + }) + } + + async function remove(id: number): Promise { + await api.delete(`/task_statuses/${id}`, {}, { + toastSuccessKey: 'taskStatuses.deleted', + }) + } + + return { getAll, create, update, remove } +} +``` + +Reproduire ce pattern pour les 5 autres services : +- `useTaskEffortService()` → `/task_efforts` → clé i18n `taskEfforts` +- `useTaskPriorityService()` → `/task_priorities` → clé i18n `taskPriorities` +- `useTaskTypeService()` → `/task_types` → clé i18n `taskTypes` +- `useTaskGroupService()` → `/task_groups` → clé i18n `taskGroups` +- `useTaskService()` → `/tasks` → clé i18n `tasks` (+ paramètre query `project` pour filtrer) + +Pour `tasks.ts` ajouter un `getByProject` : + +```typescript +// frontend/services/tasks.ts +import type { Task, TaskWrite } from './dto/task' +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export function useTaskService() { + const api = useApi() + + async function getByProject(projectId: number): Promise { + const data = await api.get>('/tasks', { project: `/api/projects/${projectId}` }) + return extractHydraMembers(data) + } + + async function create(payload: TaskWrite): Promise { + return api.post('/tasks', payload as Record, { + toastSuccessKey: 'tasks.created', + }) + } + + async function update(id: number, payload: Partial): Promise { + return api.patch(`/tasks/${id}`, payload as Record, { + toastSuccessKey: 'tasks.updated', + }) + } + + async function remove(id: number): Promise { + await api.delete(`/tasks/${id}`, {}, { + toastSuccessKey: 'tasks.deleted', + }) + } + + return { getByProject, create, update, remove } +} +``` + +**Step 2: Commit** + +```bash +git add frontend/services/task-statuses.ts frontend/services/task-efforts.ts \ + frontend/services/task-priorities.ts frontend/services/task-types.ts \ + frontend/services/task-groups.ts frontend/services/tasks.ts +git commit -m "feat : add task-related API services" +``` + +--- + +### Task 2.3 : Traductions i18n + +**File:** Modify `frontend/i18n/locales/fr.json` + +Ajouter les clés : + +```json +{ + "taskStatuses": { + "created": "Statut créé avec succès.", + "updated": "Statut mis à jour avec succès.", + "deleted": "Statut supprimé avec succès." + }, + "taskEfforts": { + "created": "Effort créé avec succès.", + "updated": "Effort mis à jour avec succès.", + "deleted": "Effort supprimé avec succès." + }, + "taskPriorities": { + "created": "Priorité créée avec succès.", + "updated": "Priorité mise à jour avec succès.", + "deleted": "Priorité supprimée avec succès." + }, + "taskTypes": { + "created": "Type créé avec succès.", + "updated": "Type mis à jour avec succès.", + "deleted": "Type supprimé avec succès." + }, + "taskGroups": { + "created": "Groupe créé avec succès.", + "updated": "Groupe mis à jour avec succès.", + "deleted": "Groupe supprimé avec succès." + }, + "tasks": { + "created": "Ticket créé avec succès.", + "updated": "Ticket mis à jour avec succès.", + "deleted": "Ticket supprimé avec succès." + } +} +``` + +**Step 2: Commit** + +```bash +git add frontend/i18n/locales/fr.json +git commit -m "feat : add i18n translations for task entities" +``` + +--- + +## Phase 3 : Frontend — Page Admin (CRUD Status, Effort, Priorité, Type) + +### Task 3.1 : Page Admin avec onglets + +**Files:** +- Create: `frontend/pages/admin.vue` + +La page affiche 4 onglets : **Statuts**, **Efforts**, **Priorités**, **Types**. Chaque onglet montre une liste avec bouton d'ajout et un drawer pour créer/modifier. + +**Step 1: Créer la page admin** + +```vue + + + +``` + +**Step 2: Commit** + +```bash +git add frontend/pages/admin.vue +git commit -m "feat : add admin page with tabs" +``` + +--- + +### Task 3.2 : Composant AdminStatusTab + +**Files:** +- Create: `frontend/components/AdminStatusTab.vue` +- Create: `frontend/components/TaskStatusDrawer.vue` + +**Step 1: Créer TaskStatusDrawer** + +```vue + + + +``` + +**Step 2: Créer AdminStatusTab** + +```vue + + + +``` + +**Step 3: Commit** + +```bash +git add frontend/components/AdminStatusTab.vue frontend/components/TaskStatusDrawer.vue +git commit -m "feat : add admin status tab with drawer CRUD" +``` + +--- + +### Task 3.3 : Composant AdminEffortTab + +**Files:** +- Create: `frontend/components/AdminEffortTab.vue` +- Create: `frontend/components/TaskEffortDrawer.vue` + +Même pattern que AdminStatusTab mais sans couleur ni position. Le drawer a juste un champ `label`. La table affiche juste `Libellé` et `Actions`. + +**Step 1: Commit** + +```bash +git add frontend/components/AdminEffortTab.vue frontend/components/TaskEffortDrawer.vue +git commit -m "feat : add admin effort tab with drawer CRUD" +``` + +--- + +### Task 3.4 : Composant AdminPriorityTab + +**Files:** +- Create: `frontend/components/AdminPriorityTab.vue` +- Create: `frontend/components/TaskPriorityDrawer.vue` + +Même pattern que AdminStatusTab mais avec `label` + `color` (ColorPicker). Pas de position. + +**Step 1: Commit** + +```bash +git add frontend/components/AdminPriorityTab.vue frontend/components/TaskPriorityDrawer.vue +git commit -m "feat : add admin priority tab with drawer CRUD" +``` + +--- + +### Task 3.5 : Composant AdminTypeTab + +**Files:** +- Create: `frontend/components/AdminTypeTab.vue` +- Create: `frontend/components/TaskTypeDrawer.vue` + +Identique à AdminPriorityTab : `label` + `color`. + +**Step 1: Commit** + +```bash +git add frontend/components/AdminTypeTab.vue frontend/components/TaskTypeDrawer.vue +git commit -m "feat : add admin type tab with drawer CRUD" +``` + +--- + +### Task 3.6 : Ajouter le lien Admin dans la sidebar + +**File:** Modify `frontend/layouts/default.vue` + +Ajouter un nouveau `NuxtLink` après "Clients" dans la sidebar : + +```vue + + + Administration + +``` + +**Step 1: Commit** + +```bash +git add frontend/layouts/default.vue +git commit -m "feat : add Admin nav link in sidebar" +``` + +--- + +## Phase 4 : Frontend — Page Projet (Kanban + Backlog) + +### Task 4.1 : Page projet dynamique + +**Files:** +- Create: `frontend/pages/projects/[id].vue` + +Cette page affiche le board d'un projet. Elle charge : le projet, les tasks, les statuses, les groupes, les efforts, les priorités, les types, les users. + +```vue + + + +``` + +Note : il faut ajouter `getById` dans `useProjectService()` : + +```typescript +async function getById(id: number): Promise { + return api.get(`/projects/${id}`) +} +``` + +Et `getByProject` dans `useTaskGroupService()` : + +```typescript +async function getByProject(projectId: number): Promise { + const data = await api.get>('/task_groups', { project: `/api/projects/${projectId}` }) + return extractHydraMembers(data) +} +``` + +**Step 2: Commit** + +```bash +git add frontend/pages/projects/\[id\].vue frontend/services/projects.ts frontend/services/task-groups.ts +git commit -m "feat : add project board page with kanban and backlog" +``` + +--- + +### Task 4.2 : Composant TaskCard (carte Kanban) + +**Files:** +- Create: `frontend/components/TaskCard.vue` + +```vue + + + +``` + +**Step 1: Commit** + +```bash +git add frontend/components/TaskCard.vue +git commit -m "feat : add TaskCard component for kanban" +``` + +--- + +### Task 4.3 : Composant TaskDrawer (créer/modifier un ticket) + +**Files:** +- Create: `frontend/components/TaskDrawer.vue` + +```vue + + + +``` + +**Step 1: Commit** + +```bash +git add frontend/components/TaskDrawer.vue +git commit -m "feat : add TaskDrawer component for ticket create/edit" +``` + +--- + +### Task 4.4 : Composant TaskGroupDrawer + +**Files:** +- Create: `frontend/components/TaskGroupDrawer.vue` + +```vue + + + +``` + +**Step 1: Commit** + +```bash +git add frontend/components/TaskGroupDrawer.vue +git commit -m "feat : add TaskGroupDrawer component" +``` + +--- + +### Task 4.5 : Navigation projet → board + +**File:** Modify `frontend/pages/projects.vue` + +Changer le clic sur une carte projet pour naviguer vers le board au lieu d'ouvrir le drawer d'édition. Ajouter un bouton d'édition séparé sur la carte. + +Remplacer le `@click="openEdit(project)"` sur la carte par `@click="navigateTo(`/projects/${project.id}`)"`. + +Garder le bouton edit avec un `@click.stop="openEdit(project)"` sur un petit bouton icon dans la carte. + +**Step 1: Commit** + +```bash +git add frontend/pages/projects.vue +git commit -m "feat : navigate to project board on card click" +``` + +--- + +## Phase 5 : Vérification finale + +### Task 5.1 : Vérification + +**Step 1:** Lancer `make db-reset` pour vérifier les fixtures + +**Step 2:** Lancer `make dev-nuxt` et vérifier : +- Page admin accessible, CRUD status/effort/priorité/type fonctionnels +- Clic sur un projet → board avec kanban + backlog +- Filtre par groupe fonctionne +- Création de ticket via drawer +- Création de groupe via drawer + +**Step 3:** Lancer `make php-cs-fixer-allow-risky` pour fixer le code style PHP + +**Step 4: Commit final** + +```bash +git add -A +git commit -m "fix : code style and final adjustments" +``` + +--- + +## Résumé des fichiers + +### Backend (à créer) +| Fichier | Description | +|---------|-------------| +| `src/Entity/TaskStatus.php` | Entité statut (label, color, position) | +| `src/Entity/TaskEffort.php` | Entité effort (label) | +| `src/Entity/TaskPriority.php` | Entité priorité (label, color) | +| `src/Entity/TaskType.php` | Entité type (label, color) | +| `src/Entity/TaskGroup.php` | Entité groupe (title, description, color, project) | +| `src/Entity/Task.php` | Entité tâche (title, desc, relations) | +| `src/Repository/TaskStatus…Repository.php` | 6 repositories | + +### Backend (à modifier) +| Fichier | Modification | +|---------|-------------| +| `src/Entity/User.php` | Ajouter group `task:read` sur id et username | +| `src/DataFixtures/AppFixtures.php` | Ajouter fixtures pour toutes les nouvelles entités | + +### Frontend (à créer) +| Fichier | Description | +|---------|-------------| +| `frontend/services/dto/task-status.ts` | DTO TaskStatus | +| `frontend/services/dto/task-effort.ts` | DTO TaskEffort | +| `frontend/services/dto/task-priority.ts` | DTO TaskPriority | +| `frontend/services/dto/task-type.ts` | DTO TaskType | +| `frontend/services/dto/task-group.ts` | DTO TaskGroup | +| `frontend/services/dto/task.ts` | DTO Task | +| `frontend/services/task-statuses.ts` | Service API statuts | +| `frontend/services/task-efforts.ts` | Service API efforts | +| `frontend/services/task-priorities.ts` | Service API priorités | +| `frontend/services/task-types.ts` | Service API types | +| `frontend/services/task-groups.ts` | Service API groupes | +| `frontend/services/tasks.ts` | Service API tâches | +| `frontend/pages/admin.vue` | Page admin avec onglets | +| `frontend/pages/projects/[id].vue` | Page board projet (kanban + backlog) | +| `frontend/components/AdminStatusTab.vue` | Tab CRUD statuts | +| `frontend/components/AdminEffortTab.vue` | Tab CRUD efforts | +| `frontend/components/AdminPriorityTab.vue` | Tab CRUD priorités | +| `frontend/components/AdminTypeTab.vue` | Tab CRUD types | +| `frontend/components/TaskStatusDrawer.vue` | Drawer statut | +| `frontend/components/TaskEffortDrawer.vue` | Drawer effort | +| `frontend/components/TaskPriorityDrawer.vue` | Drawer priorité | +| `frontend/components/TaskTypeDrawer.vue` | Drawer type | +| `frontend/components/TaskCard.vue` | Carte kanban | +| `frontend/components/TaskDrawer.vue` | Drawer ticket | +| `frontend/components/TaskGroupDrawer.vue` | Drawer groupe | + +### Frontend (à modifier) +| Fichier | Modification | +|---------|-------------| +| `frontend/i18n/locales/fr.json` | Ajouter traductions task* | +| `frontend/layouts/default.vue` | Ajouter lien Admin sidebar | +| `frontend/pages/projects.vue` | Naviguer vers board au clic | +| `frontend/services/projects.ts` | Ajouter `getById()` | diff --git a/src/State/MeProvider.php b/src/State/MeProvider.php index 221a3e6..f2866a7 100644 --- a/src/State/MeProvider.php +++ b/src/State/MeProvider.php @@ -20,7 +20,7 @@ final readonly class MeProvider implements ProviderInterface public function provide(Operation $operation, array $uriVariables = [], array $context = []): User { - /** @var User $user */ + // @var User $user return $this->security->getUser(); } }