diff --git a/docs/superpowers/plans/2026-03-12-task-archiving.md b/docs/superpowers/plans/2026-03-12-task-archiving.md new file mode 100644 index 0000000..9c16e68 --- /dev/null +++ b/docs/superpowers/plans/2026-03-12-task-archiving.md @@ -0,0 +1,1180 @@ +# 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**