feat(heures) : calendrier des jours validés (vue Jour) + harmonisation Malio UI

- Calendrier MalioDate en vue Jour (Heures + Heures Conducteurs) : jours
  entièrement validés (admin) peints en vert. Endpoint GET
  /work-hours/validation-status?from=&to=[&driver=1] (scope conducteur inversé),
  chargement à la volée par mois, refresh après validation/saisie/absence.
- Suite à @malio/layer-ui 1.7.11 : reserveMessageSpace=false sur les champs ;
  tous les drawers migrés sur MalioDrawer (titre via slot #header, AppDrawer
  custom supprimé) ; boutons d'action en MalioButton (deux boutons partagent
  l'espace) ; inputs date en MalioDate ; MalioDateWeek en vue Semaine.
- Boutons d'ajout uniformisés sur « Ajouter » + icône.
- .env : EXCLUDED_PUBLIC_HOLIDAYS="null".
- Doc : doc/hours-validated-days.md, documentation-content.ts, CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 15:47:23 +02:00
parent 5d2b5d1c54
commit 34dc52d92b
37 changed files with 1881 additions and 495 deletions
+7 -4
View File
@@ -3,7 +3,7 @@
<div class="flex items-center justify-between pb-6">
<h1 class="text-4xl font-bold text-primary-500">Types de statut</h1>
<MalioButton
label="Ajouter un type"
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
@click="openCreate"
@@ -55,16 +55,19 @@
</div>
</div>
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
<MalioDrawer v-model="isDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">{{ drawerTitle }}</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="form.code"
label="Code *"
group-class="mt-2"
:max-length="10"
:error="showCodeError ? 'Le code est obligatoire.' : ''"
/>
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="form.label"
label="Libellé *"
group-class="mt-2"
+3 -3
View File
@@ -5,7 +5,7 @@
</div>
<div class="flex flex-col gap-3 py-6">
<div class="flex items-center justify-between gap-4">
<MalioSelectCheckbox
<MalioSelectCheckbox :reserve-message-space="false"
v-model="selectedSiteIds"
:options="siteOptions"
label="Sites"
@@ -14,7 +14,7 @@
/>
<div class="flex gap-4">
<MalioButton
label="Ajouter une absence"
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
@click="openCreateFromToday"
@@ -31,7 +31,7 @@
<div class="flex justify-between">
<div class="flex items-center gap-4">
<div class="w-80">
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="employeeFilter"
label="Recherche d'un employé"
icon-name="mdi:magnify"
+5
View File
@@ -13,6 +13,8 @@
:sites="sites"
:absence-types="absenceTypes"
:formatted-selected-date="formattedSelectedDate"
:show-validation-calendar="true"
:marked-dates="markedDates"
:shortcut-button-class="shortcutButtonClass"
:week-shortcut-button-class="weekShortcutButtonClass"
:get-week-shortcut-label="getWeekShortcutLabel"
@@ -23,6 +25,7 @@
@set-this-week="setThisWeek"
@set-next-week="setNextWeek"
@shift-date="shiftDate"
@month-change="onCalendarMonthChange"
/>
<div v-if="isLoading" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
@@ -193,6 +196,8 @@ const {
isSelectedDateHoliday,
selectedHolidayLabel,
handleSave,
markedDates,
onCalendarMonthChange,
isWeekCommentDrawerOpen,
weekCommentContext,
openWeekCommentDrawer,
+1 -1
View File
@@ -36,7 +36,7 @@
</div>
</div>
<div v-if="showPicker" class="mt-3 flex items-center gap-3">
<MalioSelect
<MalioSelect :reserve-message-space="false"
label="Contrat"
:model-value="selectedPhase?.id ?? null"
:options="phaseOptions"
+48 -53
View File
@@ -12,7 +12,7 @@
@click="openExportDrawer"
/>
<MalioButton
label="Ajouter un employé"
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
@click="openCreate"
@@ -21,14 +21,14 @@
</div>
<div class="flex items-center gap-3 py-7">
<div class="w-80">
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="employeeFilter"
label="Recherche d'un employé"
icon-name="mdi:magnify"
/>
</div>
<div v-if="sites.length > 0" class="relative z-50 w-80">
<MalioSelectCheckbox
<MalioSelectCheckbox :reserve-message-space="false"
v-model="selectedSiteIds"
:options="siteOptions"
groupClass="w-80"
@@ -37,7 +37,7 @@
/>
</div>
<MalioSelect
<MalioSelect :reserve-message-space="false"
v-model="contractStatusFilter"
label="Statut contrat"
:options="contractStatusOptions"
@@ -84,21 +84,24 @@
</NuxtLink>
</div>
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
<MalioDrawer v-model="isDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">{{ drawerTitle }}</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="form.firstName"
label="Prénom *"
group-class="mt-2"
:error="showFirstNameError ? 'Le prénom est obligatoire.' : ''"
/>
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="form.lastName"
label="Nom *"
group-class="mt-2"
:error="showLastNameError ? 'Le nom est obligatoire.' : ''"
/>
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="form.siteId === '' ? null : form.siteId"
:options="formSiteOptions"
label="Site *"
@@ -107,7 +110,7 @@
@update:model-value="(v) => { form.siteId = v === null ? '' : Number(v) }"
/>
<template v-if="!editingEmployee">
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="form.contractNature"
:options="contractNatureFormOptions"
label="Type de contrat *"
@@ -115,7 +118,7 @@
:error="showContractNatureError ? 'Le type de contrat est obligatoire.' : ''"
@update:model-value="(v) => { if (v !== null) form.contractNature = v as 'CDI' | 'CDD' | 'INTERIM' }"
/>
<MalioSelect
<MalioSelect :reserve-message-space="false"
v-if="form.contractNature === 'INTERIM'"
:model-value="form.interimAgencyId === '' ? null : form.interimAgencyId"
:options="interimAgencyOptions"
@@ -123,7 +126,7 @@
min-width=""
@update:model-value="(v) => { form.interimAgencyId = v === null ? '' : Number(v) }"
/>
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="form.contractId === '' ? null : form.contractId"
:options="contractFormOptions"
label="Temps de travail *"
@@ -131,37 +134,27 @@
:error="showContractError ? 'Le temps de travail est obligatoire.' : ''"
@update:model-value="(v) => { form.contractId = v === null ? '' : Number(v) }"
/>
<div>
<label class="text-md font-semibold text-neutral-700" for="contract-start-date">
Début contrat <span class="text-red-600">*</span>
</label>
<input
id="contract-start-date"
v-model="form.contractStartDate"
type="date"
:class="[dateInputBaseClass, form.contractStartDate ? 'border-black' : 'border-m-muted', showContractStartDateError ? '!border-m-danger' : '']"
/>
<p v-if="showContractStartDateError" class="mt-1 text-sm text-red-600">
La date de début est obligatoire.
</p>
</div>
<div v-if="showsContractEndDateComputed">
<label class="text-md font-semibold text-neutral-700" for="contract-end-date">
Fin contrat
<span v-if="requiresContractEndDateComputed" class="text-red-600">*</span>
</label>
<input
id="contract-end-date"
v-model="form.contractEndDate"
type="date"
:class="[dateInputBaseClass, form.contractEndDate ? 'border-black' : 'border-m-muted', showContractEndDateError ? '!border-m-danger' : '']"
/>
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">
La date de fin est obligatoire pour un CDD ou un Intérim.
</p>
</div>
<MalioDate
:model-value="form.contractStartDate"
label="Début contrat"
required
:reserve-message-space="false"
:error="showContractStartDateError ? 'La date de début est obligatoire.' : ''"
group-class="w-full"
@update:model-value="(v) => form.contractStartDate = v ?? ''"
/>
<MalioDate
v-if="showsContractEndDateComputed"
:model-value="form.contractEndDate"
label="Fin contrat"
:required="requiresContractEndDateComputed"
:reserve-message-space="false"
:error="showContractEndDateError ? 'La date de fin est obligatoire pour un CDD ou un Intérim.' : ''"
group-class="w-full"
@update:model-value="(v) => form.contractEndDate = v ?? ''"
/>
<div class="flex h-10 items-center rounded-md border border-neutral-200 bg-neutral-50 px-3">
<MalioCheckbox
<MalioCheckbox :reserve-message-space="false"
v-model="form.isDriver"
label="Chauffeur"
group-class="flex items-center"
@@ -173,24 +166,29 @@
:contract-weekly-hours="selectedContract?.weeklyHours ?? null"
/>
</template>
<div class="flex justify-end gap-3 pt-2">
<div class="grid grid-cols-2 gap-3 pt-2">
<MalioButton
label="Annuler"
variant="tertiary"
button-class="w-full"
@click="isDrawerOpen = false"
/>
<MalioButton
type="submit"
label="Enregistrer"
button-class="w-full"
:disabled="isSubmitting || !isFormValid"
@click="handleSubmit"
/>
</div>
</form>
</MalioDrawer>
<MalioDrawer v-model="isExportDrawerOpen" title="Export">
<MalioDrawer v-model="isExportDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Export</h2>
</template>
<div class="space-y-4">
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="exportChoice === '' ? null : exportChoice"
:options="exportTypeOptions"
label="Type d'export"
@@ -213,14 +211,14 @@
</div>
<template v-else-if="exportChoice === 'yearly-hours'">
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="exportYear"
:options="exportYearOptions"
label="Année *"
min-width=""
@update:model-value="(v) => { if (v !== null) exportYear = Number(v) }"
/>
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="exportMonth === '' ? null : exportMonth"
:options="exportMonthOptions"
label="Mois *"
@@ -231,7 +229,7 @@
</template>
<div v-else-if="exportChoice === 'night-contingent'">
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="exportYear"
:options="exportYearOptions"
label="Année *"
@@ -241,14 +239,14 @@
</div>
<div v-else-if="exportChoice === 'overtime-contingent'" class="flex flex-col gap-4">
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="exportYear"
:options="exportYearOptions"
label="Année *"
min-width=""
@update:model-value="(v) => { if (v !== null) exportYear = Number(v) }"
/>
<MalioSelectCheckbox
<MalioSelectCheckbox :reserve-message-space="false"
v-model="exportSiteIds"
:options="siteOptions"
label="Sites"
@@ -467,9 +465,6 @@ const showContractEndDateError = computed(
() => !editingEmployee.value && validationTouched.contractEndDate && !isContractEndDateValid.value
)
const dateInputBaseClass =
'mt-2 h-10 w-full rounded-md border px-3 text-md text-black outline-none focus:border-2 focus:border-m-primary'
const formSiteOptions = computed(() =>
sites.value.map((site) => ({ label: site.name, value: site.id }))
)
+5
View File
@@ -29,6 +29,8 @@
:sites="sites"
:absence-types="absenceTypes"
:formatted-selected-date="formattedSelectedDate"
:show-validation-calendar="true"
:marked-dates="markedDates"
:shortcut-button-class="shortcutButtonClass"
:week-shortcut-button-class="weekShortcutButtonClass"
:get-week-shortcut-label="getWeekShortcutLabel"
@@ -39,6 +41,7 @@
@set-this-week="setThisWeek"
@set-next-week="setNextWeek"
@shift-date="shiftDate"
@month-change="onCalendarMonthChange"
/>
<div v-if="isLoading" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
@@ -225,6 +228,8 @@ const {
closeAbsenceDrawer,
formatMinutes,
handleSave,
markedDates,
onCalendarMonthChange,
isWeekCommentDrawerOpen,
weekCommentContext,
openWeekCommentDrawer,
+2 -2
View File
@@ -9,14 +9,14 @@
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
@submit.prevent="handleSubmit"
>
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="username"
label="Nom d'utilisateur"
autocomplete="username"
group-class="mt-2"
/>
<MalioInputPassword
<MalioInputPassword :reserve-message-space="false"
v-model="password"
label="Mot de passe"
autocomplete="current-password"
+6 -3
View File
@@ -3,7 +3,7 @@
<div class="flex items-center justify-between pb-6">
<h1 class="text-4xl font-bold text-primary-500">Sites</h1>
<MalioButton
label="Ajouter un site"
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
@click="openCreate"
@@ -51,9 +51,12 @@
</div>
</div>
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
<MalioDrawer v-model="isDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">{{ drawerTitle }}</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="form.name"
label="Nom *"
group-class="mt-2"
+9 -7
View File
@@ -94,10 +94,12 @@
<MalioDrawer
v-model="isDrawerOpen"
:title="editingUser ? 'Modifier un utilisateur' : 'Ajouter un utilisateur'"
>
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">{{ editingUser ? 'Modifier un utilisateur' : 'Ajouter un utilisateur' }}</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="form.username"
:label="editingUser ? `Nom d'utilisateur` : `Nom d'utilisateur *`"
group-class="mt-2"
@@ -105,7 +107,7 @@
/>
<div>
<MalioInputPassword
<MalioInputPassword :reserve-message-space="false"
v-model="form.password"
:label="editingUser ? 'Mot de passe' : 'Mot de passe *'"
:hint="editingUser ? 'Laisse vide pour ne pas changer le mot de passe.' : ''"
@@ -153,7 +155,7 @@
</div>
<div v-if="form.accessMode === 'self'">
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="form.employeeId === '' ? null : form.employeeId"
:options="employeeOptions"
label="Employé lié"
@@ -172,7 +174,7 @@
:key="site.id"
class="flex h-10 items-center rounded-md border border-neutral-200 px-3"
>
<MalioCheckbox
<MalioCheckbox :reserve-message-space="false"
:model-value="form.siteIds.includes(site.id)"
:label="site.name"
group-class="flex items-center"
@@ -186,7 +188,7 @@
</div>
<div>
<MalioCheckbox
<MalioCheckbox :reserve-message-space="false"
v-model="form.isLocked"
label="Verrouiller le compte"
hint="Un compte verrouillé ne peut plus se connecter."
@@ -194,7 +196,7 @@
</div>
<div>
<MalioCheckbox
<MalioCheckbox :reserve-message-space="false"
v-model="form.hasLeaveRecapAccess"
label="Accès à l'écran Récap. congés"
hint="Affiche l'onglet dans la sidebar et donne accès au tableau récap."