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

@@ -14,7 +14,7 @@
<span class="font-normal text-neutral-600 text-sm">({{ contractLabel(employee) }})</span>
</p>
<p class="text-sm text-neutral-500 truncate">
{{ employee.site?.name ?? 'Sans site' }}<span v-if="employee.currentContractNature"> {{ contractNatureLabel(employee.currentContractNature) }}</span>
{{ employee.site?.name ?? 'Sans site' }}<span v-if="getRowContractNature(employee.id)"> {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}</span>
</p>
</div>
@@ -212,7 +212,7 @@
</p>
<p class="text-neutral-500 truncate inline-flex items-center gap-2">
<span>
{{ employee.site?.name ?? 'Sans site' }}<span v-if="employee.currentContractNature"> {{ contractNatureLabel(employee.currentContractNature) }}</span>
{{ employee.site?.name ?? 'Sans site' }}<span v-if="getRowContractNature(employee.id)"> {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}</span>
</span>
<span
v-if="isAdmin && (rows[employee.id]?.isSiteValid ?? false)"
@@ -405,6 +405,7 @@ const props = defineProps<{
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
hasRowFormation: (employeeId: number) => boolean
getRowFormationLabel: (employeeId: number) => string
getRowContractNature: (employeeId: number) => 'CDI' | 'CDD' | 'INTERIM' | null
getRowUpdatedAt: (employeeId: number) => string
getPresenceDayValue: (employeeId: number) => string
onAbsenceClick: (employeeId: number) => void

View File

@@ -1,17 +1,32 @@
<template>
<div class="py-4 flex flex-col gap-3 lg:py-6">
<!-- Desktop: filters row -->
<div class="hidden lg:flex lg:gap-4">
<SiteFilterSelector v-if="sites.length > 0 && isAdmin" v-model="selectedSiteIds" :sites="sites" />
<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
v-model="selectedSiteIds"
:options="siteOptions"
label="Sites"
display-select-all
/>
</div>
<div v-if="isAdmin" class="w-80">
<EmployeeNameFilterInput v-model="employeeFilter" />
<MalioInputText
v-model="employeeFilter"
label="Recherche d'un employé"
icon-name="mdi:magnify"
/>
</div>
</div>
<!-- Mobile: search + filter button -->
<div v-if="isAdmin" class="flex gap-2 lg:hidden">
<div v-if="isAdmin" class="flex items-center gap-2 lg:hidden">
<div class="flex-1 min-w-0">
<EmployeeNameFilterInput v-model="employeeFilter" />
<MalioInputText
v-model="employeeFilter"
label="Recherche d'un employé"
icon-name="mdi:magnify"
/>
</div>
<button
type="button"
@@ -28,7 +43,12 @@
<div v-if="sites.length > 0 && isAdmin">
<label class="text-md font-semibold text-neutral-700">Sites</label>
<div class="mt-2">
<SiteFilterSelector v-model="selectedSiteIds" :sites="sites" />
<MalioSelectCheckbox
v-model="selectedSiteIds"
:options="siteOptions"
label="Sites"
display-select-all
/>
</div>
</div>
<div v-if="isAdmin">
@@ -172,8 +192,6 @@
<script setup lang="ts">
import type { Site } from '~/services/dto/site'
import type { AbsenceType } from '~/services/dto/absence-type'
import EmployeeNameFilterInput from '~/components/EmployeeNameFilterInput.vue'
import SiteFilterSelector from '~/components/SiteFilterSelector.vue'
import PeriodStepperPicker from '~/components/PeriodStepperPicker.vue'
import AppDrawer from '~/components/AppDrawer.vue'
import { weekInputValueToYmd, ymdToWeekInputValue } from '~/utils/date'
@@ -183,7 +201,7 @@ const viewMode = defineModel<'day' | 'week'>('viewMode', { required: true })
const selectedSiteIds = defineModel<number[]>('selectedSiteIds', { required: true })
const employeeFilter = defineModel<string>('employeeFilter', { required: true })
defineProps<{
const props = defineProps<{
isAdmin: boolean
sites: Site[]
absenceTypes: AbsenceType[]
@@ -193,6 +211,8 @@ defineProps<{
getWeekShortcutLabel: (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => string
}>()
const siteOptions = computed(() => props.sites.map((site) => ({ label: site.name, value: site.id })))
const emit = defineEmits<{
(e: 'set-yesterday'): void
(e: 'set-today'): void