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
isFinalproperty 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
archivedproperty 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
archivedproperty 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
isFinalto 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
archivedto 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
archivedto 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
getByProjectto 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"
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):
<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
- Create/edit a status in admin → verify
isFinalcheckbox works - Set a task to "Terminé" status → verify "Archiver" button appears in TaskDrawer
- Archive a task → verify it disappears from kanban
- Go to Archives page → verify the archived task appears
- Unarchive the task → verify it reappears in kanban
- Delete button → verify confirmation modal appears
- In Groups page → verify archive button shows when all group tasks are final
- Archive a group → verify group and tasks disappear from kanban
- Toggle "Voir les groupes archivés" → verify archived groups appear with unarchive button
- Step 3: Final commit if any fixes needed