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
@@ -1,49 +1,43 @@
<template>
<AppDrawer v-model="drawerOpen" title="Export des heures">
<MalioDrawer v-model="drawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Export des heures</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="hours-export-date">
Date <span class="text-red-600">*</span>
</label>
<input
id="hours-export-date"
v-model="selectedDate"
type="date"
class="mt-2 w-full rounded-md border border-black px-3 py-2 text-md text-neutral-900"
>
</div>
<MalioDate
v-model="selectedDate"
label="Date"
required
:clearable="false"
:reserve-message-space="false"
group-class="w-full"
/>
<div>
<label class="text-md font-semibold text-neutral-700">
Sites <span class="text-red-600">*</span>
</label>
<MalioSelectCheckbox
v-model="selectedSites"
:options="siteOptions"
groupClass="w-full mt-2"
label="Sites"
display-select-all
display-tag
/>
</div>
<MalioSelectCheckbox
v-model="selectedSites"
:options="siteOptions"
label="Sites"
required
:reserve-message-space="false"
groupClass="w-full"
display-select-all
display-tag
/>
<div class="flex justify-center pt-2">
<button
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 disabled:opacity-50 disabled:cursor-not-allowed"
<MalioButton
:label="isLoading ? 'Génération en cours...' : 'Exporter'"
button-class="w-[200px]"
:disabled="isLoading || !selectedDate || selectedSites.length === 0"
>
<template v-if="isLoading">Génération en cours...</template>
<template v-else>Exporter</template>
</button>
@click="handleSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import AppDrawer from '~/components/AppDrawer.vue'
import type { Site } from '~/services/dto/site'
const props = defineProps<{
+56 -10
View File
@@ -3,7 +3,7 @@
<!-- Desktop: filters row -->
<div class="hidden lg:flex lg:items-center lg:gap-4">
<div v-if="sites.length > 0 && isAdmin" class="relative z-50 w-80">
<MalioSelectCheckbox
<MalioSelectCheckbox :reserve-message-space="false"
v-model="selectedSiteIds"
:options="siteOptions"
groupClass="w-80"
@@ -11,8 +11,8 @@
display-select-all
/>
</div>
<div v-if="isAdmin" class="w-80">
<MalioInputText
<div v-if="isAdmin" class="w-96">
<MalioInputText :reserve-message-space="false"
v-model="employeeFilter"
label="Recherche d'un employé"
icon-name="mdi:magnify"
@@ -23,7 +23,7 @@
<!-- Mobile: search + filter button -->
<div v-if="isAdmin" class="flex items-center gap-2 lg:hidden">
<div class="flex-1 min-w-0">
<MalioInputText
<MalioInputText :reserve-message-space="false"
v-model="employeeFilter"
label="Recherche d'un employé"
icon-name="mdi:magnify"
@@ -39,12 +39,15 @@
</div>
<!-- Mobile filters drawer -->
<AppDrawer v-model="filtersDrawerOpen" title="Filtres">
<MalioDrawer v-model="filtersDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Filtres</h2>
</template>
<div class="space-y-6">
<div v-if="sites.length > 0 && isAdmin">
<label class="text-md font-semibold text-neutral-700">Sites</label>
<div class="mt-2">
<MalioSelectCheckbox
<MalioSelectCheckbox :reserve-message-space="false"
v-model="selectedSiteIds"
:options="siteOptions"
groupClass="w-80"
@@ -77,11 +80,11 @@
</div>
</div>
</div>
</AppDrawer>
</MalioDrawer>
<!-- Date navigation -->
<div class="flex flex-col gap-3 lg:flex-row lg:justify-between lg:items-center lg:gap-4">
<div class="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:gap-4">
<div class="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:items-center lg:gap-4">
<div
v-if="viewMode === 'day'"
class="inline-flex h-10 w-full overflow-hidden rounded-md border border-primary-500 bg-white lg:w-[320px]"
@@ -142,10 +145,33 @@
</button>
</div>
<!-- Vue Jour (opt-in) : calendrier Malio avec jours validés en vert (markedDates). -->
<MalioDate
v-if="viewMode === 'day' && showValidationCalendar"
:model-value="selectedDate"
:clearable="false"
:reserve-message-space="false"
:marked-dates="markedDates"
group-class="w-full lg:w-96"
label="Date"
@update:model-value="onDatePicked"
@month-change="(payload) => emit('month-change', payload)"
/>
<!-- Vue Semaine : sélecteur de semaine Malio. -->
<MalioDateWeek
v-else-if="viewMode === 'week'"
:model-value="pickerValue"
:clearable="false"
:reserve-message-space="false"
group-class="w-full lg:w-96"
label="Semaine"
@update:model-value="onWeekPicked"
/>
<PeriodStepperPicker
v-else
width-class="w-full lg:w-[320px]"
:label="formattedSelectedDate"
:picker-type="viewMode === 'week' ? 'week' : 'date'"
picker-type="date"
:picker-value="pickerValue"
prev-aria-label="Période précédente"
next-aria-label="Période suivante"
@@ -195,7 +221,6 @@
import type { Site } from '~/services/dto/site'
import type { AbsenceType } from '~/services/dto/absence-type'
import PeriodStepperPicker from '~/components/PeriodStepperPicker.vue'
import AppDrawer from '~/components/AppDrawer.vue'
import { weekInputValueToYmd, ymdToWeekInputValue } from '~/utils/date'
const selectedDate = defineModel<string>('selectedDate', { required: true })
@@ -208,6 +233,10 @@ const props = defineProps<{
sites: Site[]
absenceTypes: AbsenceType[]
formattedSelectedDate: string
// Calendrier des jours validés (vert) : opt-in, réservé à l'écran Heures.
// L'écran Heures Conducteurs ne le passe pas → garde le PeriodStepperPicker.
showValidationCalendar?: boolean
markedDates?: Record<string, 'success' | 'danger'>
shortcutButtonClass: (target: 'yesterday' | 'today' | 'tomorrow') => string
weekShortcutButtonClass: (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => string
getWeekShortcutLabel: (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => string
@@ -223,6 +252,7 @@ const emit = defineEmits<{
(e: 'set-this-week'): void
(e: 'set-next-week'): void
(e: 'shift-date', value: number): void
(e: 'month-change', value: { month: number; year: number }): void
}>()
const filtersDrawerOpen = ref(false)
@@ -252,4 +282,20 @@ const onPickerValue = (value: string) => {
selectedDate.value = value
}
// Sélection d'un jour dans le calendrier MalioDate (vue Jour). `clearable=false`
// → pas de null en pratique, mais on garde la garde par sécurité.
const onDatePicked = (value: string | null) => {
if (!value) return
selectedDate.value = value
}
// Sélection d'une semaine dans MalioDateWeek (vue Semaine) : v-model au format ISO
// week (YYYY-Www) → on repositionne selectedDate sur le lundi de cette semaine.
const onWeekPicked = (value: string | null) => {
if (!value) return
const ymd = weekInputValueToYmd(value)
if (!ymd) return
selectedDate.value = ymd
}
</script>
@@ -1,9 +1,12 @@
<template>
<MalioDrawer v-model="drawerOpen" title="Commentaire">
<MalioDrawer v-model="drawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Commentaire</h2>
</template>
<form class="space-y-4" @submit.prevent="onSave">
<div v-if="employeeLabel" class="text-md font-semibold text-neutral-700">{{ employeeLabel }}</div>
<div class="text-md font-semibold text-neutral-700">{{ formatWeekRange }}</div>
<MalioInputTextArea
<MalioInputTextArea :reserve-message-space="false"
v-model="content"
label="Commentaire"
:size="8"
@@ -11,17 +14,18 @@
:show-counter="true"
resize="vertical"
/>
<div class="sticky bottom-0 -mx-[20px] bg-white px-[20px] py-3 flex justify-between gap-3">
<div class="sticky bottom-0 -mx-[20px] bg-white px-[20px] py-3 flex gap-3">
<MalioButton
v-if="commentId"
label="Supprimer"
variant="danger"
button-class="flex-1"
:disabled="isSubmitting"
@click="onDelete"
/>
<MalioButton
label="Enregistrer"
button-class="ml-auto"
button-class="flex-1"
:disabled="isSubmitting || !canSubmit"
@click="onSave"
/>