feat(heures) : calendrier des jours validés (vue Jour) + harmonisation Malio UI (#30)
Auto Tag Develop / tag (push) Successful in 10s

## Fonctionnel
- Calendrier MalioDate en vue Jour (écrans Heures ET Heures Conducteurs) : les jours entièrement validés par un admin sont peints en vert.
  - Endpoint `GET /work-hours/validation-status?from=&to=[&driver=1]` (scope conducteur inversé via `driver=1`), périmètre complet (ignore le filtre sites).
  - Chargement à la volée par mois (event `@month-change`), refresh après validation / saisie / absence.

## Harmonisation @malio/layer-ui 1.7.11
- `reserveMessageSpace=false` sur tous les champs (alignement).
- Tous les drawers migrés sur `MalioDrawer` (titre via slot `#header`, `AppDrawer` custom supprimé).
- Boutons d'action en `MalioButton` ; deux boutons côte à côte partagent l'espace.
- Inputs date en `MalioDate`, sélecteur semaine en `MalioDateWeek`.
- Boutons d'ajout uniformisés sur « Ajouter » + icône.

## Divers
- `.env` : `EXCLUDED_PUBLIC_HOLIDAYS="null"`.
- Doc : `doc/hours-validated-days.md`, `documentation-content.ts`, `CLAUDE.md`.
- Tests : provider `WorkHourValidationStatus` (suite complète 236/236 OK via pre-commit hook).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #30
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #30.
This commit is contained in:
2026-06-16 13:53:03 +00:00
committed by Autin
parent 5d2b5d1c54
commit 74abecbe03
37 changed files with 1881 additions and 495 deletions
+34 -43
View File
@@ -1,7 +1,10 @@
<template>
<AppDrawer v-model="drawerOpen" title="Nouvelle absence">
<MalioDrawer v-model="drawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Nouvelle absence</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="absenceForm.employeeId === '' ? null : absenceForm.employeeId"
:options="employeeOptions"
label="Employé *"
@@ -12,7 +15,7 @@
@update:model-value="onEmployeeChange"
/>
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="absenceForm.typeId === '' ? null : absenceForm.typeId"
:options="typeOptions"
label="Type d'absence *"
@@ -24,16 +27,16 @@
<div class="space-y-4">
<div>
<label class="text-md font-semibold text-neutral-700" for="start-date">Début</label>
<div class="mt-2 grid grid-cols-2 gap-2">
<input
id="start-date"
<label class="text-md font-semibold text-neutral-700">Début</label>
<div class="mt-2 space-y-2">
<MalioDate
v-model="absenceForm.startDate"
type="date"
:class="[dateInputBaseClass, absenceForm.startDate ? 'border-black' : 'border-m-muted']"
:clearable="false"
:reserve-message-space="false"
:disabled="props.lockDates"
group-class="w-full"
/>
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="absenceForm.startHalf"
:options="halfDayOptions"
min-width=""
@@ -42,16 +45,16 @@
</div>
</div>
<div>
<label class="text-md font-semibold text-neutral-700" for="end-date">Fin</label>
<div class="mt-2 grid grid-cols-2 gap-2">
<input
id="end-date"
<label class="text-md font-semibold text-neutral-700">Fin</label>
<div class="mt-2 space-y-2">
<MalioDate
v-model="absenceForm.endDate"
type="date"
:class="[dateInputBaseClass, absenceForm.endDate ? 'border-black' : 'border-m-muted']"
:clearable="false"
:reserve-message-space="false"
:disabled="props.lockDates"
group-class="w-full"
/>
<MalioSelect
<MalioSelect :reserve-message-space="false"
:model-value="absenceForm.endHalf"
:options="halfDayOptions"
min-width=""
@@ -72,31 +75,30 @@
</div>
<div v-if="editingAbsence" class="grid grid-cols-2 gap-3 pt-2">
<button
type="button"
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
<MalioButton
label="Supprimer"
variant="danger"
button-class="w-full"
@click="handleDelete"
>
Supprimer
</button>
<button
type="submit"
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
:class="submitButtonClass"
>
Modifier
</button>
/>
<MalioButton
label="Modifier"
variant="primary"
button-class="w-full"
:disabled="props.isSubmitting || !isFormValid"
@click="handleSubmit"
/>
</div>
<div v-else class="flex justify-center pt-2">
<MalioButton
type="submit"
label="Valider"
button-class="w-[200px]"
:disabled="props.isSubmitting || !isFormValid"
@click="handleSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</template>
<script setup lang="ts">
@@ -106,7 +108,6 @@ import type { AbsenceType } from '~/services/dto/absence-type'
import type { Absence } from '~/services/dto/absence'
import type { HalfDay } from '~/services/dto/half-day'
import { HALF_DAYS } from '~/services/dto/half-day'
import AppDrawer from '~/components/AppDrawer.vue'
const props = defineProps<{
modelValue: boolean
@@ -159,13 +160,6 @@ const showTypeError = computed(
() => validationTouched.type && !isTypeValid.value
)
const submitButtonClass = computed(() => {
if (props.isSubmitting || !isFormValid.value) {
return 'opacity-50 cursor-not-allowed'
}
return ''
})
const employeeOptions = computed(() =>
props.employees.map((e) => ({ label: `${e.firstName} ${e.lastName}`, value: e.id }))
)
@@ -174,9 +168,6 @@ const typeOptions = computed(() =>
)
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)
}
+30 -55
View File
@@ -1,36 +1,28 @@
<template>
<AppDrawer v-model="drawerOpen" title="Imprimer les absences">
<MalioDrawer v-model="drawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Imprimer les absences</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-md font-semibold text-neutral-700" for="print-from">
Date de début <span class="text-red-600">*</span>
</label>
<input
id="print-from"
v-model="printForm.from"
type="date"
:class="fromFieldClass"
/>
<p v-if="showFromError" class="mt-1 text-sm text-red-600">
La date de début est obligatoire.
</p>
</div>
<div>
<label class="text-md font-semibold text-neutral-700" for="print-to">
Date de fin <span class="text-red-600">*</span>
</label>
<input
id="print-to"
v-model="printForm.to"
type="date"
:class="toFieldClass"
/>
<p v-if="showToError" class="mt-1 text-sm text-red-600">
La date de fin est obligatoire.
</p>
</div>
</div>
<MalioDate
v-model="printForm.from"
label="Date de début"
required
:clearable="false"
:reserve-message-space="false"
:error="showFromError ? 'La date de début est obligatoire.' : ''"
group-class="w-full"
/>
<MalioDate
v-model="printForm.to"
label="Date de fin"
required
:clearable="false"
:reserve-message-space="false"
:error="showToError ? 'La date de fin est obligatoire.' : ''"
group-class="w-full"
/>
<div class="space-y-2">
<p class="text-md font-semibold text-neutral-700">
@@ -97,21 +89,19 @@
</div>
<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"
:class="submitButtonClass"
>
Imprimer
</button>
<MalioButton
label="Imprimer"
variant="primary"
:button-class="submitButtonClass"
@click="handleSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</template>
<script setup lang="ts">
import { computed, reactive, toRef, watch } from 'vue'
import AppDrawer from '~/components/AppDrawer.vue'
type SiteOption = {
id: number
@@ -190,21 +180,6 @@ const showSitesError = computed(() => validationTouched.sites && !isSitesValid.v
const showContractNaturesError = computed(() => validationTouched.contractNatures && !isContractNaturesValid.value)
const showWorkContractsError = computed(() => validationTouched.workContracts && !isWorkContractsValid.value)
const baseInputClass =
'mt-2 w-full rounded-md border px-3 py-2 text-md text-neutral-900'
const fromFieldClass = computed(() => {
if (showFromError.value) {
return `${baseInputClass} border-red-500`
}
return `${baseInputClass} border-neutral-300`
})
const toFieldClass = computed(() => {
if (showToError.value) {
return `${baseInputClass} border-red-500`
}
return `${baseInputClass} border-neutral-300`
})
const submitButtonClass = computed(() => {
if (!isFormValid.value) {
return 'opacity-50 cursor-not-allowed'
-56
View File
@@ -1,56 +0,0 @@
<template>
<div v-if="modelValue" class="fixed inset-0 z-[60]">
<Transition name="drawer-backdrop">
<div class="absolute inset-0 bg-black/40" @click="close" />
</Transition>
<Transition name="drawer-panel">
<div class="absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-xl flex flex-col">
<div class="shrink-0 flex items-center justify-between px-[20px] pt-8 pb-8">
<h2 class="text-[32px] font-semibold text-primary-500">
{{ title }}
</h2>
<button
type="button"
class="rounded-md p-1 text-primary-500 hover:text-secondary-500"
@click="close"
>
<Icon name="mdi:close" size="24"/>
</button>
</div>
<div class="min-h-0 flex-1 overflow-y-auto px-[20px] pb-4 pt-1">
<slot />
</div>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ modelValue: boolean; title?: string }>()
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
const close = () => emit('update:modelValue', false)
</script>
<style scoped>
.drawer-backdrop-enter-active,
.drawer-backdrop-leave-active {
transition: opacity 0.2s ease;
}
.drawer-backdrop-enter-from,
.drawer-backdrop-leave-to {
opacity: 0;
}
.drawer-panel-enter-active,
.drawer-panel-leave-active {
transition: transform 0.2s ease, opacity 0.2s ease;
}
.drawer-panel-enter-from,
.drawer-panel-leave-to {
transform: translateX(100%);
opacity: 0;
}
</style>
+10 -14
View File
@@ -1,5 +1,8 @@
<template>
<AppDrawer v-model="drawerOpen" title="Export heures (tous les employés)">
<MalioDrawer v-model="drawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Export heures (tous les employés)</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="bulk-yearly-hours-year">
@@ -29,26 +32,19 @@
</div>
<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...' : 'Imprimer'"
button-class="w-[200px]"
:disabled="isLoading || selectedMonth === ''"
>
<template v-if="isLoading">
Génération en cours...
</template>
<template v-else>
Imprimer
</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'
const props = defineProps<{
modelValue: boolean
@@ -1,5 +1,8 @@
<template>
<AppDrawer v-model="drawerOpen" title="Export heures">
<MalioDrawer v-model="drawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Export heures</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="yearly-hours-year">
@@ -29,20 +32,18 @@
</div>
<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"
>
Imprimer
</button>
<MalioButton
label="Imprimer"
button-class="w-[200px]"
@click="handleSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import AppDrawer from '~/components/AppDrawer.vue'
const props = defineProps<{
modelValue: boolean
+10 -17
View File
@@ -1,5 +1,8 @@
<template>
<AppDrawer v-model="drawerOpen" title="Récapitulatif Salaire">
<MalioDrawer v-model="drawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Récapitulatif Salaire</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="salary-recap-month">
@@ -17,21 +20,18 @@
</div>
<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"
:class="submitButtonClass"
>
Imprimer
</button>
<MalioButton
label="Imprimer"
button-class="w-[200px]"
@click="handleSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import AppDrawer from '~/components/AppDrawer.vue'
const props = defineProps<{
modelValue: boolean
@@ -63,13 +63,6 @@ const monthFieldClass = computed(() => {
return `${baseInputClass} border-neutral-300`
})
const submitButtonClass = computed(() => {
if (!isMonthValid.value) {
return 'opacity-50 cursor-not-allowed'
}
return ''
})
const handleSubmit = () => {
validationTouched.value = true
if (!isMonthValid.value) return
+24 -21
View File
@@ -33,7 +33,10 @@
</button>
</div>
<AppDrawer v-model="isDrawerOpen" :title="isEditing ? 'Modification prime' : 'Nouvelle prime'">
<MalioDrawer v-model="isDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">{{ isEditing ? 'Modification prime' : 'Nouvelle prime' }}</h2>
</template>
<form class="space-y-4" @submit.prevent="onSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="bonus-month">
@@ -75,38 +78,38 @@
</div>
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
<button
type="button"
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
<MalioButton
label="Supprimer"
variant="danger"
button-class="w-full"
@click="onDelete"
>
Supprimer
</button>
<button
type="submit"
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
/>
<MalioButton
label="Modifier"
variant="primary"
button-class="w-full"
:disabled="!isFormValid"
>
Modifier
</button>
@click="onSubmit"
/>
</div>
<div v-else 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:cursor-not-allowed disabled:opacity-50"
<MalioButton
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
variant="primary"
button-class="w-[200px]"
:disabled="!isFormValid"
>
+ Ajouter
</button>
@click="onSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</section>
</template>
<script setup lang="ts">
import type { Bonus } from '~/services/dto/bonus'
import AppDrawer from '~/components/AppDrawer.vue'
const props = defineProps<{
bonuses: Bonus[]
+33 -28
View File
@@ -43,7 +43,10 @@
</button>
</div>
<AppDrawer :model-value="isContractDrawerOpen" title="Modifier le contrat" @update:model-value="onUpdateContractDrawerOpen">
<MalioDrawer :model-value="isContractDrawerOpen" @update:model-value="onUpdateContractDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Modifier le contrat</h2>
</template>
<div class="mb-4 flex border-b border-neutral-200">
<button
type="button"
@@ -141,13 +144,12 @@
</div>
<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:cursor-not-allowed disabled:opacity-50"
<MalioButton
label="Modifier"
button-class="w-[200px]"
:disabled="isContractSubmitting || !isContractEndDateValid"
>
Modifier
</button>
@click="onSubmitCloseContract"
/>
</div>
</form>
</div>
@@ -188,27 +190,29 @@
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
/>
</div>
<button
type="button"
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-60"
<MalioButton
:label="form.id ? 'Modifier' : 'Ajouter'"
button-class="w-full"
:disabled="!form.startDate || isSuspensionSubmitting"
@click="onSubmitSuspension(index)"
>
{{ form.id ? 'Modifier' : 'Ajouter' }}
</button>
/>
</div>
<button
type="button"
class="w-full rounded-md border-2 border-dashed border-primary-500/50 px-4 py-3 text-base font-semibold text-primary-500/50 transition hover:border-primary-500 hover:text-primary-500"
<MalioButton
label="Ajouter une suspension"
icon-name="mdi:plus"
icon-position="left"
variant="tertiary"
button-class="w-full"
@click="onAddSuspensionForm"
>
+ Ajouter une suspension
</button>
/>
</div>
</AppDrawer>
</MalioDrawer>
<AppDrawer :model-value="isCreateContractDrawerOpen" title="Ajouter un contrat" @update:model-value="onUpdateCreateContractDrawerOpen">
<MalioDrawer :model-value="isCreateContractDrawerOpen" @update:model-value="onUpdateCreateContractDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Ajouter un contrat</h2>
</template>
<form class="space-y-4" @submit.prevent="onSubmitCreateContract">
<div>
<label class="text-md font-semibold text-neutral-700" for="create-contract-nature">
@@ -282,16 +286,17 @@
/>
<div class="sticky bottom-0 -mx-[20px] bg-white px-[20px] py-3 flex justify-center">
<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:cursor-not-allowed disabled:opacity-50"
<MalioButton
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
button-class="w-[200px]"
:disabled="isCreateContractSubmitting || !isCreateContractFormValid"
>
+ Ajouter
</button>
@click="onSubmitCreateContract"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</section>
</template>
+24 -21
View File
@@ -47,7 +47,10 @@
</button>
</div>
<AppDrawer v-model="isDrawerOpen" title="Formation">
<MalioDrawer v-model="isDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Formation</h2>
</template>
<form class="space-y-4" @submit.prevent="onSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="formation-start-date">
@@ -107,39 +110,39 @@
</div>
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
<button
type="button"
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
<MalioButton
label="Supprimer"
variant="danger"
button-class="w-full"
@click="onDelete"
>
Supprimer
</button>
<button
type="submit"
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
/>
<MalioButton
label="Modifier"
variant="primary"
button-class="w-full"
:disabled="!isFormValid"
>
Modifier
</button>
@click="onSubmit"
/>
</div>
<div v-else 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:cursor-not-allowed disabled:opacity-50"
<MalioButton
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
variant="primary"
button-class="w-[200px]"
:disabled="!isFormValid"
>
+ Ajouter
</button>
@click="onSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</section>
</template>
<script setup lang="ts">
import type {Formation} from '~/services/dto/formation'
import {getFormationJustificatifUrl} from '~/services/formations'
import AppDrawer from '~/components/AppDrawer.vue'
const props = defineProps<{
formations: Formation[]
+32 -31
View File
@@ -111,7 +111,10 @@
</select>
</div>
</div>
<AppDrawer v-model="isFractionedDrawerOpen" title="Jours fractionnés">
<MalioDrawer v-model="isFractionedDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Jours fractionnés</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmitFractioned">
<div>
<label class="text-md font-semibold text-neutral-700" for="fractioned-days">
@@ -127,24 +130,25 @@
/>
</div>
<div class="flex justify-end gap-3 pt-2">
<button
type="button"
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
<div class="grid grid-cols-2 gap-3 pt-2">
<MalioButton
label="Annuler"
variant="tertiary"
button-class="w-full"
@click="isFractionedDrawerOpen = false"
>
Annuler
</button>
<button
type="submit"
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
>
Enregistrer
</button>
/>
<MalioButton
label="Enregistrer"
button-class="w-full"
@click="handleSubmitFractioned"
/>
</div>
</form>
</AppDrawer>
<AppDrawer v-model="isPaidLeaveDrawerOpen" title="Congés N-1 payés">
</MalioDrawer>
<MalioDrawer v-model="isPaidLeaveDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Congés N-1 payés</h2>
</template>
<form class="space-y-4" @submit.prevent="handleSubmitPaidLeave">
<div>
<label class="text-md font-semibold text-neutral-700" for="paid-leave-days">
@@ -160,23 +164,21 @@
/>
</div>
<div class="flex justify-end gap-3 pt-2">
<button
type="button"
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
<div class="grid grid-cols-2 gap-3 pt-2">
<MalioButton
label="Annuler"
variant="tertiary"
button-class="w-full"
@click="isPaidLeaveDrawerOpen = false"
>
Annuler
</button>
<button
type="submit"
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
>
Enregistrer
</button>
/>
<MalioButton
label="Enregistrer"
button-class="w-full"
@click="handleSubmitPaidLeave"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</section>
</template>
@@ -184,7 +186,6 @@
import type {Absence} from '~/services/dto/absence'
import type {EmployeeLeaveSummary} from '~/services/dto/employee-leave-summary'
import {normalizeDate, toYmd} from '~/utils/date'
import AppDrawer from '~/components/AppDrawer.vue'
type DayLeaveState = {
am: boolean
+24 -21
View File
@@ -64,7 +64,10 @@
</div>
<AppDrawer v-model="isDrawerOpen" title="Frais">
<MalioDrawer v-model="isDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Frais</h2>
</template>
<form class="space-y-4" @submit.prevent="onSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="mileage-month">
@@ -157,39 +160,39 @@
</div>
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
<button
type="button"
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
<MalioButton
label="Supprimer"
variant="danger"
button-class="w-full"
@click="onDelete"
>
Supprimer
</button>
<button
type="submit"
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
/>
<MalioButton
label="Modifier"
variant="primary"
button-class="w-full"
:disabled="!isFormValid"
>
Modifier
</button>
@click="onSubmit"
/>
</div>
<div v-else 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:cursor-not-allowed disabled:opacity-50"
<MalioButton
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
variant="primary"
button-class="w-[200px]"
:disabled="!isFormValid"
>
+ Ajouter
</button>
@click="onSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</section>
</template>
<script setup lang="ts">
import type {MileageAllowance} from '~/services/dto/mileage-allowance'
import {getKmReceiptUrl, getAmountReceiptUrl} from '~/services/mileage-allowances'
import AppDrawer from '~/components/AppDrawer.vue'
const props = defineProps<{
allowances: MileageAllowance[]
@@ -31,7 +31,10 @@
</button>
</div>
<AppDrawer v-model="isDrawerOpen" :title="isEditing ? 'Modification observation' : 'Nouvelle observation'">
<MalioDrawer v-model="isDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">{{ isEditing ? 'Modification observation' : 'Nouvelle observation' }}</h2>
</template>
<form class="space-y-4" @submit.prevent="onSubmit">
<div>
<label class="text-md font-semibold text-neutral-700" for="observation-month">
@@ -59,38 +62,38 @@
</div>
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
<button
type="button"
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
<MalioButton
label="Supprimer"
variant="danger"
button-class="w-full"
@click="onDelete"
>
Supprimer
</button>
<button
type="submit"
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
/>
<MalioButton
label="Modifier"
variant="primary"
button-class="w-full"
:disabled="!isFormValid"
>
Modifier
</button>
@click="onSubmit"
/>
</div>
<div v-else 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:cursor-not-allowed disabled:opacity-50"
<MalioButton
label="Ajouter"
icon-name="mdi:plus"
icon-position="left"
variant="primary"
button-class="w-[200px]"
:disabled="!isFormValid"
>
+ Ajouter
</button>
@click="onSubmit"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</section>
</template>
<script setup lang="ts">
import type { Observation } from '~/services/dto/observation'
import AppDrawer from '~/components/AppDrawer.vue'
const props = defineProps<{
observations: Observation[]
+16 -16
View File
@@ -203,7 +203,10 @@
</div>
<!-- Payment Drawer -->
<AppDrawer v-model="isPaymentDrawerOpen" title="Payer des RTT">
<MalioDrawer v-model="isPaymentDrawerOpen">
<template #header>
<h2 class="text-[32px] font-semibold text-primary-500">Payer des RTT</h2>
</template>
<form @submit.prevent="onSubmitPayment">
<div class="mb-4">
<label class="block text-sm font-medium text-neutral-700">Mois</label>
@@ -254,30 +257,27 @@
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
/>
</div>
<div class="flex justify-end gap-3">
<button
type="button"
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
<div class="grid grid-cols-2 gap-3 pt-2">
<MalioButton
label="Annuler"
variant="tertiary"
button-class="w-full"
@click="isPaymentDrawerOpen = false"
>
Annuler
</button>
<button
type="submit"
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-medium text-white hover:bg-primary-600"
>
Enregistrer
</button>
/>
<MalioButton
label="Enregistrer"
button-class="w-full"
@click="onSubmitPayment"
/>
</div>
</form>
</AppDrawer>
</MalioDrawer>
</section>
</template>
<script setup lang="ts">
import type { EmployeeRttSummary, EmployeeRttWeekSummary } from '~/services/dto/employee-rtt-summary'
import type { ContractPhase } from '~/services/dto/contract-phase'
import AppDrawer from '~/components/AppDrawer.vue'
type RttYearOption = {
value: number
@@ -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"
/>