# 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()` |