diff --git a/docs/plans/2026-03-12-admin-clients-global-statuses.md b/docs/plans/2026-03-12-admin-clients-global-statuses.md new file mode 100644 index 0000000..90c5438 --- /dev/null +++ b/docs/plans/2026-03-12-admin-clients-global-statuses.md @@ -0,0 +1,816 @@ +# 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 + + + +``` + +- [ ] **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('clients') +``` + +Add the component in the template `
` block: + +```html + +``` + +- [ ] **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 + +``` + +- [ ] **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 + ['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 { + const data = await api.get>('/task_statuses') + return extractHydraMembers(data) + } + + async function create(payload: TaskStatusWrite): Promise { + return api.post('/task_statuses', payload as Record, { + toastSuccessKey: 'taskStatuses.created', + }) + } + + async function update(id: number, payload: Partial): Promise { + return api.patch(`/task_statuses/${id}`, payload as Record, { + toastSuccessKey: 'taskStatuses.updated', + }) + } + + async function remove(id: number): Promise { + 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 { + const data = await api.get>('/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 + + + +``` + +- [ ] **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 + +``` + +- [ ] **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 + +``` + +- [ ] **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" +``` diff --git a/docs/superpowers/plans/2026-03-12-time-entry-multi-type-select.md b/docs/superpowers/plans/2026-03-12-time-entry-multi-type-select.md new file mode 100644 index 0000000..f62d669 --- /dev/null +++ b/docs/superpowers/plans/2026-03-12-time-entry-multi-type-select.md @@ -0,0 +1,158 @@ +# Time Entry Multi-Type Selection 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 selecting multiple task types in the TimeEntryDrawer, matching the existing multi-select pattern used in TaskDrawer. + +**Architecture:** Replace the single-select `MalioSelect` dropdown for types with the checkbox-based colored badge multi-select already used in TaskDrawer. The backend (ManyToMany relation) and DTO (`types: string[]`) already support multiple types — only the frontend form state and template need updating. + +**Tech Stack:** Vue 3, TypeScript + +--- + +## Chunk 1: Multi-Type Select in TimeEntryDrawer + +### Task 1: Update TimeEntryDrawer to support multiple type selection + +**Files:** +- Modify: `frontend/components/time-tracking/TimeEntryDrawer.vue` + +- [ ] **Step 1: Change form state from single typeId to typeIds array** + +In the `form` reactive object (line 133-142), replace: + +```typescript +typeId: null as number | null, +``` + +with: + +```typescript +typeIds: [] as number[], +``` + +- [ ] **Step 2: Add toggleType function** + +Add this function after the `durationLabel` computed (after line 165): + +```typescript +function toggleType(id: number) { + const idx = form.typeIds.indexOf(id) + if (idx >= 0) { + form.typeIds.splice(idx, 1) + } else { + form.typeIds.push(id) + } +} +``` + +- [ ] **Step 3: Remove the typeOptions computed** + +Delete the `typeOptions` computed (lines 152-154): + +```typescript +const typeOptions = computed(() => + props.types.map(t => ({ label: t.label, value: t.id })) +) +``` + +This is no longer needed since we won't use `MalioSelect`. + +- [ ] **Step 4: Replace MalioSelect template with multi-select badges** + +Replace the `MalioSelect` for type (lines 75-81): + +```vue + +``` + +with: + +```vue +
+

Types

+
+ +
+
+``` + +- [ ] **Step 5: Update populateForm to use typeIds** + +In the `populateForm` function, replace (line 194): + +```typescript +form.typeId = entry.types?.[0]?.id ?? null +``` + +with: + +```typescript +form.typeIds = entry.types?.map(t => t.id) ?? [] +``` + +And in the else branch (line 203), replace: + +```typescript +form.typeId = null +``` + +with: + +```typescript +form.typeIds = [] +``` + +- [ ] **Step 6: Update onSubmit payload to use typeIds** + +In the `onSubmit` function, replace (line 233): + +```typescript +types: form.typeId ? [`/api/task_types/${form.typeId}`] : [], +``` + +with: + +```typescript +types: form.typeIds.map(id => `/api/task_types/${id}`), +``` + +- [ ] **Step 7: Verify in browser** + +Run: `make dev-nuxt` + +1. Open time tracking page +2. Open an existing time entry → verify existing types are pre-selected as colored badges +3. Toggle types on/off → verify visual feedback (colored background when selected) +4. Save → verify types are persisted correctly +5. Create a new time entry with multiple types → verify they save correctly + +- [ ] **Step 8: Commit** + +```bash +git add frontend/components/time-tracking/TimeEntryDrawer.vue +git commit -m "feat(frontend) : allow multiple type selection in time entry drawer" +```