# Admin Clients + Global Statuses 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:** Move clients management into the admin page as a tab, and make task statuses global (shared across all projects) instead of per-project.

**Architecture:** Two independent changes: (1) Extract client CRUD from its dedicated page into an `AdminClientTab` component inside the admin page, remove the standalone `/clients` page and sidebar link. (2) Remove the `project` relationship from `TaskStatus` entity, update the frontend to use `getAll()` everywhere instead of `getByProject()`, remove per-project status management pages/links, and update `AdminStatusTab` + `TaskStatusDrawer` to work without `projectId`.

**Tech Stack:** PHP 8.4 / Symfony 8 / Doctrine ORM (backend), Nuxt 4 / Vue 3 / TypeScript (frontend)

---

## Chunk 1: Move Clients into Admin

### Task 1: Create AdminClientTab component

**Files:**
- Create: `frontend/components/admin/AdminClientTab.vue`

- [ ] **Step 1: Create AdminClientTab.vue**

Extract the logic from `frontend/pages/clients.vue` into a new admin tab component, following the same pattern as `AdminPriorityTab.vue` (h2 title instead of h1, no `useHead`).

```vue
<template>
    <div>
        <div class="flex items-center justify-between">
            <h2 class="text-lg font-bold text-neutral-900">Clients</h2>
            <button
                class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
                @click="openCreate"
            >
                + Ajouter un client
            </button>
        </div>

        <DataTable
            :columns="columns"
            :items="clients"
            :loading="isLoading"
            empty-message="Aucun client trouvé."
            deletable
            @row-click="openEdit"
            @delete="(item) => handleDelete(item.id)"
        >
            <template #cell-email="{ item }">
                {{ item.email ?? '-' }}
            </template>
            <template #cell-address="{ item }">
                {{ formatAddress(item) }}
            </template>
            <template #cell-phone="{ item }">
                {{ item.phone ?? '-' }}
            </template>
        </DataTable>

        <ClientDrawer
            v-model="drawerOpen"
            :client="selectedClient"
            @saved="onSaved"
        />
    </div>
</template>

<script setup lang="ts">
import type { Client } from '~/services/dto/client'
import { useClientService } from '~/services/clients'

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

const columns: DataTableColumn[] = [
    { key: 'name', label: 'Nom', primary: true },
    { key: 'email', label: 'Email', class: 'text-primary-500' },
    { key: 'address', label: 'Adresse', class: 'text-neutral-700' },
    { key: 'phone', label: 'Téléphone', class: 'text-primary-500' },
]

const { getAll, remove } = useClientService()
const clients = ref<Client[]>([])
const isLoading = ref(true)
const drawerOpen = ref(false)
const selectedClient = ref<Client | null>(null)

async function loadClients() {
    isLoading.value = true
    try {
        clients.value = await getAll()
    } finally {
        isLoading.value = false
    }
}

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

function openEdit(client: Client) {
    selectedClient.value = client
    drawerOpen.value = true
}

function formatAddress(client: Client): string {
    return [client.street, client.postalCode, client.city]
        .filter(Boolean)
        .join(', ') || '-'
}

async function handleDelete(id: number) {
    await remove(id)
    await loadClients()
}

async function onSaved() {
    await loadClients()
}

onMounted(() => {
    loadClients()
})
</script>
```

- [ ] **Step 2: Commit**

```bash
git add frontend/components/admin/AdminClientTab.vue
git commit -m "feat(admin) : add AdminClientTab component"
```

### Task 2: Add Clients tab to admin page and remove standalone page

**Files:**
- Modify: `frontend/pages/admin.vue`
- Delete: `frontend/pages/clients.vue`

- [ ] **Step 3: Update admin.vue to include Clients tab**

Add `AdminClientTab` to the admin page. Add the tab entry to the `tabs` array and the corresponding `v-if` block:

In `frontend/pages/admin.vue`, update the `tabs` array:

```typescript
const tabs = [
    { key: 'clients', label: 'Clients' },
    { key: 'efforts', label: 'Efforts' },
    { key: 'priorities', label: 'Priorités' },
    { key: 'types', label: 'Types' },
    { key: 'users', label: 'Utilisateurs' },
] as const
```

Change the default active tab:

```typescript
const activeTab = ref<TabKey>('clients')
```

Add the component in the template `<div class="mt-6">` block:

```html
<AdminClientTab v-if="activeTab === 'clients'" />
```

- [ ] **Step 4: Delete standalone clients page**

Delete `frontend/pages/clients.vue`.

- [ ] **Step 5: Commit**

```bash
git add frontend/pages/admin.vue
git rm frontend/pages/clients.vue
git commit -m "feat(admin) : move clients into admin page, remove standalone page"
```

### Task 3: Remove clients sidebar link

**Files:**
- Modify: `frontend/layouts/default.vue`

- [ ] **Step 6: Remove the Clients SidebarLink from default.vue**

Remove the following block from `frontend/layouts/default.vue` (lines 60-65):

```html
                    <SidebarLink
                        to="/clients"
                        icon="mdi:account-group-outline"
                        label="Clients"
                        :collapsed="ui.sidebarCollapsed"
                    />
```

- [ ] **Step 7: Commit**

```bash
git add frontend/layouts/default.vue
git commit -m "refactor(frontend) : remove clients sidebar link"
```

---

## Chunk 2: Make Task Statuses Global

### Task 4: Remove project relationship from TaskStatus entity

**Files:**
- Modify: `src/Entity/TaskStatus.php`

- [ ] **Step 8: Update TaskStatus entity to remove project relationship**

In `src/Entity/TaskStatus.php`:

1. Remove the `SearchFilter` import and `#[ApiFilter]` attribute
2. Remove the `$project` property, its `#[ORM]` annotations, and the `getProject()`/`setProject()` methods

The entity should become:

```php
<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskStatusRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;

#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(security: "is_granted('ROLE_ADMIN')"),
        new Patch(security: "is_granted('ROLE_ADMIN')"),
        new Delete(security: "is_granted('ROLE_ADMIN')"),
    ],
    normalizationContext: ['groups' => ['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]
    #[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 9: Commit**

```bash
git add src/Entity/TaskStatus.php
git commit -m "refactor(backend) : remove project relationship from TaskStatus entity"
```

### Task 5: Generate and run Doctrine migration

- [ ] **Step 10: Generate the migration**

```bash
make shell
# Inside container:
php bin/console doctrine:migrations:diff
exit
```

This should generate a migration that:
- Drops the `project_id` foreign key from `task_status` table
- Drops the `project_id` column from `task_status` table

- [ ] **Step 11: Review the migration**

Read the generated migration file in `migrations/` to verify it only drops the FK and column.

- [ ] **Step 12: Reset database (since structure changed significantly)**

```bash
make db-reset
```

- [ ] **Step 13: Commit**

```bash
git add migrations/
git commit -m "feat(backend) : add migration to remove project_id from task_status"
```

### Task 6: Update fixtures for global statuses

**Files:**
- Modify: `src/DataFixtures/AppFixtures.php`

- [ ] **Step 14: Update fixtures to create global statuses instead of per-project**

In `src/DataFixtures/AppFixtures.php`, replace the entire per-project status block (lines 95-124, from `// Task Statuses (per project)` through `$statusDone = $sirhStatuses['Terminé'];`) with global creation:

```php
        // Task Statuses (global)
        $defaultStatuses = [
            ['A faire', '#222783', 0],
            ['En cours', '#4A90D9', 1],
            ['Bloqué', '#C62828', 2],
            ['En attente de validation', '#FF8F00', 3],
            ['Terminé', '#26A69A', 4],
        ];

        $statusObjects = [];
        foreach ($defaultStatuses as [$label, $color, $position]) {
            $status = new TaskStatus();
            $status->setLabel($label);
            $status->setColor($color);
            $status->setPosition($position);
            $manager->persist($status);
            $statusObjects[$label] = $status;
        }

        $statusTodo       = $statusObjects['A faire'];
        $statusInProgress = $statusObjects['En cours'];
        $statusBlocked    = $statusObjects['Bloqué'];
        $statusReview     = $statusObjects['En attente de validation'];
        $statusDone       = $statusObjects['Terminé'];
```

This replaces the loop that created statuses per-project AND the `$statusesByProject` / `$sirhStatuses` extraction lines (95-124). The task variable references (`$statusTodo`, etc.) remain identical so downstream task creation is unchanged.

- [ ] **Step 15: Reload fixtures to verify**

```bash
make db-reset
```

- [ ] **Step 16: Commit**

```bash
git add src/DataFixtures/AppFixtures.php
git commit -m "fix(fixtures) : create global statuses instead of per-project"
```

### Task 7: Update frontend DTO and service for global statuses

**Files:**
- Modify: `frontend/services/dto/task-status.ts`
- Modify: `frontend/services/task-statuses.ts`

- [ ] **Step 17: Update TaskStatus DTO to remove project field**

In `frontend/services/dto/task-status.ts`, remove the `project` import and field from both types:

```typescript
export type TaskStatus = {
    id: number
    '@id'?: string
    label: string
    color: string
    position: number
}

export type TaskStatusWrite = {
    label: string
    color: string
    position: number
}
```

- [ ] **Step 18: Remove getByProject from task-statuses service**

In `frontend/services/task-statuses.ts`, remove the `getByProject` function and its return:

```typescript
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<TaskStatus[]> {
        const data = await api.get<HydraCollection<TaskStatus>>('/task_statuses')
        return extractHydraMembers(data)
    }

    async function create(payload: TaskStatusWrite): Promise<TaskStatus> {
        return api.post<TaskStatus>('/task_statuses', payload as Record<string, unknown>, {
            toastSuccessKey: 'taskStatuses.created',
        })
    }

    async function update(id: number, payload: Partial<TaskStatusWrite>): Promise<TaskStatus> {
        return api.patch<TaskStatus>(`/task_statuses/${id}`, payload as Record<string, unknown>, {
            toastSuccessKey: 'taskStatuses.updated',
        })
    }

    async function remove(id: number): Promise<void> {
        await api.delete(`/task_statuses/${id}`, {}, {
            toastSuccessKey: 'taskStatuses.deleted',
        })
    }

    return { getAll, create, update, remove }
}
```

- [ ] **Step 19: Commit**

```bash
git add frontend/services/dto/task-status.ts frontend/services/task-statuses.ts
git commit -m "refactor(frontend) : remove project from TaskStatus DTO and service"
```

### Task 8: Update TaskStatusDrawer to remove projectId

**Files:**
- Modify: `frontend/components/task/TaskStatusDrawer.vue`

- [ ] **Step 20: Remove projectId prop from TaskStatusDrawer**

In `frontend/components/task/TaskStatusDrawer.vue`:

1. Remove `projectId` from props:

```typescript
const props = defineProps<{
    modelValue: boolean
    item: TaskStatus | null
}>()
```

2. Remove the `project` field from the payload in `handleSubmit`:

```typescript
        const payload: TaskStatusWrite = {
            label: form.label.trim(),
            position: Number(form.position),
            color: form.color,
        }
```

- [ ] **Step 21: Commit**

```bash
git add frontend/components/task/TaskStatusDrawer.vue
git commit -m "refactor(frontend) : remove projectId from TaskStatusDrawer"
```

### Task 9: Add getAll to task service and update AdminStatusTab

**Files:**
- Modify: `frontend/services/tasks.ts`
- Modify: `frontend/components/admin/AdminStatusTab.vue`

- [ ] **Step 22: Add getAll() method to task service**

`frontend/services/tasks.ts` currently only has `getByProject()`. Add a `getAll` function (needed by AdminStatusTab to check all tasks across projects when deleting a status).

Add this function inside `useTaskService()`, before `getByProject`:

```typescript
    async function getAll(): Promise<Task[]> {
        const data = await api.get<HydraCollection<Task>>('/tasks')
        return extractHydraMembers(data)
    }
```

Update the return statement to include it:

```typescript
    return { getAll, getByProject, create, update, remove }
```

- [ ] **Step 23: Update AdminStatusTab to handle task reassignment on delete**

The existing `AdminStatusTab` does a simple `remove(id)` which would leave tasks orphaned. Port the reassignment logic from `ProjectStatusTab` (which is being deleted). Since statuses are now global, we need to load ALL tasks (not per-project) to check for affected tasks.

Replace the full content of `frontend/components/admin/AdminStatusTab.vue` with:

```vue
<template>
    <div>
        <div class="flex items-center justify-between">
            <h2 class="text-lg font-bold text-neutral-900">Statuts</h2>
            <button
                class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
                @click="openCreate"
            >
                + Ajouter un statut
            </button>
        </div>

        <DataTable
            :columns="columns"
            :items="items"
            :loading="isLoading"
            empty-message="Aucun statut trouvé."
            deletable
            @row-click="openEdit"
            @delete="requestDelete"
        >
            <template #cell-color="{ item }">
                <span
                    class="inline-block h-6 w-6 rounded-full"
                    :style="{ backgroundColor: item.color }"
                />
            </template>
        </DataTable>

        <TaskStatusDrawer
            v-model="drawerOpen"
            :item="selectedItem"
            @saved="onSaved"
        />

        <ConfirmDeleteStatusModal
            v-model="confirmModalOpen"
            :status-label="statusToDelete?.label ?? ''"
            :task-count="affectedTaskCount"
            :available-statuses="reassignTargets"
            @confirm="onConfirmDelete"
        />
    </div>
</template>

<script setup lang="ts">
import type { TaskStatus } from '~/services/dto/task-status'
import type { Task } from '~/services/dto/task'
import { useTaskStatusService } from '~/services/task-statuses'
import { useTaskService } from '~/services/tasks'

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

const columns: DataTableColumn[] = [
    { key: 'label', label: 'Libellé', primary: true },
    { key: 'color', label: 'Couleur' },
    { key: 'position', label: 'Position', class: 'text-neutral-700' },
]

const statusService = useTaskStatusService()
const taskService = useTaskService()

const items = ref<TaskStatus[]>([])
const tasks = ref<Task[]>([])
const isLoading = ref(true)
const drawerOpen = ref(false)
const selectedItem = ref<TaskStatus | null>(null)
const confirmModalOpen = ref(false)
const statusToDelete = ref<TaskStatus | null>(null)

const affectedTaskCount = computed(() => {
    if (!statusToDelete.value) return 0
    return tasks.value.filter(t => t.status?.id === statusToDelete.value!.id).length
})

const reassignTargets = computed(() => {
    if (!statusToDelete.value) return items.value
    return items.value.filter(s => s.id !== statusToDelete.value!.id)
})

async function loadItems() {
    isLoading.value = true
    try {
        const [statuses, allTasks] = await Promise.all([
            statusService.getAll(),
            taskService.getAll(),
        ])
        items.value = statuses
        tasks.value = allTasks
    } finally {
        isLoading.value = false
    }
}

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

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

async function requestDelete(item: TaskStatus) {
    statusToDelete.value = item
    const count = tasks.value.filter(t => t.status?.id === item.id).length
    if (count === 0) {
        await statusService.remove(item.id)
        await loadItems()
    } else {
        confirmModalOpen.value = true
    }
}

async function onConfirmDelete(targetStatusId: number | null) {
    if (!statusToDelete.value) return

    const affectedTasks = tasks.value.filter(t => t.status?.id === statusToDelete.value!.id)
    const statusIri = targetStatusId ? `/api/task_statuses/${targetStatusId}` : null

    await Promise.all(
        affectedTasks.map(t => taskService.update(t.id, { status: statusIri }))
    )

    await statusService.remove(statusToDelete.value.id)
    confirmModalOpen.value = false
    statusToDelete.value = null
    await loadItems()
}

async function onSaved() {
    await loadItems()
}

onMounted(() => {
    loadItems()
})
</script>
```

- [ ] **Step 24: Commit**

```bash
git add frontend/services/tasks.ts frontend/components/admin/AdminStatusTab.vue
git commit -m "feat(admin) : add task reassignment logic to AdminStatusTab"
```

### Task 10: Add Statuts tab to admin page

**Files:**
- Modify: `frontend/pages/admin.vue`

- [ ] **Step 24: Add Statuts tab to admin.vue**

In `frontend/pages/admin.vue`, update the `tabs` array to include statuses:

```typescript
const tabs = [
    { key: 'clients', label: 'Clients' },
    { key: 'statuses', label: 'Statuts' },
    { key: 'efforts', label: 'Efforts' },
    { key: 'priorities', label: 'Priorités' },
    { key: 'types', label: 'Types' },
    { key: 'users', label: 'Utilisateurs' },
] as const
```

Add the component in the template:

```html
<AdminStatusTab v-if="activeTab === 'statuses'" />
```

- [ ] **Step 25: Commit**

```bash
git add frontend/pages/admin.vue
git commit -m "feat(admin) : add statuts tab to admin page"
```

### Task 11: Update kanban page to use global statuses

**Files:**
- Modify: `frontend/pages/projects/[id]/index.vue`

- [ ] **Step 26: Change kanban to load global statuses**

In `frontend/pages/projects/[id]/index.vue`, in the `loadData` function, change:

```typescript
statusService.getByProject(projectId.value),
```

to:

```typescript
statusService.getAll(),
```

- [ ] **Step 27: Commit**

```bash
git add frontend/pages/projects/[id]/index.vue
git commit -m "refactor(frontend) : load global statuses in kanban page"
```

### Task 12: Remove per-project status pages, sidebar link, and orphaned components

**Files:**
- Delete: `frontend/pages/projects/[id]/statuses.vue`
- Delete: `frontend/components/project/ProjectStatusTab.vue`
- Modify: `frontend/layouts/default.vue`

- [ ] **Step 28: Delete per-project statuses page and ProjectStatusTab**

```bash
git rm frontend/pages/projects/[id]/statuses.vue
git rm frontend/components/project/ProjectStatusTab.vue
```

- [ ] **Step 29: Remove statuses SidebarLink from default.vue**

In `frontend/layouts/default.vue`, remove the statuses sidebar link block (inside the `v-if="currentProjectId"` template, lines 52-58):

```html
                        <SidebarLink
                            :to="`/projects/${currentProjectId}/statuses`"
                            icon="mdi:list-status"
                            label="Statuts"
                            :collapsed="ui.sidebarCollapsed"
                            sub
                        />
```

- [ ] **Step 30: Commit**

```bash
git add frontend/layouts/default.vue
git commit -m "refactor(frontend) : remove per-project statuses page and sidebar link"
```

### Task 13: Verify and clean up

- [ ] **Step 31: Check for remaining references to getByProject in task-statuses**

Search for any remaining `getByProject` calls on the status service and `projectId` references in status-related components:

```bash
cd /home/matthieu/dev_malio/Lesstime
grep -rn "getByProject\|projectId" frontend/ --include="*.vue" --include="*.ts" | grep -i status
grep -rn "ConfirmDeleteStatusModal\|ProjectStatusTab" frontend/ --include="*.vue" --include="*.ts"
```

Fix any remaining references found.

- [ ] **Step 32: Run the dev server and verify**

```bash
make db-reset && make dev-nuxt
```

Verify:
1. Admin page shows Clients tab with full CRUD (create, edit, delete)
2. Admin page shows Statuts tab with global statuses CRUD
3. Sidebar no longer shows "Clients" or per-project "Statuts" links
4. Kanban board displays all global statuses as columns
5. No errors in browser console

- [ ] **Step 33: Final commit if any cleanup was needed**

```bash
git add -A
git commit -m "chore : clean up remaining references after global statuses refactor"
```
