feat : migrate filter/form UI to @malio/layer-ui + fix hours/calendar contract scoping

- Add @malio/layer-ui as Nuxt layer (extends in nuxt.config.ts)
- Migrate site/employee/contract filters to MalioSelectCheckbox / MalioInputText / MalioSelect on employees, calendar and hours screens
- Migrate absence drawer selects + submit button to Malio (MalioSelect + MalioButton)
- Migrate calendar "Ajouter une absence" / "Imprimer" actions to MalioButton
- Drop now-unused EmployeeNameFilterInput and SiteFilterSelector components
- Hours day view: resolve contract nature at selected date (WorkHourDayContext.contractNature) instead of employee.currentContractNature (today-based); fixes interim contracts showing as CDI when mission ended
- Calendar: hide employees whose contract periods do not intersect the displayed month
- Layout: scrollbar-gutter:stable on <main> to avoid horizontal shift when dropdowns open
- Update functional-rules.md, in-app documentation, CLAUDE.md to match

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 16:54:38 +02:00
parent 90843dd997
commit bd93c52197
25 changed files with 309 additions and 368 deletions

View File

@@ -1,44 +1,26 @@
<template>
<AppDrawer v-model="drawerOpen" title="Nouvelle absence">
<form class="space-y-4" @submit.prevent="handleSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="employee">
Employé <span class="text-red-600">*</span>
</label>
<select
id="employee"
v-model="absenceForm.employeeId"
:class="employeeFieldClass"
:disabled="props.lockEmployee"
>
<option value="" disabled>Choisir un employé</option>
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
{{ employee.firstName }} {{ employee.lastName }}
</option>
</select>
<p v-if="showEmployeeError" class="mt-1 text-sm text-red-600">
L'employé est obligatoire.
</p>
</div>
<MalioSelect
:model-value="absenceForm.employeeId === '' ? null : absenceForm.employeeId"
:options="employeeOptions"
label="Employé *"
empty-option-label="Choisir un employé"
min-width=""
:disabled="props.lockEmployee"
:error="showEmployeeError ? `L'employé est obligatoire.` : ''"
@update:model-value="onEmployeeChange"
/>
<div>
<label class="text-md font-semibold text-neutral-700" for="type">
Type d'absence <span class="text-red-600">*</span>
</label>
<select
id="type"
v-model="absenceForm.typeId"
:class="typeFieldClass"
>
<option value="" disabled>Choisir un type</option>
<option v-for="type in absenceTypes" :key="type.id" :value="type.id">
{{ type.label }} ({{ type.code }})
</option>
</select>
<p v-if="showTypeError" class="mt-1 text-sm text-red-600">
Le type d'absence est obligatoire.
</p>
</div>
<MalioSelect
:model-value="absenceForm.typeId === '' ? null : absenceForm.typeId"
:options="typeOptions"
label="Type d'absence *"
empty-option-label="Choisir un type"
min-width=""
:error="showTypeError ? `Le type d'absence est obligatoire.` : ''"
@update:model-value="onTypeChange"
/>
<div class="space-y-4">
<div>
@@ -48,17 +30,15 @@
id="start-date"
v-model="absenceForm.startDate"
type="date"
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
:class="[dateInputBaseClass, absenceForm.startDate ? 'border-black' : 'border-m-muted']"
:disabled="props.lockDates"
/>
<select
v-model="absenceForm.startHalf"
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
>
<option v-for="half in HALF_DAYS" :key="half.value" :value="half.value">
{{ half.label }}
</option>
</select>
<MalioSelect
:model-value="absenceForm.startHalf"
:options="halfDayOptions"
min-width=""
@update:model-value="(v) => { if (v !== null) absenceForm.startHalf = v as HalfDay }"
/>
</div>
</div>
<div>
@@ -68,17 +48,15 @@
id="end-date"
v-model="absenceForm.endDate"
type="date"
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
:class="[dateInputBaseClass, absenceForm.endDate ? 'border-black' : 'border-m-muted']"
:disabled="props.lockDates"
/>
<select
v-model="absenceForm.endHalf"
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
>
<option v-for="half in HALF_DAYS" :key="half.value" :value="half.value">
{{ half.label }}
</option>
</select>
<MalioSelect
:model-value="absenceForm.endHalf"
:options="halfDayOptions"
min-width=""
@update:model-value="(v) => { if (v !== null) absenceForm.endHalf = v as HalfDay }"
/>
</div>
</div>
</div>
@@ -110,13 +88,12 @@
</button>
</div>
<div v-else class="flex justify-center pt-2">
<button
<MalioButton
type="submit"
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
:class="submitButtonClass"
>
+ Ajouter
</button>
label="Valider"
button-class="w-[200px]"
:disabled="props.isSubmitting || !isFormValid"
/>
</div>
</form>
</AppDrawer>
@@ -189,20 +166,23 @@ const submitButtonClass = computed(() => {
return ''
})
const baseSelectClass =
'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
const employeeFieldClass = computed(() => {
if (showEmployeeError.value) {
return `${baseSelectClass} border-red-500`
}
return `${baseSelectClass} border-neutral-300`
})
const typeFieldClass = computed(() => {
if (showTypeError.value) {
return `${baseSelectClass} border-red-500`
}
return `${baseSelectClass} border-neutral-300`
})
const employeeOptions = computed(() =>
props.employees.map((e) => ({ label: `${e.firstName} ${e.lastName}`, value: e.id }))
)
const typeOptions = computed(() =>
props.absenceTypes.map((t) => ({ label: `${t.label} (${t.code})`, value: t.id }))
)
const halfDayOptions = HALF_DAYS.map((h) => ({ label: h.label, value: h.value }))
const dateInputBaseClass =
'h-10 w-full rounded-md border px-3 text-md text-black outline-none focus:border-2 focus:border-m-primary'
const onEmployeeChange = (value: string | number | null) => {
absenceForm.value.employeeId = value === null ? '' : Number(value)
}
const onTypeChange = (value: string | number | null) => {
absenceForm.value.typeId = value === null ? '' : Number(value)
}
watch(
() => props.modelValue,