diff --git a/CLAUDE.md b/CLAUDE.md index 5d26d72..87575ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,10 +14,10 @@ Application de gestion de projet. Monorepo Symfony 8 (API Platform 4) + Nuxt 4. ## Structure ``` -src/Entity/ # Entités Doctrine (User, Client, Project, Task, TaskStatus, TaskEffort, TaskPriority, TaskTag, TaskGroup, TimeEntry, GiteaConfiguration, ClientTicket, Notification, TaskDocument, BookStackConfiguration, TaskBookStackLink, TaskRecurrence, ZimbraConfiguration) +src/Entity/ # Entités Doctrine (User, Client, Project, Task, TaskStatus, TaskEffort, TaskPriority, TaskTag, TaskGroup, TimeEntry, GiteaConfiguration, Notification, TaskDocument, BookStackConfiguration, TaskBookStackLink, TaskRecurrence, ZimbraConfiguration) src/ApiResource/ # Ressources API Platform (si découplées des entités) (ZimbraSettings, ZimbraTestConnection) src/Enum/ # PHP enums (RecurrenceType) -src/State/ # Providers et Processors API Platform (MeProvider, AppVersionProvider, ActiveTimeEntryProvider, UserPasswordHasherProcessor, TaskNumberProcessor, ClientTicket*Provider/Processor, NotificationProvider, Gitea*Provider, Gitea*Processor, ZimbraSettingsProvider/Processor, ZimbraTestConnectionProvider, TaskCalendarProcessor, RecurrenceHandler) +src/State/ # Providers et Processors API Platform (MeProvider, AppVersionProvider, ActiveTimeEntryProvider, UserPasswordHasherProcessor, TaskNumberProcessor, NotificationProvider, Gitea*Provider, Gitea*Processor, ZimbraSettingsProvider/Processor, ZimbraTestConnectionProvider, TaskCalendarProcessor, RecurrenceHandler) src/Service/ # Services métier (NotificationService, CalDavService, RecurrenceCalculator) src/Controller/ # Controllers custom Symfony (NotificationUnreadCountController, MarkAllReadController, UserAvatarController, TaskDocumentDownloadController) src/Mcp/Tool/ # MCP tools organisés par domaine (Project/, Task/, TaskMeta/, TimeEntry/, Reference/) @@ -31,12 +31,12 @@ migrations/ # Migrations Doctrine docs/plans/ # Plans d'implémentation docs/superpowers/ # Plans et specs superpowers frontend/ # App Nuxt 4 -frontend/pages/ # Pages (index, login, my-tasks, profile, projects, projects/[id], projects/[id]/groups, projects/[id]/archives, time-tracking, admin, portal/, portal/projects/[id], portal/projects/[id]/new-ticket) -frontend/layouts/ # Layouts (default, portal) -frontend/components/ # Composants Vue organisés en sous-dossiers (ui/, client/, project/, task/, user/, admin/, time-tracking/, client-ticket/, notification/) — inclut admin/AdminZimbraTab -frontend/composables/# Composables (useApi, useAppVersion, useNotifications, useClientTicketHelpers, useAvatarService) +frontend/pages/ # Pages (index, login, my-tasks, profile, projects, projects/[id], projects/[id]/groups, projects/[id]/archives, time-tracking, admin) +frontend/layouts/ # Layouts (default) +frontend/components/ # Composants Vue organisés en sous-dossiers (ui/, client/, project/, task/, user/, admin/, time-tracking/, notification/) — inclut admin/AdminZimbraTab +frontend/composables/# Composables (useApi, useAppVersion, useNotifications, useAvatarService) frontend/stores/ # Stores Pinia (auth, ui, timer) -frontend/services/ # Services API (auth, clients, gitea, projects, tasks, task-statuses, task-efforts, task-groups, task-priorities, task-tags, users, time-entries, client-tickets, notifications, task-documents, zimbra, task-recurrences) +frontend/services/ # Services API (auth, clients, gitea, projects, tasks, task-statuses, task-efforts, task-groups, task-priorities, task-tags, users, time-entries, notifications, task-documents, zimbra, task-recurrences) frontend/services/dto/ # Types TypeScript frontend/i18n/locales/ # Fichiers de traduction (langDir résolu depuis i18n/) ``` @@ -85,13 +85,13 @@ Exemples : `feat : add login page`, `fix(auth) : prevent null token crash` - Routes API préfixées `/api` (via `config/routes/api_platform.yaml`) - Le login (`/login_check`) est hors prefix `/api`, nginx réécrit `REQUEST_URI` vers `/login_check` - PHP CS Fixer : règles Symfony + PSR-12 + strict types -- Rôles : `ROLE_ADMIN`, `ROLE_USER`, `ROLE_CLIENT` — hiérarchie dans `security.yaml` -- `User::getRoles()` n'ajoute PAS `ROLE_USER` si l'user a `ROLE_CLIENT` (isolation) +- Rôles : `ROLE_ADMIN`, `ROLE_USER` — hiérarchie dans `security.yaml` +- `User::getRoles()` ajoute toujours `ROLE_USER` - PostgreSQL : `LIKE` sur colonne JSON ne marche pas → utiliser `roles::text LIKE` via native SQL - Controllers custom sous `/api/` : ajouter `priority: 1` sur `#[Route]` pour éviter le conflit avec API Platform `{id}` - Serialization : pour embarquer une relation (pas IRI), ajouter le groupe du parent aux propriétés de l'entité cible - Upload fichiers : utiliser `$file->getMimeType()` (pas `getClientMimeType()`) pour valider côté serveur — nécessite `symfony/mime` -- Auth endpoints mixtes (ROLE_USER + ROLE_CLIENT) : utiliser `#[IsGranted('IS_AUTHENTICATED_FULLY')]` au lieu d'un rôle spécifique +- Endpoints ouverts à tout utilisateur authentifié : utiliser `#[IsGranted('IS_AUTHENTICATED_FULLY')]` au lieu d'un rôle spécifique ### Frontend @@ -102,8 +102,6 @@ Exemples : `feat : add login page`, `fix(auth) : prevent null token crash` - Traductions dans `frontend/i18n/locales/` (le module résout `langDir` depuis `i18n/`) - 4 espaces d'indentation - MalioSelect : options `{ label: string, value: string | number | null }` — accepte les valeurs **string** (enums string OK, ex `category`/`StatusCategory`), pas seulement `number` (vérifié dans la source `Select.vue` : `modelValue: string | number | null`). L'option vide `null` n'est ajoutée que si `empty-option-label` est passé (ne pas le passer pour un champ requis). Largeur via `group-class` (pas de prop `minWidth`/`min-width`). ⚠️ Le `COMPONENTS.md` de la lib est inexact sur ce composant (il indique une clé `text` et une prop `minWidth` inexistantes) : la clé d'affichage réelle est `label`. Ne jamais modifier la lib `malio-layer-ui` depuis ce projet. -- Portal client : pages sous `/portal/`, layout `portal.vue`, middleware redirige `ROLE_CLIENT` (sans `ROLE_ADMIN`) vers `/portal` -- Users admin+client : ne pas bloquer — vérifier `ROLE_CLIENT && !ROLE_ADMIN` pour les restrictions ### Composants UI @@ -138,7 +136,6 @@ La librairie `@malio/layer-ui` fournit les composants de formulaire et d'action. - User admin : `admin` / `admin` (ROLE_ADMIN) - Users internes : `alice` / `alice`, `bob` / `bob`, `charlie` / `charlie` (ROLE_USER) -- Users client : `client-liot` / `client` (ROLE_CLIENT, client LIOT → SIRH), `client-acme` / `client` (ROLE_CLIENT, client ACME → CRM) - API token admin (dev) : `dev-mcp-token-for-testing-only-do-not-use-in-production` - ZimbraConfiguration : serverUrl `https://mail.ovh.com`, username `lesstime@ovh.fr`, enabled false - TaskRecurrence (hebdomadaire lun/mer/ven) attachée à la tâche "Réunion de suivi hebdomadaire" (SIRH) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 820b46a..35949cd 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,6 +1,6 @@ security: role_hierarchy: - ROLE_ADMIN: [ROLE_USER, ROLE_CLIENT] + ROLE_ADMIN: [ROLE_USER] # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: @@ -64,7 +64,7 @@ security: - { path: ^/api/version, roles: PUBLIC_ACCESS, methods: [ GET ] } - { path: ^/_mcp, roles: PUBLIC_ACCESS, methods: [ GET ] } - { path: ^/_mcp, roles: IS_AUTHENTICATED_FULLY } - # Mail : requiert authentification (les checks ROLE_USER/ROLE_CLIENT sont dans MailAccessChecker) + # Mail : requiert authentification (le check ROLE_USER est dans MailAccessChecker) - { path: ^/api/mail, roles: IS_AUTHENTICATED_FULLY } - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } diff --git a/docs/superpowers/plans/2026-05-22-employee-management-reorg.md b/docs/superpowers/plans/2026-05-22-employee-management-reorg.md new file mode 100644 index 0000000..ed84e08 --- /dev/null +++ b/docs/superpowers/plans/2026-05-22-employee-management-reorg.md @@ -0,0 +1,571 @@ +# Réorganisation gestion employés — Plan d'implémentation + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Sortir l'édition des informations RH du `UserDrawer` (qui ne garde que la case « Employé ») vers un onglet « Employés » dédié dans `team-absences`, avec une liste users⋈soldes et un drawer d'édition. + +**Architecture:** Réorganisation 100 % frontend. Les champs employé existent déjà sur l'entité `User` (backend) et le DTO `UserData`/`UserWrite` ; la persistance passe par `usersService.update()` (PATCH partiel, sans écrasement). La liste de l'onglet joint `usersService.getAll()` (filtré `isEmployee`) avec `absenceService.getBalances({ type: 'cp' })`. + +**Tech Stack:** Nuxt 4 / Vue 3 Composition API, TypeScript, composants `@malio/layer-ui` (MalioDate, MalioSelect, MalioInputText, MalioDataTable, MalioDrawer, MalioCheckbox), i18n `@nuxtjs/i18n`. + +**Conventions projet :** +- 4 espaces d'indentation, TypeScript strict. +- Pas de framework de test frontend → vérification au navigateur (serveur dev sur `http://localhost:3002`, Chrome DevTools MCP) + compilation HMR sans erreur console. +- **Commits gérés par l'utilisateur** : ne committer qu'après son feu vert explicite (règle CLAUDE.md). Les étapes « Commit » sont fournies mais à déclencher sur demande. Format : `() : `. + +--- + +### Task 1 : Clés i18n + +**Files:** +- Modify: `frontend/i18n/locales/fr.json` (objet `absences.admin`) + +- [ ] **Step 1 : Ajouter l'onglet et le bloc `employees`** + +Dans `absences.admin`, ajouter `"employees"` à `tabs`, et un nouveau bloc `employees`. Repérer le bloc existant : + +```json +"tabs": { "requests": "Demandes", "calendar": "Calendrier", "balances": "Soldes" }, +``` + +le remplacer par : + +```json +"tabs": { "requests": "Demandes", "calendar": "Calendrier", "balances": "Soldes", "employees": "Employés" }, +``` + +puis ajouter, à la suite des clés de `admin` (par ex. après `"adjust"`), le bloc : + +```json +"employees": { + "columns": { + "name": "Nom", + "contract": "Contrat", + "cpTaken": "CP pris", + "cpRemaining": "CP restants" + }, + "empty": "Aucun employé. Cochez « Employé » sur un utilisateur dans l'administration.", + "noContract": "—", + "drawer": { + "title": "Informations employé", + "save": "Enregistrer" + }, + "fields": { + "hireDate": "Date d'embauche", + "endDate": "Date de sortie", + "contractType": "Type de contrat", + "familySituation": "Situation familiale", + "workTimeRatio": "Temps de travail (ex : 1.0)", + "annualLeaveDays": "CP annuels (jours)", + "referencePeriodStart": "Début période réf. (MM-DD)", + "initialLeaveBalance": "Solde CP initial", + "nbChildren": "Nombre d'enfants" + }, + "contract": { + "cdi": "CDI", + "cdd": "CDD", + "stage": "Stage", + "alternance": "Alternance", + "autre": "Autre" + }, + "family": { + "celibataire": "Célibataire", + "marie": "Marié(e)", + "pacse": "Pacsé(e)", + "divorce": "Divorcé(e)", + "veuf": "Veuf(ve)" + } +} +``` + +- [ ] **Step 2 : Vérifier la validité JSON** + +Run: `cd frontend && python3 -c "import json; json.load(open('i18n/locales/fr.json')); print('OK')"` +Expected: `OK` + +- [ ] **Step 3 : Commit** (sur feu vert utilisateur) + +```bash +git add frontend/i18n/locales/fr.json +git commit -m "feat(absences) : clés i18n onglet et drawer employés" +``` + +--- + +### Task 2 : Composant `EmployeeDrawer.vue` + +**Files:** +- Create: `frontend/components/absence/EmployeeDrawer.vue` + +- [ ] **Step 1 : Créer le composant** + +Crée `frontend/components/absence/EmployeeDrawer.vue` avec ce contenu exact : + +```vue + + + +``` + +- [ ] **Step 2 : Vérifier la compilation** + +Le serveur dev (`http://localhost:3002`) recompile à la sauvegarde. Vérifier qu'aucune erreur de compilation/HMR n'apparaît dans la console du terminal `make dev-nuxt` ni dans la console navigateur. (Le composant n'est pas encore monté ; cette étape ne fait que valider la syntaxe.) + +- [ ] **Step 3 : Commit** (sur feu vert utilisateur) + +```bash +git add frontend/components/absence/EmployeeDrawer.vue +git commit -m "feat(absences) : drawer d'édition des informations employé" +``` + +--- + +### Task 3 : Onglet « Employés » dans `team-absences` + +**Files:** +- Modify: `frontend/pages/team-absences.vue` + +- [ ] **Step 1 : Ajouter l'import du service users et le type** + +Après les imports existants (`useAbsenceHelpers`), ajouter : + +```ts +import { useUserService } from "~/services/users"; +import type { UserData } from "~/services/dto/user-data"; +``` + +Et après la déclaration `type BalanceRow = ...`, ajouter le type de ligne : + +```ts +type EmployeeRow = UserData & { + contractText: string; + cpTakenText: string; + cpRemainingText: string; +}; +``` + +- [ ] **Step 2 : Ajouter l'onglet à `tabs`** + +Remplacer le tableau `tabs` (qui se termine par l'onglet `balances`) en ajoutant l'entrée employés : + +```ts +const tabs = [ + { + key: "requests", + label: t("absences.admin.tabs.requests"), + icon: "mdi:format-list-bulleted", + }, + { + key: "calendar", + label: t("absences.admin.tabs.calendar"), + icon: "mdi:calendar-month", + }, + { + key: "balances", + label: t("absences.admin.tabs.balances"), + icon: "mdi:scale-balance", + }, + { + key: "employees", + label: t("absences.admin.tabs.employees"), + icon: "mdi:account-group", + }, +]; +``` + +- [ ] **Step 3 : Ajouter l'état, les colonnes et les lignes de l'onglet** + +Après `const balances = ref([]);`, ajouter : + +```ts +const employees = ref([]); +const employeeDrawerOpen = ref(false); +const selectedEmployee = ref(null); +``` + +Après `const balanceRows = computed(...)`, ajouter colonnes + lignes : + +```ts +const employeeColumns = [ + { key: "username", label: t("absences.admin.employees.columns.name") }, + { key: "contractText", label: t("absences.admin.employees.columns.contract") }, + { key: "cpTakenText", label: t("absences.admin.employees.columns.cpTaken") }, + { key: "cpRemainingText", label: t("absences.admin.employees.columns.cpRemaining") }, +]; + +const employeeRows = computed(() => { + // Map user.id -> solde CP de la période courante. + const cpByUser = new Map(); + for (const b of balances.value) { + if (b.type === "cp") cpByUser.set(b.user.id, b); + } + const dash = t("absences.admin.employees.noContract"); + return employees.value.map((u) => { + const cp = cpByUser.get(u.id); + return { + ...u, + contractText: u.contractType ?? dash, + cpTakenText: cp ? formatDays(cp.taken) : dash, + cpRemainingText: cp ? formatDays(cp.available) : dash, + }; + }); +}); +``` + +- [ ] **Step 4 : Ajouter le chargement et l'ouverture du drawer** + +Après `async function loadBalances() {...}`, ajouter : + +```ts +async function loadEmployees() { + const all = await useUserService().getAll(); + employees.value = all.filter((u) => u.isEmployee); +} + +function openEmployee(item: Record) { + selectedEmployee.value = item as EmployeeRow; + employeeDrawerOpen.value = true; +} +``` + +Puis inclure `loadEmployees()` au montage. Remplacer le `onMounted` existant : + +```ts +onMounted(async () => { + await Promise.all([reloadRequests(), loadBalances()]); +}); +``` + +par : + +```ts +onMounted(async () => { + await Promise.all([reloadRequests(), loadBalances(), loadEmployees()]); +}); +``` + +- [ ] **Step 5 : Ajouter le slot d'onglet dans le template** + +Juste après la fermeture du slot `` de l'onglet `#balances` (avant ``), ajouter : + +```vue + + +``` + +- [ ] **Step 6 : Monter le drawer employé** + +Après le composant `` (avant `` de fin de template), ajouter : + +```vue + +``` + +- [ ] **Step 7 : Vérification navigateur** + +Aller sur `http://localhost:3002/team-absences`, onglet « Employés ». Vérifier : liste des users `isEmployee` avec Nom / Contrat / CP pris / CP restants ; clic sur une ligne ouvre le drawer ; aucune erreur console. + +- [ ] **Step 8 : Commit** (sur feu vert utilisateur) + +```bash +git add frontend/pages/team-absences.vue +git commit -m "feat(absences) : onglet Employés (liste + ouverture drawer)" +``` + +--- + +### Task 4 : Allègement du `UserDrawer` + +**Files:** +- Modify: `frontend/components/user/UserDrawer.vue` + +- [ ] **Step 1 : Réduire le bloc RH du template à la seule case** + +Remplacer le bloc (lignes ~74-107) : + +```vue + +
+ + +
+ +
+
+``` + +par : + +```vue + +
+ +

+ Les informations RH (contrat, dates, CP…) se gèrent dans Absences équipe → onglet Employés. +

+
+``` + +- [ ] **Step 2 : Nettoyer l'état du formulaire** + +Dans `const form = reactive({...})`, supprimer les champs détaillés et ne garder que `isEmployee`. Résultat : + +```ts +const form = reactive({ + username: '', + password: '', + roles: [] as string[], + clientId: null as number | null, + allowedProjectIds: [] as number[], + isEmployee: false, +}) +``` + +- [ ] **Step 3 : Nettoyer l'hydratation à l'ouverture** + +Dans le `watch(() => props.modelValue, ...)`, supprimer toutes les lignes `form.hireDate = ...` → `form.nbChildren = ...` des deux branches (`props.item` et `else`). Conserver `form.isEmployee = props.item.isEmployee ?? false` (branche édition) et `form.isEmployee = false` (branche création). + +- [ ] **Step 4 : Ne plus envoyer les champs détaillés dans le payload** + +Dans `handleSubmit`, réduire le `payload` aux champs de compte + `isEmployee` : + +```ts + const payload: UserWrite = { + username: form.username.trim(), + roles: form.roles, + client: form.clientId !== null ? `/api/clients/${form.clientId}` : null, + allowedProjects: form.clientId !== null + ? form.allowedProjectIds.map((id) => `/api/projects/${id}`) + : [], + isEmployee: form.isEmployee, + } + if (form.password) { + payload.plainPassword = form.password + } +``` + +- [ ] **Step 5 : Supprimer les imports/constantes devenus inutiles** + +Dans le ` diff --git a/frontend/components/absence/AbsenceBalanceCards.vue b/frontend/components/absence/AbsenceBalanceCards.vue new file mode 100644 index 0000000..4d831d8 --- /dev/null +++ b/frontend/components/absence/AbsenceBalanceCards.vue @@ -0,0 +1,119 @@ + + + diff --git a/frontend/components/absence/AbsenceCalendar.vue b/frontend/components/absence/AbsenceCalendar.vue new file mode 100644 index 0000000..604bf76 --- /dev/null +++ b/frontend/components/absence/AbsenceCalendar.vue @@ -0,0 +1,143 @@ + + + diff --git a/frontend/components/absence/AbsenceDateField.vue b/frontend/components/absence/AbsenceDateField.vue new file mode 100644 index 0000000..fbf31e9 --- /dev/null +++ b/frontend/components/absence/AbsenceDateField.vue @@ -0,0 +1,73 @@ + + + diff --git a/frontend/components/absence/AbsenceDetailDrawer.vue b/frontend/components/absence/AbsenceDetailDrawer.vue new file mode 100644 index 0000000..1ba3820 --- /dev/null +++ b/frontend/components/absence/AbsenceDetailDrawer.vue @@ -0,0 +1,193 @@ + + + diff --git a/frontend/components/absence/AbsenceRejectDrawer.vue b/frontend/components/absence/AbsenceRejectDrawer.vue new file mode 100644 index 0000000..1828743 --- /dev/null +++ b/frontend/components/absence/AbsenceRejectDrawer.vue @@ -0,0 +1,69 @@ + + + diff --git a/frontend/components/absence/AbsenceRequestDrawer.vue b/frontend/components/absence/AbsenceRequestDrawer.vue new file mode 100644 index 0000000..35b4ce0 --- /dev/null +++ b/frontend/components/absence/AbsenceRequestDrawer.vue @@ -0,0 +1,296 @@ + + + diff --git a/frontend/components/absence/EmployeeDrawer.vue b/frontend/components/absence/EmployeeDrawer.vue new file mode 100644 index 0000000..ae1029e --- /dev/null +++ b/frontend/components/absence/EmployeeDrawer.vue @@ -0,0 +1,163 @@ + + + diff --git a/frontend/components/admin/AdminAbsencePolicyTab.vue b/frontend/components/admin/AdminAbsencePolicyTab.vue new file mode 100644 index 0000000..290df97 --- /dev/null +++ b/frontend/components/admin/AdminAbsencePolicyTab.vue @@ -0,0 +1,76 @@ + + + diff --git a/frontend/components/admin/AdminClientTicketTab.vue b/frontend/components/admin/AdminClientTicketTab.vue deleted file mode 100644 index 85f7b54..0000000 --- a/frontend/components/admin/AdminClientTicketTab.vue +++ /dev/null @@ -1,382 +0,0 @@ - - - - - diff --git a/frontend/components/admin/WorkflowDrawer.vue b/frontend/components/admin/WorkflowDrawer.vue index bf2b206..3ffd57d 100644 --- a/frontend/components/admin/WorkflowDrawer.vue +++ b/frontend/components/admin/WorkflowDrawer.vue @@ -1,5 +1,8 @@ - - - - diff --git a/frontend/components/client-ticket/ProjectClientTickets.vue b/frontend/components/client-ticket/ProjectClientTickets.vue deleted file mode 100644 index 83995d3..0000000 --- a/frontend/components/client-ticket/ProjectClientTickets.vue +++ /dev/null @@ -1,333 +0,0 @@ - - - - - diff --git a/frontend/components/client/ClientDrawer.vue b/frontend/components/client/ClientDrawer.vue index 1c1a536..918b188 100644 --- a/frontend/components/client/ClientDrawer.vue +++ b/frontend/components/client/ClientDrawer.vue @@ -1,5 +1,8 @@