feat(absences) : avancement module absences + suppression du portail client
Deux lots regroupés sur la branche feat/absence-management. Suppression complète du portail client : - retire ROLE_CLIENT (security.yaml) ; User::getRoles() ajoute toujours ROLE_USER - supprime l'entité ClientTicket (+ repo, states, relations), User.client et User.allowedProjects, NotificationService, ProjectAllowedExtension, le bloc ROLE_CLIENT de MailAccessChecker - front : pages /portal, layout portal, composants client-ticket/, AdminClientTicketTab, services/dto/i18n/docs associés - fixtures : retire les users client-liot / client-acme - migration Version20260522110000 (drop client_ticket, user_allowed_projects, colonnes liées ; task_document.task_id -> NOT NULL) - tests : retire les cas obsolètes testant le blocage des clients sur le mail Module gestion des absences (WIP) : - entités / migrations (Version20260521160000, Version20260522090000) - pages absences.vue / team-absences.vue, composants frontend/components/absence/ - services front, AccrueLeaveCommand, PublicHolidayController Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
571
docs/superpowers/plans/2026-05-22-employee-management-reorg.md
Normal file
571
docs/superpowers/plans/2026-05-22-employee-management-reorg.md
Normal file
@@ -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 : `<type>(<scope>) : <message>`.
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
<template>
|
||||
<MalioDrawer v-model="open" drawer-class="max-w-lg">
|
||||
<template #header>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">{{ $t('absences.admin.employees.drawer.title') }}</h2>
|
||||
<p v-if="user" class="text-sm text-neutral-500">{{ user.username }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<form v-if="user" class="grid grid-cols-1 gap-4 sm:grid-cols-2" @submit.prevent="save">
|
||||
<MalioDate
|
||||
v-model="form.hireDate"
|
||||
:label="$t('absences.admin.employees.fields.hireDate')"
|
||||
group-class="w-full"
|
||||
/>
|
||||
<MalioDate
|
||||
v-model="form.endDate"
|
||||
:label="$t('absences.admin.employees.fields.endDate')"
|
||||
group-class="w-full"
|
||||
/>
|
||||
<MalioSelect
|
||||
v-model="form.contractType"
|
||||
:label="$t('absences.admin.employees.fields.contractType')"
|
||||
:options="contractOptions"
|
||||
empty-option-label="—"
|
||||
group-class="w-full"
|
||||
/>
|
||||
<MalioSelect
|
||||
v-model="form.familySituation"
|
||||
:label="$t('absences.admin.employees.fields.familySituation')"
|
||||
:options="familyOptions"
|
||||
empty-option-label="—"
|
||||
group-class="w-full"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="form.workTimeRatio"
|
||||
:label="$t('absences.admin.employees.fields.workTimeRatio')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="form.annualLeaveDays"
|
||||
:label="$t('absences.admin.employees.fields.annualLeaveDays')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="form.referencePeriodStart"
|
||||
:label="$t('absences.admin.employees.fields.referencePeriodStart')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="form.initialLeaveBalance"
|
||||
:label="$t('absences.admin.employees.fields.initialLeaveBalance')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="form.nbChildren"
|
||||
:label="$t('absences.admin.employees.fields.nbChildren')"
|
||||
input-class="w-full"
|
||||
/>
|
||||
|
||||
<div class="col-span-full mt-2 flex justify-end">
|
||||
<MalioButton
|
||||
:label="$t('absences.admin.employees.drawer.save')"
|
||||
button-class="w-auto px-6"
|
||||
:disabled="submitting"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</MalioDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ContractType, FamilySituation, UserData } from '~/services/dto/user-data'
|
||||
import { useUserService } from '~/services/users'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
user: UserData | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'saved': []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { update } = useUserService()
|
||||
|
||||
const open = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v),
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
const contractOptions = [
|
||||
{ label: t('absences.admin.employees.contract.cdi'), value: 'CDI' },
|
||||
{ label: t('absences.admin.employees.contract.cdd'), value: 'CDD' },
|
||||
{ label: t('absences.admin.employees.contract.stage'), value: 'STAGE' },
|
||||
{ label: t('absences.admin.employees.contract.alternance'), value: 'ALTERNANCE' },
|
||||
{ label: t('absences.admin.employees.contract.autre'), value: 'AUTRE' },
|
||||
]
|
||||
|
||||
const familyOptions = [
|
||||
{ label: t('absences.admin.employees.family.celibataire'), value: 'CELIBATAIRE' },
|
||||
{ label: t('absences.admin.employees.family.marie'), value: 'MARIE' },
|
||||
{ label: t('absences.admin.employees.family.pacse'), value: 'PACSE' },
|
||||
{ label: t('absences.admin.employees.family.divorce'), value: 'DIVORCE' },
|
||||
{ label: t('absences.admin.employees.family.veuf'), value: 'VEUF' },
|
||||
]
|
||||
|
||||
const form = reactive({
|
||||
hireDate: null as string | null,
|
||||
endDate: null as string | null,
|
||||
contractType: null as ContractType | null,
|
||||
familySituation: null as FamilySituation | null,
|
||||
workTimeRatio: '1.0',
|
||||
annualLeaveDays: '25',
|
||||
referencePeriodStart: '06-01',
|
||||
initialLeaveBalance: '0',
|
||||
nbChildren: '0',
|
||||
})
|
||||
|
||||
function hydrate(u: UserData | null) {
|
||||
if (!u) return
|
||||
form.hireDate = u.hireDate ? u.hireDate.slice(0, 10) : null
|
||||
form.endDate = u.endDate ? u.endDate.slice(0, 10) : null
|
||||
form.contractType = u.contractType ?? null
|
||||
form.familySituation = u.familySituation ?? null
|
||||
form.workTimeRatio = String(u.workTimeRatio ?? 1)
|
||||
form.annualLeaveDays = String(u.annualLeaveDays ?? 25)
|
||||
form.referencePeriodStart = u.referencePeriodStart ?? '06-01'
|
||||
form.initialLeaveBalance = String(u.initialLeaveBalance ?? 0)
|
||||
form.nbChildren = String(u.nbChildren ?? 0)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (isOpen) => {
|
||||
if (isOpen) hydrate(props.user)
|
||||
})
|
||||
|
||||
async function save() {
|
||||
if (!props.user) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await update(props.user.id, {
|
||||
isEmployee: true,
|
||||
hireDate: form.hireDate || null,
|
||||
endDate: form.endDate || null,
|
||||
contractType: form.contractType,
|
||||
familySituation: form.familySituation,
|
||||
workTimeRatio: Number(form.workTimeRatio) || 1,
|
||||
annualLeaveDays: Number(form.annualLeaveDays) || 0,
|
||||
referencePeriodStart: form.referencePeriodStart || '06-01',
|
||||
initialLeaveBalance: Number(form.initialLeaveBalance) || 0,
|
||||
nbChildren: Number(form.nbChildren) || 0,
|
||||
})
|
||||
emit('saved')
|
||||
open.value = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
- [ ] **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<AbsenceBalance[]>([]);`, ajouter :
|
||||
|
||||
```ts
|
||||
const employees = ref<UserData[]>([]);
|
||||
const employeeDrawerOpen = ref(false);
|
||||
const selectedEmployee = ref<UserData | null>(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<EmployeeRow[]>(() => {
|
||||
// Map user.id -> solde CP de la période courante.
|
||||
const cpByUser = new Map<number, AbsenceBalance>();
|
||||
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<string, unknown>) {
|
||||
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 `</template>` de l'onglet `#balances` (avant `</MalioTabList>`), ajouter :
|
||||
|
||||
```vue
|
||||
<!-- Employees -->
|
||||
<template #employees>
|
||||
<div class="min-h-[30rem] pt-10">
|
||||
<MalioDataTable
|
||||
:columns="employeeColumns"
|
||||
:items="employeeRows"
|
||||
:total-items="employeeRows.length"
|
||||
:empty-message="$t('absences.admin.employees.empty')"
|
||||
@row-click="openEmployee"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
- [ ] **Step 6 : Monter le drawer employé**
|
||||
|
||||
Après le composant `<AbsenceBalanceAdjustDrawer ... />` (avant `</div>` de fin de template), ajouter :
|
||||
|
||||
```vue
|
||||
<EmployeeDrawer
|
||||
v-model="employeeDrawerOpen"
|
||||
:user="selectedEmployee"
|
||||
@saved="loadEmployees"
|
||||
/>
|
||||
```
|
||||
|
||||
- [ ] **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
|
||||
<!-- RH / Absences -->
|
||||
<div class="mt-6 border-t border-neutral-200 pt-4">
|
||||
<MalioCheckbox v-model="form.isEmployee" label="Employé (soumis à la gestion des absences)" />
|
||||
|
||||
<div v-if="form.isEmployee" class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<!-- ... tous les champs détaillés ... -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
par :
|
||||
|
||||
```vue
|
||||
<!-- RH / Absences -->
|
||||
<div class="mt-6 border-t border-neutral-200 pt-4">
|
||||
<MalioCheckbox v-model="form.isEmployee" label="Employé (soumis à la gestion des absences)" />
|
||||
<p v-if="form.isEmployee" class="mt-2 text-xs text-neutral-500">
|
||||
Les informations RH (contrat, dates, CP…) se gèrent dans Absences équipe → onglet Employés.
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **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 `<script setup>` : supprimer `contractOptions` et `familyOptions` (constantes locales) ; retirer `ContractType, FamilySituation` de l'import `~/services/dto/user-data` (garder `UserData, UserWrite`). Vérifier qu'aucune autre référence ne subsiste.
|
||||
|
||||
Run: `cd frontend && grep -n "contractOptions\|familyOptions\|ContractType\|FamilySituation\|hireDate\|nbChildren" components/user/UserDrawer.vue`
|
||||
Expected: aucune ligne (sortie vide).
|
||||
|
||||
- [ ] **Step 6 : Vérification navigateur**
|
||||
|
||||
Aller sur `http://localhost:3002/admin`, ouvrir un utilisateur. Vérifier : seule la case « Employé » + la note ; cocher/décocher et enregistrer fonctionne ; rouvrir un employé déjà renseigné depuis l'onglet Employés → ses champs RH sont intacts (non écrasés par l'enregistrement du UserDrawer). Aucune erreur console.
|
||||
|
||||
- [ ] **Step 7 : Commit** (sur feu vert utilisateur)
|
||||
|
||||
```bash
|
||||
git add frontend/components/user/UserDrawer.vue
|
||||
git commit -m "refactor(users) : UserDrawer ne gère plus que le flag Employé"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5 : Vérification de bout en bout
|
||||
|
||||
**Files:** aucun (vérification navigateur via Chrome DevTools MCP)
|
||||
|
||||
- [ ] **Step 1 : Flux complet**
|
||||
|
||||
1. `admin` → ouvrir un user non-employé → cocher « Employé » → enregistrer.
|
||||
2. `team-absences` → onglet « Employés » → l'utilisateur apparaît.
|
||||
3. Clic sur sa ligne → `EmployeeDrawer` s'ouvre → renseigner dates (JJ/MM/AAAA), contrat, CP annuels → enregistrer.
|
||||
4. La liste se recharge ; rouvrir la ligne → valeurs persistées.
|
||||
5. Retour `admin` sur le même user → seule la case « Employé » (toujours cochée), pas de champ RH.
|
||||
|
||||
- [ ] **Step 2 : Contrôle console**
|
||||
|
||||
Vérifier l'absence d'erreurs/warnings Vue sur les trois écrans (admin, onglet Employés, drawer).
|
||||
|
||||
- [ ] **Step 3 : Commit final éventuel** (sur feu vert utilisateur)
|
||||
|
||||
Si des ajustements ont été faits pendant la vérification, les committer avec un message approprié.
|
||||
|
||||
---
|
||||
|
||||
## Self-review (couverture du spec)
|
||||
|
||||
- UserDrawer réduit à la case « Employé » → Task 4. ✅
|
||||
- Onglet « Employés » admin-only (page déjà `middleware: ["admin"]`) → Task 3. ✅
|
||||
- Liste Nom · Contrat · CP pris · CP restants (users ⋈ soldes cp) → Task 3 (Steps 3-5). ✅
|
||||
- Drawer d'édition en composants Malio (MalioDate/MalioSelect/MalioInputText) → Task 2. ✅
|
||||
- Persistance via `usersService.update` (PATCH partiel) → Task 2 (Step 1, `save`). ✅
|
||||
- i18n regroupé sous `absences.admin.employees.*` + onglet → Task 1. ✅
|
||||
- Pas de placeholder, types cohérents (`EmployeeRow`, `UserData`, `ContractType`/`FamilySituation`), noms de méthodes alignés (`loadEmployees`, `openEmployee`, `save`). ✅
|
||||
@@ -0,0 +1,119 @@
|
||||
# Refonte UX du formulaire « Nouvelle demande d'absence »
|
||||
|
||||
Date : 2026-05-21
|
||||
Composant : `frontend/components/absence/AbsenceRequestDrawer.vue`
|
||||
Branche : `feat/absence-management`
|
||||
|
||||
## Contexte & problème
|
||||
|
||||
Le formulaire actuel fonctionne mais est inutilisable côté UX :
|
||||
|
||||
- `VueDatePicker` brut **non thématisé** (couleur violette par défaut, calendrier au rendu anglo-saxon / semaine au dimanche) → aspect « cassé », rien à voir avec PayFit.
|
||||
- `<label>` et `<input type="file">` bruts au lieu des composants Malio.
|
||||
- **Aucune erreur explicite** : le bouton « Valider » est simplement grisé via `canSubmit` quand un champ manque, sans dire pourquoi.
|
||||
- `preview` qui échoue en silence (`catch { preview.value = null }`).
|
||||
- Solde insuffisant signalé par un simple warning ambre.
|
||||
|
||||
Cible : reproduire l'ergonomie PayFit (référence fournie par l'utilisateur) — apparition progressive des champs, deux champs date saisissables + popup, pills de demi-journée, lignes de solde, erreurs explicites.
|
||||
|
||||
## Objectifs
|
||||
|
||||
1. Look & ergonomie alignés sur PayFit, cohérents avec le design system Malio.
|
||||
2. Apparition **progressive** des champs au fil des choix.
|
||||
3. **Erreurs explicites** par champ, affichées au clic « Valider », qui se vident dès correction.
|
||||
4. Champs de date en français `JJ / MM / AAAA`, calendrier FR / semaine au lundi.
|
||||
5. Justificatif via `MalioInputUpload`, affiché seulement si le type l'exige.
|
||||
|
||||
## Hors périmètre
|
||||
|
||||
- Aucun changement backend : payload (`AbsenceRequestWrite`), endpoints `create` / `preview` / `uploadJustification` inchangés.
|
||||
- Aucune modification des autres composants du module (traités séparément dans les points #2 à #7 de la revue).
|
||||
|
||||
## Layout cible (drawer `max-w-xl`, 1 colonne, apparition progressive)
|
||||
|
||||
**Étape 1 — toujours visible**
|
||||
- `Type d'absence` : `MalioSelect` (options = policies actives). Erreur inline si vide au submit.
|
||||
|
||||
**Étape 2 — apparaît dès qu'un type est choisi**
|
||||
- `Date de début` : champ texte `JJ / MM / AAAA` saisissable + icône calendrier (popup) + bouton effacer. `VueDatePicker` mono-date, thématisé (voir ci-dessous).
|
||||
- Pills demi-journée début : `Journée entière` | `Matin` | `Après-midi` (défaut : Journée entière).
|
||||
- Ligne `Solde au <date début> : X jours` (valeur = `preview.available`, alignée à droite, **non repliable**).
|
||||
|
||||
**Étape 3 — apparaît dès que la date de début est renseignée**
|
||||
- `Date de fin` : même composant que la date de début.
|
||||
- Pills demi-journée fin : `Journée entière` | `Matin` (défaut : Journée entière).
|
||||
- Ligne `Durée de la demande : N jours` (valeur = `preview.countedDays`).
|
||||
- Ligne `Solde après validation : Y jours` (valeur = `preview.projectedAvailable`, non repliable). Si `< 0` → bandeau ambre **non bloquant** « solde après = … j, demande soumise pour validation ».
|
||||
|
||||
**Étape 4**
|
||||
- `Justificatif` : `MalioInputUpload`, affiché **uniquement si** `selectedPolicy.justificationRequired`. Label avec `*`. Erreur inline si requis et absent.
|
||||
- `Commentaire` (optionnel) : `MalioInputTextArea`, placeholder « Écrire un commentaire… ».
|
||||
|
||||
**Footer**
|
||||
- `[ Annuler (tertiary) ] [ Valider (primary) ]`, bouton **toujours actif**.
|
||||
|
||||
## Datepicker — thématisation
|
||||
|
||||
Reprendre le pattern de `frontend/components/ui/DateFilter.vue` :
|
||||
|
||||
- `VueDatePicker` mono-date, `:enable-time-picker="false"`, `auto-apply`, `text-input` activé (saisie clavier), `format` → `JJ/MM/AAAA`.
|
||||
- `locale="fr"` (chaîne) + semaine au lundi (`week-start: 1`).
|
||||
- Variables CSS `--dp-primary-color: #222783`, `--dp-border-color`, `--dp-hover-color`, `--dp-font-size`, `font-family: inherit` (scopées au composant).
|
||||
- `:min-date` sur la date de fin = date de début ; `:max-date` sur la date de début = date de fin.
|
||||
|
||||
## Pills demi-journée → payload
|
||||
|
||||
Segment de boutons (style pill, bordure + fond bleu primaire quand sélectionné).
|
||||
|
||||
| Pill début | `startHalfDay` | Pill fin | `endHalfDay` |
|
||||
|------------|----------------|----------|--------------|
|
||||
| Journée entière | `null` | Journée entière | `null` |
|
||||
| Matin | `matin` | Matin | `matin` |
|
||||
| Après-midi | `apres_midi` | — | — |
|
||||
|
||||
Cas particulier — **demande sur une seule journée** (`startDate == endDate`) : seules les pills de début sont pertinentes (`Journée entière` / `Matin` / `Après-midi`) ; les pills de fin sont masquées et `endHalfDay` recopie `startHalfDay`. Le décompte reste calculé par `preview` côté backend.
|
||||
|
||||
## Validation
|
||||
|
||||
État réactif `errors: { type?: string; startDate?: string; endDate?: string; justification?: string }`.
|
||||
|
||||
`validate()` (au clic « Valider ») remplit `errors` :
|
||||
- `type` manquant → `absences.form.errors.typeRequired`
|
||||
- `startDate` manquante → `absences.form.errors.startRequired`
|
||||
- `endDate` manquante → `absences.form.errors.endRequired`
|
||||
- `endDate < startDate` → `absences.form.errors.endBeforeStart`
|
||||
- `preview.countedDays <= 0` → `absences.form.errors.zeroDays`
|
||||
- justificatif requis et absent → `absences.form.errors.justificationRequired`
|
||||
|
||||
Si `errors` non vide → on stoppe la soumission. Des `watch` vident chaque message dès que le champ correspondant redevient valide.
|
||||
|
||||
Le solde insuffisant **n'est pas** une erreur bloquante (seul le bandeau ambre).
|
||||
|
||||
### Erreurs serveur
|
||||
- `service.create` / `uploadJustification` : le toast d'erreur de `useApi` reste ; en plus, un bandeau d'erreur en tête de formulaire affiche le message renvoyé (422 de validation).
|
||||
- `preview` : conserver le `catch` pour les coupures réseau, mais ne plus masquer une erreur de validation de période (afficher le message si l'API en renvoie un).
|
||||
|
||||
## Calcul live (preview)
|
||||
|
||||
Inchangé sur le principe : `watch` debouncé (300 ms) sur `[type, startDate, endDate, startHalfDay, endHalfDay]` → `service.preview(payload)`. Les lignes de solde et de durée se mettent à jour à partir du résultat (`available`, `countedDays`, `projectedAvailable`).
|
||||
|
||||
## i18n
|
||||
|
||||
Nouvelles clés sous `absences.form.errors.*` dans `frontend/i18n/locales/fr.json` :
|
||||
`typeRequired`, `startRequired`, `endRequired`, `endBeforeStart`, `zeroDays`, `justificationRequired`, plus `absences.form.balanceAt` (« Solde au {date} ») et `absences.form.duration` (« Durée de la demande »).
|
||||
|
||||
## Découpage du composant
|
||||
|
||||
`AbsenceRequestDrawer.vue` orchestre l'état du formulaire. Pour garder le fichier focalisé, extraire :
|
||||
- `AbsenceDateField.vue` : champ date thématisé + pills demi-journée (props : `modelValue`, `halfValue`, `halfOptions`, `label`, `error`, `min`/`max`).
|
||||
|
||||
Le reste (lignes de solde, bandeau, footer) reste inline dans le drawer.
|
||||
|
||||
## Critères d'acceptation
|
||||
|
||||
- Au chargement, seul « Type d'absence » est visible ; les sections suivantes apparaissent au fur et à mesure.
|
||||
- Les dates s'affichent et se saisissent en `JJ / MM / AAAA`, calendrier en français, semaine au lundi, thème bleu primaire.
|
||||
- Cliquer « Valider » sans remplir → messages d'erreur explicites sous les champs concernés ; ils disparaissent dès correction.
|
||||
- Un type à justificatif obligatoire affiche le champ d'upload et bloque la soumission tant qu'aucun fichier n'est fourni.
|
||||
- Une période dépassant le solde affiche le bandeau ambre mais reste soumissible.
|
||||
- Le payload envoyé au backend est identique à l'actuel.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Réorganisation de la gestion des employés (module Absences)
|
||||
|
||||
Date : 2026-05-22
|
||||
Branche : `feat/absence-management`
|
||||
Statut : design approuvé, prêt pour plan d'implémentation
|
||||
|
||||
## Contexte
|
||||
|
||||
Aujourd'hui, le `UserDrawer` (admin → utilisateurs) porte deux responsabilités mélangées :
|
||||
|
||||
1. l'administration du compte (nom, mot de passe, rôles, client, projets) ;
|
||||
2. **tout le détail RH/employé** : case « Employé » + un bloc de champs (date d'embauche, date de sortie, type de contrat, situation familiale, temps de travail, CP annuels, début de période de référence, solde CP initial, nombre d'enfants).
|
||||
|
||||
Ces champs existent déjà sur l'entité `User` (backend) et dans le DTO `UserData`/`UserWrite` (frontend). La persistance se fait via l'API user (`PATCH /api/users/{id}`).
|
||||
|
||||
On veut séparer ces deux préoccupations : le `UserDrawer` ne décide plus que **si un utilisateur est un employé** ; l'édition des informations RH se fait dans un espace dédié, dans le module Absences.
|
||||
|
||||
## Objectifs
|
||||
|
||||
- `UserDrawer` : ne conserver que la case à cocher « Employé ».
|
||||
- `team-absences` : ajouter un onglet « Employés » (la page est déjà `middleware: ["admin"]`, donc admin-only).
|
||||
- L'onglet liste les utilisateurs marqués `isEmployee` avec leurs soldes de congés.
|
||||
- Un drawer dédié permet d'éditer les informations RH d'un employé.
|
||||
|
||||
Hors périmètre : création d'utilisateur (reste dans l'admin), modification du flag `isEmployee` ailleurs que dans le `UserDrawer`, backend (déjà en place).
|
||||
|
||||
## Composants
|
||||
|
||||
### 1. `frontend/components/user/UserDrawer.vue`
|
||||
|
||||
- Supprimer le bloc détaillé employé (`hireDate`, `endDate`, `contractType`, `familySituation`, `workTimeRatio`, `annualLeaveDays`, `referencePeriodStart`, `initialLeaveBalance`, `nbChildren`).
|
||||
- Conserver **uniquement** la case `isEmployee` (« Employé (soumis à la gestion des absences) »).
|
||||
- Le payload de sauvegarde du user **n'envoie plus** les champs détaillés, pour ne pas les écraser. Il continue d'envoyer `isEmployee` et les champs de compte existants.
|
||||
- Nettoyer l'état du formulaire et les imports devenus inutiles (options contrat / situation familiale si elles ne servent plus que là).
|
||||
|
||||
### 2. `frontend/pages/team-absences.vue` — onglet « Employés »
|
||||
|
||||
- Ajouter un 4ᵉ onglet dans `tabs` : `{ key: 'employees', label: t('absences.admin.tabs.employees'), icon: 'mdi:account-group' }`.
|
||||
- Slot `#employees` : `MalioDataTable` avec les colonnes **Nom · Contrat · CP pris · CP restants**.
|
||||
- Clic sur une ligne → ouvre `EmployeeDrawer` avec l'utilisateur sélectionné (cohérent avec l'onglet Demandes qui ouvre le détail au clic ligne).
|
||||
- Chargement des données :
|
||||
- `usersService.getAll()` filtré sur `isEmployee === true` ;
|
||||
- `absenceService.getBalances({ type: 'cp' })` → map par `user.id` pour récupérer `taken` (CP pris) et `available` (CP restants) ;
|
||||
- fusion en lignes de tableau (`taken`/`available` à `—` si pas de solde CP pour l'employé).
|
||||
- Recharger la liste après `saved` du drawer.
|
||||
|
||||
### 3. `frontend/components/absence/EmployeeDrawer.vue` (nouveau)
|
||||
|
||||
- Props : `modelValue: boolean`, `user: UserData | null`.
|
||||
- Events : `update:modelValue`, `saved`.
|
||||
- Formulaire en **composants Malio** :
|
||||
- `MalioDate` : `hireDate`, `endDate` (valeurs ISO `YYYY-MM-DD`) ;
|
||||
- `MalioSelect` : `contractType` (CDI/CDD/Stage/Alternance/Autre), `familySituation` (Célibataire/Marié/Pacsé/Divorcé/Veuf) ;
|
||||
- `MalioInputText` : `workTimeRatio`, `annualLeaveDays`, `referencePeriodStart` (MM-DD), `initialLeaveBalance`, `nbChildren`.
|
||||
- À l'ouverture, initialiser le formulaire depuis `props.user` ; remettre à jour si `user` change.
|
||||
- Sauvegarde : `usersService.update(user.id, { …champs employé })` ; à la réussite, émettre `saved` et fermer.
|
||||
- En-tête : nom de l'employé.
|
||||
|
||||
### 4. i18n (`frontend/i18n/locales/fr.json`)
|
||||
|
||||
Nouvelles clés regroupées sous `absences.admin.employees.*` (et l'onglet sous `absences.admin.tabs.employees`) :
|
||||
- onglet : `absences.admin.tabs.employees` = « Employés » ;
|
||||
- colonnes liste : `absences.admin.employees.columns.{name,contract,cpTaken,cpRemaining}` ;
|
||||
- drawer : `absences.admin.employees.drawer.title` + libellés de champs sous `absences.admin.employees.fields.*`.
|
||||
|
||||
Les libellés de contrat et de situation familiale (CDI/CDD/…, Célibataire/Marié/…) actuellement codés en dur dans `UserDrawer` sont déplacés avec leur drawer ; on les passe en clés i18n à cette occasion.
|
||||
|
||||
## Flux de données
|
||||
|
||||
```
|
||||
UserDrawer --PATCH isEmployee--> User (backend)
|
||||
|
|
||||
team-absences (onglet Employés) |
|
||||
getAll() ∩ isEmployee <-----------+
|
||||
getBalances({type:'cp'}) --> map user.id -> {taken, available}
|
||||
=> lignes tableau (nom, contrat, CP pris, CP restants)
|
||||
|
|
||||
clic ligne
|
||||
v
|
||||
EmployeeDrawer(user) --usersService.update--> User (backend)
|
||||
|
|
||||
@saved --> recharge liste
|
||||
```
|
||||
|
||||
## Découpage / responsabilités
|
||||
|
||||
- `UserDrawer` : compte + flag employé. Ne connaît plus le détail RH.
|
||||
- Onglet Employés : vue dérivée en lecture (jointure users ⋈ soldes), aiguille vers le drawer.
|
||||
- `EmployeeDrawer` : seule unité qui édite les champs RH ; interface claire (`user` en entrée, `saved` en sortie), testable isolément.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `UserDrawer` : ouvrir un user, vérifier qu'il ne reste que la case « Employé », cocher/décocher et sauvegarder sans perte des autres champs RH (qui ne sont plus envoyés).
|
||||
- Onglet Employés : la liste affiche les users `isEmployee` avec CP pris/restants cohérents avec l'onglet Soldes.
|
||||
- `EmployeeDrawer` : éditer un employé (dates en JJ/MM/AAAA, selects FR), sauvegarder, vérifier la persistance (recharge) et l'absence d'erreurs console.
|
||||
- Vérification navigateur via Chrome DevTools sur les trois écrans.
|
||||
Reference in New Issue
Block a user