# Task Archiving Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Allow archiving individual tasks (when status is final) and entire groups (when all tasks have final status), with a dedicated archives page per project and a delete confirmation modal. **Architecture:** Add `isFinal` boolean on TaskStatus, `archived` boolean on Task and TaskGroup. Frontend filters archived items from kanban, shows them in a new `/projects/[id]/archives` page. Group archiving is handled via sequential PATCH calls from frontend. **Tech Stack:** Symfony 8 / API Platform 4 / Doctrine ORM (backend), Nuxt 4 / Vue 3 / Pinia / Tailwind CSS (frontend) --- ## Chunk 1: Backend — Schema & API changes ### Task 1: Add `isFinal` to TaskStatus entity **Files:** - Modify: `src/Entity/TaskStatus.php:46-48` - [ ] **Step 1: Add `isFinal` property with ORM mapping and serialization groups** Add after the `$position` property (line 48): ```php #[ORM\Column(type: 'boolean')] #[Groups(['task_status:read', 'task_status:write', 'task:read'])] private bool $isFinal = false; ``` - [ ] **Step 2: Add getter and setter** Add after `setPosition()` (line 89): ```php public function isFinal(): bool { return $this->isFinal; } public function setIsFinal(bool $isFinal): static { $this->isFinal = $isFinal; return $this; } ``` - [ ] **Step 3: Commit** ```bash git add src/Entity/TaskStatus.php git commit -m "feat(backend) : add isFinal field to TaskStatus entity" ``` ### Task 2: Add `archived` to Task entity **Files:** - Modify: `src/Entity/Task.php:7-8,34,84` - [ ] **Step 1: Add BooleanFilter import and filter attribute** Add import at top of file: ```php use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; ``` Add a second `#[ApiFilter]` line after the existing SearchFilter (line 34): ```php #[ApiFilter(BooleanFilter::class, properties: ['archived'])] ``` - [ ] **Step 2: Add `archived` property with ORM mapping and serialization groups** Add after the `$tags` property (line 94): ```php #[ORM\Column(type: 'boolean')] #[Groups(['task:read', 'task:write'])] private bool $archived = false; ``` - [ ] **Step 3: Add getter and setter** Add after `removeTag()` (line 233): ```php public function isArchived(): bool { return $this->archived; } public function setArchived(bool $archived): static { $this->archived = $archived; return $this; } ``` - [ ] **Step 4: Commit** ```bash git add src/Entity/Task.php git commit -m "feat(backend) : add archived field to Task entity" ``` ### Task 3: Add `archived` to TaskGroup entity **Files:** - Modify: `src/Entity/TaskGroup.php:7-8,31,56` - [ ] **Step 1: Add BooleanFilter import and filter attribute** Add import at top of file: ```php use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; ``` Add a second `#[ApiFilter]` line after the existing SearchFilter (line 31): ```php #[ApiFilter(BooleanFilter::class, properties: ['archived'])] ``` - [ ] **Step 2: Add `archived` property with ORM mapping and serialization groups** Add after `$project` property (line 56): ```php #[ORM\Column(type: 'boolean')] #[Groups(['task_group:read', 'task_group:write', 'task:read'])] private bool $archived = false; ``` - [ ] **Step 3: Add getter and setter** Add after `setProject()` (line 108): ```php public function isArchived(): bool { return $this->archived; } public function setArchived(bool $archived): static { $this->archived = $archived; return $this; } ``` - [ ] **Step 4: Commit** ```bash git add src/Entity/TaskGroup.php git commit -m "feat(backend) : add archived field to TaskGroup entity" ``` ### Task 4: Generate and run migration **Files:** - Create: `migrations/VersionXXXXXXXXXXXXXX.php` (auto-generated) - [ ] **Step 1: Generate migration** ```bash make shell # Inside container: php bin/console doctrine:migrations:diff exit ``` - [ ] **Step 2: Run migration** ```bash make migration-migrate ``` - [ ] **Step 3: Commit** ```bash git add migrations/ git commit -m "feat(backend) : add migration for isFinal, archived fields" ``` ### Task 5: Update fixtures — set `isFinal` on "Terminé" **Files:** - Modify: `src/DataFixtures/AppFixtures.php:110-115` - [ ] **Step 1: Add `setIsFinal(true)` on "Terminé" status** In the fixture loop (line 109-116), add after the `$status` creation block, right before `$manager->persist($status)`: ```php $statusObjects = []; foreach ($defaultStatuses as [$label, $color, $position]) { $status = new TaskStatus(); $status->setLabel($label); $status->setColor($color); $status->setPosition($position); if ($label === 'Terminé') { $status->setIsFinal(true); } $manager->persist($status); $statusObjects[$label] = $status; } ``` - [ ] **Step 2: Reload fixtures to verify** ```bash make fixtures ``` - [ ] **Step 3: Commit** ```bash git add src/DataFixtures/AppFixtures.php git commit -m "feat(backend) : set isFinal on Terminé status in fixtures" ``` --- ## Chunk 2: Frontend — DTOs, services, and i18n ### Task 6: Update DTOs **Files:** - Modify: `frontend/services/dto/task-status.ts` - Modify: `frontend/services/dto/task.ts` - Modify: `frontend/services/dto/task-group.ts` - [ ] **Step 1: Add `isFinal` to TaskStatus DTO** In `frontend/services/dto/task-status.ts`, add `isFinal` to both types: ```typescript export type TaskStatus = { id: number '@id'?: string label: string color: string position: number isFinal: boolean } export type TaskStatusWrite = { label: string color: string position: number isFinal: boolean } ``` - [ ] **Step 2: Add `archived` to Task DTO** In `frontend/services/dto/task.ts`, add `archived` to both types: ```typescript // In Task type, add after tags: archived: boolean // In TaskWrite type, add after tags: archived?: boolean ``` - [ ] **Step 3: Add `archived` to TaskGroup DTO** In `frontend/services/dto/task-group.ts`, add `archived` to both types: ```typescript // In TaskGroup type, add after project: archived: boolean // In TaskGroupWrite type, add after project: archived?: boolean ``` - [ ] **Step 4: Commit** ```bash git add frontend/services/dto/task-status.ts frontend/services/dto/task.ts frontend/services/dto/task-group.ts git commit -m "feat(frontend) : add isFinal and archived fields to DTOs" ``` ### Task 7: Update task service — add `getByProjectArchived` **Files:** - Modify: `frontend/services/tasks.ts` - [ ] **Step 1: Add method to fetch archived tasks** Add after `getByProject` (line 18): ```typescript async function getByProjectArchived(projectId: number): Promise { const data = await api.get>('/tasks', { project: `/api/projects/${projectId}`, archived: true, }) return extractHydraMembers(data) } ``` - [ ] **Step 2: Update `getByProject` to filter non-archived only** Update the existing `getByProject` to explicitly pass `archived: false`: ```typescript async function getByProject(projectId: number): Promise { const data = await api.get>('/tasks', { project: `/api/projects/${projectId}`, archived: false, }) return extractHydraMembers(data) } ``` - [ ] **Step 3: Export the new method** Update the return statement (line 38): ```typescript return { getAll, getByProject, getByProjectArchived, create, update, remove } ``` - [ ] **Step 4: Commit** ```bash git add frontend/services/tasks.ts git commit -m "feat(frontend) : add getByProjectArchived to task service" ``` ### Task 8: Update i18n translations **Files:** - Modify: `frontend/i18n/locales/fr.json` - [ ] **Step 1: Add archiving translation keys** Add the following keys to `fr.json`: ```json "tasks": { "created": "Ticket créé avec succès.", "updated": "Ticket mis à jour avec succès.", "deleted": "Ticket supprimé avec succès.", "archived": "Ticket archivé avec succès.", "unarchived": "Ticket désarchivé avec succès.", "deleteConfirmTitle": "Supprimer le ticket", "deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer ce ticket ? Cette action est irréversible." }, "taskGroups": { "created": "Groupe créé avec succès.", "updated": "Groupe mis à jour avec succès.", "deleted": "Groupe supprimé avec succès.", "archived": "Groupe archivé avec succès.", "unarchived": "Groupe désarchivé avec succès." } ``` Also add: ```json "archive": { "title": "Archives", "empty": "Aucun ticket archivé.", "archiveButton": "Archiver", "unarchiveButton": "Désarchiver", "showArchived": "Voir les groupes archivés", "hideArchived": "Masquer les groupes archivés", "statusFinal": "Statut final", "groupArchiveDisabled": "Tous les tickets doivent être en statut final pour archiver le groupe." } ``` - [ ] **Step 2: Commit** ```bash git add frontend/i18n/locales/fr.json git commit -m "feat(frontend) : add archiving i18n translations" ``` --- ## Chunk 3: Frontend — TaskDrawer (archive button + delete confirmation modal) ### Task 9: Create ConfirmDeleteTaskModal component **Files:** - Create: `frontend/components/ui/ConfirmDeleteTaskModal.vue` - [ ] **Step 1: Create the modal component** Follow the pattern of `ConfirmDeleteStatusModal.vue`: ```vue ``` - [ ] **Step 2: Commit** ```bash git add frontend/components/ui/ConfirmDeleteTaskModal.vue git commit -m "feat(frontend) : create ConfirmDeleteTaskModal component" ``` ### Task 10: Update TaskDrawer — archive button + delete confirmation **Files:** - Modify: `frontend/components/task/TaskDrawer.vue` - [ ] **Step 1: Add archive/unarchive button to template** Replace the button area (lines 76-93) with: ```vue
``` - [ ] **Step 2: Add ConfirmDeleteTaskModal to template** Add right before the closing `` tag: ```vue ``` - [ ] **Step 3: Add computed properties and handlers in script** Add after `const isSubmitting = ref(false)` (line 131): ```typescript const confirmDeleteOpen = ref(false) ``` Add computed properties after `groupOptions` (line 166): ```typescript const canArchive = computed(() => { if (!isEditing.value || !props.task) return false if (props.task.archived) return false const status = props.statuses.find(s => s.id === props.task?.status?.id) return !!status?.isFinal }) const canUnarchive = computed(() => { return isEditing.value && !!props.task?.archived }) ``` Add archive/unarchive handlers after `handleDelete` (line 224): ```typescript async function handleArchive() { if (!props.task) return const timerStore = useTimerStore() if (timerStore.activeEntry?.task && String(timerStore.activeEntry.task) === `/api/tasks/${props.task.id}`) { await timerStore.stop() } isSubmitting.value = true try { await update(props.task.id, { archived: true }) emit('saved') isOpen.value = false } finally { isSubmitting.value = false } } async function handleUnarchive() { if (!props.task) return isSubmitting.value = true try { await update(props.task.id, { archived: false }) emit('saved') isOpen.value = false } finally { isSubmitting.value = false } } ``` - [ ] **Step 4: Commit** ```bash git add frontend/components/task/TaskDrawer.vue git commit -m "feat(frontend) : add archive/unarchive buttons and delete confirmation to TaskDrawer" ``` --- ## Chunk 4: Frontend — Kanban filtering & Archives page ### Task 11: Filter archived tasks and groups from kanban **Files:** - Modify: `frontend/pages/projects/[id]/index.vue` - [ ] **Step 1: Filter archived tasks from display** Update `filteredTasks` computed (line 187-190) to also exclude archived tasks: ```typescript const filteredTasks = computed(() => { let result = tasks.value.filter(t => !t.archived) if (selectedGroupId.value) { result = result.filter(t => t.group?.id === selectedGroupId.value) } return result }) ``` - [ ] **Step 2: Filter archived groups from group filter dropdown** Update `groupFilterOptions` computed (line 183-185): ```typescript const groupFilterOptions = computed(() => groups.value.filter(g => !g.archived).map(g => ({ label: g.title, value: g.id })) ) ``` - [ ] **Step 3: Commit** ```bash git add frontend/pages/projects/[id]/index.vue git commit -m "feat(frontend) : filter archived tasks and groups from kanban view" ``` ### Task 12: Create archives page **Files:** - Create: `frontend/pages/projects/[id]/archives.vue` - [ ] **Step 1: Create the archives page** ```vue ``` - [ ] **Step 2: Commit** ```bash git add frontend/pages/projects/[id]/archives.vue git commit -m "feat(frontend) : create project archives page" ``` ### Task 13: Add Archives link to sidebar **Files:** - Modify: `frontend/layouts/default.vue:44-52` - [ ] **Step 1: Add sidebar link for archives** Add after the "Groupes" SidebarLink (line 51): ```vue ``` - [ ] **Step 2: Commit** ```bash git add frontend/layouts/default.vue git commit -m "feat(frontend) : add Archives sidebar link for projects" ``` --- ## Chunk 5: Frontend — DataTable actions slot, Group archiving & Admin isFinal toggle ### Task 14: Add `actions` slot to DataTable component **Files:** - Modify: `frontend/components/ui/DataTable.vue` - [ ] **Step 1: Add actions slot next to delete button** In the template, update the actions `` header (line 13-15) to show when either `deletable` or the `actions` slot is used: ```vue Actions ``` Update the actions `` cell (line 35-42) to include both the slot and delete button: ```vue
``` Update the empty row colspan (line 46): ```vue ``` - [ ] **Step 2: Commit** ```bash git add frontend/components/ui/DataTable.vue git commit -m "feat(frontend) : add actions slot to DataTable component" ``` ### Task 15: Update ProjectGroupTab — archive/unarchive groups **Files:** - Modify: `frontend/components/project/ProjectGroupTab.vue` - [ ] **Step 1: Add task loading and archive toggle to script** Replace the script section with: ```vue ``` - [ ] **Step 2: Update template with archive toggle and buttons** Replace the full template with: ```vue ``` - [ ] **Step 3: Commit** ```bash git add frontend/components/project/ProjectGroupTab.vue git commit -m "feat(frontend) : add group archive/unarchive to ProjectGroupTab" ``` ### Task 16: Add `isFinal` toggle to TaskStatusDrawer **Files:** - Modify: `frontend/components/task/TaskStatusDrawer.vue` - [ ] **Step 1: Add checkbox to template** Add after the ColorPicker div (line 19), before the submit button div: ```vue
``` - [ ] **Step 2: Update form reactive and populate logic** Add `isFinal` to the form reactive (line 56-60): ```typescript const form = reactive({ label: '', position: '0', color: '#222783', isFinal: false, }) ``` Update the watcher populate (line 66-79) to include `isFinal`: ```typescript watch(() => props.modelValue, (open) => { if (open) { if (props.item) { form.label = props.item.label ?? '' form.position = String(props.item.position ?? 0) form.color = props.item.color ?? '#222783' form.isFinal = props.item.isFinal ?? false } else { form.label = '' form.position = '0' form.color = '#222783' form.isFinal = false } touched.label = false } }) ``` Update the payload (line 89-93): ```typescript const payload: TaskStatusWrite = { label: form.label.trim(), position: Number(form.position), color: form.color, isFinal: form.isFinal, } ``` - [ ] **Step 3: Commit** ```bash git add frontend/components/task/TaskStatusDrawer.vue git commit -m "feat(frontend) : add isFinal toggle to TaskStatusDrawer" ``` ### Task 17: Verify everything works end-to-end - [ ] **Step 1: Run the dev server** ```bash make dev-nuxt ``` - [ ] **Step 2: Manual verification checklist** 1. Create/edit a status in admin → verify `isFinal` checkbox works 2. Set a task to "Terminé" status → verify "Archiver" button appears in TaskDrawer 3. Archive a task → verify it disappears from kanban 4. Go to Archives page → verify the archived task appears 5. Unarchive the task → verify it reappears in kanban 6. Delete button → verify confirmation modal appears 7. In Groups page → verify archive button shows when all group tasks are final 8. Archive a group → verify group and tasks disappear from kanban 9. Toggle "Voir les groupes archivés" → verify archived groups appear with unarchive button - [ ] **Step 3: Final commit if any fixes needed**