Files
Lesstime/docs/superpowers/plans/2026-03-12-task-archiving.md
Matthieu 73d0c7b4fa docs : add task archiving implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:43:21 +01:00

32 KiB

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):

#[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):

public function isFinal(): bool
{
    return $this->isFinal;
}

public function setIsFinal(bool $isFinal): static
{
    $this->isFinal = $isFinal;

    return $this;
}
  • Step 3: Commit
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:

use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;

Add a second #[ApiFilter] line after the existing SearchFilter (line 34):

#[ApiFilter(BooleanFilter::class, properties: ['archived'])]
  • Step 2: Add archived property with ORM mapping and serialization groups

Add after the $tags property (line 94):

#[ORM\Column(type: 'boolean')]
#[Groups(['task:read', 'task:write'])]
private bool $archived = false;
  • Step 3: Add getter and setter

Add after removeTag() (line 233):

public function isArchived(): bool
{
    return $this->archived;
}

public function setArchived(bool $archived): static
{
    $this->archived = $archived;

    return $this;
}
  • Step 4: Commit
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:

use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;

Add a second #[ApiFilter] line after the existing SearchFilter (line 31):

#[ApiFilter(BooleanFilter::class, properties: ['archived'])]
  • Step 2: Add archived property with ORM mapping and serialization groups

Add after $project property (line 56):

#[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):

public function isArchived(): bool
{
    return $this->archived;
}

public function setArchived(bool $archived): static
{
    $this->archived = $archived;

    return $this;
}
  • Step 4: Commit
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

make shell
# Inside container:
php bin/console doctrine:migrations:diff
exit
  • Step 2: Run migration
make migration-migrate
  • Step 3: Commit
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):

$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
make fixtures
  • Step 3: Commit
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:

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:

// 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:

// In TaskGroup type, add after project:
    archived: boolean

// In TaskGroupWrite type, add after project:
    archived?: boolean
  • Step 4: Commit
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):

async function getByProjectArchived(projectId: number): Promise<Task[]> {
    const data = await api.get<HydraCollection<Task>>('/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:

async function getByProject(projectId: number): Promise<Task[]> {
    const data = await api.get<HydraCollection<Task>>('/tasks', {
        project: `/api/projects/${projectId}`,
        archived: false,
    })
    return extractHydraMembers(data)
}
  • Step 3: Export the new method

Update the return statement (line 38):

return { getAll, getByProject, getByProjectArchived, create, update, remove }
  • Step 4: Commit
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:

"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:

"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
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:

<template>
    <Teleport v-if="modelValue" to="body">
        <Transition name="modal" appear>
            <div class="fixed inset-0 z-50 flex items-center justify-center">
                <div class="absolute inset-0 bg-black/30" @click="cancel" />
                <div class="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
                    <h3 class="text-lg font-bold text-neutral-900">{{ $t('tasks.deleteConfirmTitle') }}</h3>
                    <p class="mt-3 text-sm text-neutral-600">
                        {{ $t('tasks.deleteConfirmMessage') }}
                    </p>
                    <div class="mt-6 flex justify-end gap-3">
                        <button
                            type="button"
                            class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
                            @click="cancel"
                        >
                            Annuler
                        </button>
                        <button
                            type="button"
                            class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700"
                            @click="$emit('confirm')"
                        >
                            Supprimer
                        </button>
                    </div>
                </div>
            </div>
        </Transition>
    </Teleport>
</template>

<script setup lang="ts">
defineProps<{
    modelValue: boolean
}>()

const emit = defineEmits<{
    (e: 'update:modelValue', value: boolean): void
    (e: 'confirm'): void
}>()

function cancel() {
    emit('update:modelValue', false)
}
</script>

<style scoped>
.modal-enter-active,
.modal-leave-active {
    transition: opacity 0.2s ease;
}

.modal-enter-from,
.modal-leave-to {
    opacity: 0;
}
</style>
  • Step 2: Commit
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:

<div class="mt-6 flex flex-col gap-3">
    <div class="flex items-center" :class="isEditing ? 'justify-between' : 'justify-end'">
        <button
            v-if="isEditing"
            type="button"
            class="rounded-md bg-red-500 px-4 py-2 text-sm font-semibold text-white hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50"
            :disabled="isSubmitting"
            @click="confirmDeleteOpen = true"
        >
            Supprimer
        </button>
        <div class="flex gap-2">
            <button
                v-if="canArchive"
                type="button"
                class="rounded-md bg-neutral-500 px-4 py-2 text-sm font-semibold text-white hover:bg-neutral-600 disabled:cursor-not-allowed disabled:opacity-50"
                :disabled="isSubmitting"
                @click="handleArchive"
            >
                {{ $t('archive.archiveButton') }}
            </button>
            <button
                v-if="canUnarchive"
                type="button"
                class="rounded-md bg-neutral-500 px-4 py-2 text-sm font-semibold text-white hover:bg-neutral-600 disabled:cursor-not-allowed disabled:opacity-50"
                :disabled="isSubmitting"
                @click="handleUnarchive"
            >
                {{ $t('archive.unarchiveButton') }}
            </button>
            <button
                type="submit"
                class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
                :disabled="isSubmitting"
            >
                Enregistrer
            </button>
        </div>
    </div>
</div>
  • Step 2: Add ConfirmDeleteTaskModal to template

Add right before the closing </AppDrawer> tag:

<ConfirmDeleteTaskModal
    v-model="confirmDeleteOpen"
    @confirm="handleDelete"
/>
  • Step 3: Add computed properties and handlers in script

Add after const isSubmitting = ref(false) (line 131):

const confirmDeleteOpen = ref(false)

Add computed properties after groupOptions (line 166):

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):

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
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:

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):

const groupFilterOptions = computed(() =>
    groups.value.filter(g => !g.archived).map(g => ({ label: g.title, value: g.id }))
)
  • Step 3: Commit
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

<template>
    <div>
        <div class="flex items-center justify-between">
            <h1 class="text-2xl font-bold text-primary-500">{{ project?.name ?? '' }}  {{ $t('archive.title') }}</h1>
        </div>

        <div class="mt-4">
            <MalioSelect
                v-model="selectedGroupId"
                :options="groupFilterOptions"
                label="Groupe"
                empty-option-label="Tous les groupes"
                min-width="w-64"
            />
        </div>

        <div class="mt-6">
            <p v-if="filteredTasks.length === 0" class="text-sm text-neutral-400">
                {{ $t('archive.empty') }}
            </p>
            <div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2">
                <div
                    v-for="task in filteredTasks"
                    :key="task.id"
                    class="flex cursor-pointer items-center justify-between rounded-lg border border-neutral-200 bg-white px-4 py-3 hover:shadow-sm"
                    @click="openTaskEdit(task)"
                >
                    <div class="flex items-center gap-3">
                        <span class="text-xs font-bold text-neutral-400">{{ project?.code }}-{{ task.number }}</span>
                        <span class="text-sm font-semibold text-neutral-900">{{ task.title }}</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <span
                            v-if="task.status"
                            class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
                            :style="{ backgroundColor: task.status.color }"
                        >
                            {{ task.status.label }}
                        </span>
                        <span
                            v-if="task.group"
                            class="rounded-full border px-2 py-0.5 text-xs font-semibold"
                            :style="{ borderColor: task.group.color, color: task.group.color }"
                        >
                            {{ task.group.title }}
                        </span>
                        <span
                            v-if="task.assignee"
                            class="flex h-5 w-5 items-center justify-center rounded-full bg-primary-500 text-[10px] font-bold text-white"
                            :title="task.assignee.username"
                        >
                            {{ task.assignee.username.substring(0, 2).toUpperCase() }}
                        </span>
                    </div>
                </div>
            </div>
        </div>

        <TaskDrawer
            v-model="taskDrawerOpen"
            :task="selectedTask"
            :project-id="projectId"
            :statuses="statuses"
            :efforts="efforts"
            :priorities="priorities"
            :tags="tags"
            :groups="groups"
            :users="users"
            @saved="onSaved"
        />
    </div>
</template>

<script setup lang="ts">
import type { Project } from '~/services/dto/project'
import type { Task } from '~/services/dto/task'
import type { TaskStatus } from '~/services/dto/task-status'
import type { TaskEffort } from '~/services/dto/task-effort'
import type { TaskPriority } from '~/services/dto/task-priority'
import type { TaskTag } from '~/services/dto/task-tag'
import type { TaskGroup } from '~/services/dto/task-group'
import type { UserData } from '~/services/dto/user-data'
import { useProjectService } from '~/services/projects'
import { useTaskService } from '~/services/tasks'
import { useTaskStatusService } from '~/services/task-statuses'
import { useTaskEffortService } from '~/services/task-efforts'
import { useTaskPriorityService } from '~/services/task-priorities'
import { useTaskTagService } from '~/services/task-tags'
import { useTaskGroupService } from '~/services/task-groups'
import { useUserService } from '~/services/users'

const route = useRoute()
const projectId = computed(() => Number(route.params.id))

useHead({ title: 'Archives' })

const projectService = useProjectService()
const taskService = useTaskService()
const statusService = useTaskStatusService()
const effortService = useTaskEffortService()
const priorityService = useTaskPriorityService()
const tagService = useTaskTagService()
const groupService = useTaskGroupService()
const userService = useUserService()

const project = ref<Project | null>(null)
const archivedTasks = ref<Task[]>([])
const statuses = ref<TaskStatus[]>([])
const efforts = ref<TaskEffort[]>([])
const priorities = ref<TaskPriority[]>([])
const tags = ref<TaskTag[]>([])
const groups = ref<TaskGroup[]>([])
const users = ref<UserData[]>([])

const selectedGroupId = ref<number | null>(null)
const taskDrawerOpen = ref(false)
const selectedTask = ref<Task | null>(null)

const groupFilterOptions = computed(() =>
    groups.value.map(g => ({ label: g.title, value: g.id }))
)

const filteredTasks = computed(() => {
    if (!selectedGroupId.value) return archivedTasks.value
    return archivedTasks.value.filter(t => t.group?.id === selectedGroupId.value)
})

async function loadData() {
    const [p, t, s, e, pr, ty, g, u] = await Promise.all([
        projectService.getById(projectId.value),
        taskService.getByProjectArchived(projectId.value),
        statusService.getAll(),
        effortService.getAll(),
        priorityService.getAll(),
        tagService.getAll(),
        groupService.getByProject(projectId.value),
        userService.getAll(),
    ])
    project.value = p
    archivedTasks.value = t
    statuses.value = s
    efforts.value = e
    priorities.value = pr
    tags.value = ty
    groups.value = g
    users.value = u
}

function openTaskEdit(task: Task) {
    selectedTask.value = task
    taskDrawerOpen.value = true
}

async function onSaved() {
    await loadData()
}

onMounted(() => {
    loadData()
})
</script>
  • Step 2: Commit
git add frontend/pages/projects/[id]/archives.vue
git commit -m "feat(frontend) : create project archives page"

Files:

  • Modify: frontend/layouts/default.vue:44-52

  • Step 1: Add sidebar link for archives

Add after the "Groupes" SidebarLink (line 51):

<SidebarLink
    :to="`/projects/${currentProjectId}/archives`"
    icon="mdi:archive-outline"
    label="Archives"
    :collapsed="ui.sidebarCollapsed"
    sub
/>
  • Step 2: Commit
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 <th> header (line 13-15) to show when either deletable or the actions slot is used:

<th v-if="deletable || $slots.actions" class="px-4 py-3 font-semibold text-neutral-700">
    Actions
</th>

Update the actions <td> cell (line 35-42) to include both the slot and delete button:

<td v-if="deletable || $slots.actions" class="px-4 py-3">
    <div class="flex items-center gap-2">
        <slot name="actions" :item="item" />
        <button
            v-if="deletable"
            class="text-[red-500] hover:text-[red-700]"
            @click.stop="$emit('delete', item)"
        >
            <Icon name="mdi:delete-outline" size="20" />
        </button>
    </div>
</td>

Update the empty row colspan (line 46):

<td
    :colspan="columns.length + (deletable || $slots.actions ? 1 : 0)"
    class="px-4 py-8 text-center text-neutral-400"
>
  • Step 2: Commit
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:

<script setup lang="ts">
import type { TaskGroup } from '~/services/dto/task-group'
import type { Task } from '~/services/dto/task'
import { useTaskGroupService } from '~/services/task-groups'
import { useTaskService } from '~/services/tasks'

const props = defineProps<{
    projectId: number
}>()

const emit = defineEmits<{
    (e: 'updated'): void
}>()

import type { DataTableColumn } from '~/components/ui/DataTable.vue'

const columns: DataTableColumn[] = [
    { key: 'title', label: 'Titre', primary: true },
    { key: 'color', label: 'Couleur' },
    { key: 'description', label: 'Description', class: 'max-w-xs truncate text-neutral-700' },
]

const groupService = useTaskGroupService()
const taskService = useTaskService()

const allGroups = ref<TaskGroup[]>([])
const activeTasks = ref<Task[]>([])
const archivedTasks = ref<Task[]>([])
const isLoading = ref(true)
const drawerOpen = ref(false)
const selectedItem = ref<TaskGroup | null>(null)
const showArchived = ref(false)

const items = computed(() =>
    allGroups.value.filter(g => showArchived.value ? g.archived : !g.archived)
)

function canArchiveGroup(group: TaskGroup): boolean {
    const groupTasks = activeTasks.value.filter(t => t.group?.id === group.id)
    if (groupTasks.length === 0) return false
    return groupTasks.every(t => t.status?.isFinal === true)
}

async function loadItems() {
    isLoading.value = true
    try {
        const [g, t, at] = await Promise.all([
            groupService.getByProject(props.projectId),
            taskService.getByProject(props.projectId),
            taskService.getByProjectArchived(props.projectId),
        ])
        allGroups.value = g
        activeTasks.value = t
        archivedTasks.value = at
    } finally {
        isLoading.value = false
    }
}

function openCreate() {
    selectedItem.value = null
    drawerOpen.value = true
}

function openEdit(item: TaskGroup) {
    selectedItem.value = item
    drawerOpen.value = true
}

async function handleDelete(id: number) {
    await groupService.remove(id)
    await loadItems()
    emit('updated')
}

async function handleArchive(group: TaskGroup) {
    const groupTasks = activeTasks.value.filter(t => t.group?.id === group.id)
    await Promise.all(groupTasks.map(t => taskService.update(t.id, { archived: true })))
    await groupService.update(group.id, { archived: true })
    await loadItems()
    emit('updated')
}

async function handleUnarchive(group: TaskGroup) {
    const groupTasks = archivedTasks.value.filter(t => t.group?.id === group.id)
    await Promise.all(groupTasks.map(t => taskService.update(t.id, { archived: false })))
    await groupService.update(group.id, { archived: false })
    await loadItems()
    emit('updated')
}

async function onSaved() {
    await loadItems()
    emit('updated')
}

onMounted(() => {
    loadItems()
})
</script>
  • Step 2: Update template with archive toggle and buttons

Replace the full template with:

<template>
    <div>
        <div class="flex items-center justify-between">
            <h2 class="text-lg font-bold text-neutral-900">Groupes</h2>
            <div class="flex items-center gap-3">
                <button
                    type="button"
                    class="text-sm font-medium text-neutral-500 hover:text-neutral-700"
                    @click="showArchived = !showArchived"
                >
                    {{ showArchived ? $t('archive.hideArchived') : $t('archive.showArchived') }}
                </button>
                <button
                    v-if="!showArchived"
                    class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
                    @click="openCreate"
                >
                    + Ajouter un groupe
                </button>
            </div>
        </div>

        <DataTable
            :columns="columns"
            :items="items"
            :loading="isLoading"
            empty-message="Aucun groupe trouvé."
            :deletable="!showArchived"
            @row-click="openEdit"
            @delete="(item) => handleDelete(item.id)"
        >
            <template #cell-color="{ item }">
                <span
                    class="inline-block h-6 w-6 rounded-full"
                    :style="{ backgroundColor: item.color }"
                />
            </template>
            <template #cell-description="{ item }">
                {{ item.description ?? '—' }}
            </template>
            <template #actions="{ item }">
                <button
                    v-if="!showArchived && canArchiveGroup(item)"
                    type="button"
                    class="rounded-md bg-neutral-500 px-3 py-1 text-xs font-semibold text-white hover:bg-neutral-600"
                    @click.stop="handleArchive(item)"
                >
                    {{ $t('archive.archiveButton') }}
                </button>
                <button
                    v-if="showArchived"
                    type="button"
                    class="rounded-md bg-neutral-500 px-3 py-1 text-xs font-semibold text-white hover:bg-neutral-600"
                    @click.stop="handleUnarchive(item)"
                >
                    {{ $t('archive.unarchiveButton') }}
                </button>
            </template>
        </DataTable>

        <TaskGroupDrawer
            v-model="drawerOpen"
            :group="selectedItem"
            :project-id="projectId"
            @saved="onSaved"
        />
    </div>
</template>
  • Step 3: Commit
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:

<div class="mt-4 flex items-center gap-2">
    <input
        id="isFinal"
        v-model="form.isFinal"
        type="checkbox"
        class="h-4 w-4 rounded border-neutral-300 text-primary-500 focus:ring-primary-500"
    />
    <label for="isFinal" class="text-sm font-medium text-neutral-700">
        {{ $t('archive.statusFinal') }}
    </label>
</div>
  • Step 2: Update form reactive and populate logic

Add isFinal to the form reactive (line 56-60):

const form = reactive({
    label: '',
    position: '0',
    color: '#222783',
    isFinal: false,
})

Update the watcher populate (line 66-79) to include isFinal:

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):

const payload: TaskStatusWrite = {
    label: form.label.trim(),
    position: Number(form.position),
    color: form.color,
    isFinal: form.isFinal,
}
  • Step 3: Commit
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
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