Compare commits

..

3 Commits

Author SHA1 Message Date
a75311e67e fix : malio UI 2026-04-27 14:07:22 +02:00
5302586644 feat(leave) : align forfait CP N + display remaining workdays + N-1 absences exempt from presence
- LeaveRecapRowBuilder: forfait CP N now reflects remainingDays (acquis − pris depuis N) instead of the constant acquiredDays. Affects both PDF export and screen recap.
- Employee detail header: forfait label becomes "Forfait - 218 jours (X restants)" where X = 218 − presence days from Jan 1 to today (today included).
- New EmployeeLeaveSummary.presenceDaysToToday field, computed via the same logic as presenceDaysByMonth but capped at today.
- Forfait business rule: leaves attributed to N-1 stock no longer decrement presence days. Implemented by chronologically consuming an N-1 budget (= previousYearTakenDays) inside computePresenceDaysByMonth before counting any absence. Non-forfait unaffected (budget is 0).
- Doc updates: CLAUDE.md (forfait/N-1 rule), functional-rules.md (CP N forfait semantics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:20:23 +02:00
bd93c52197 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>
2026-04-24 16:54:38 +02:00
4 changed files with 184 additions and 84 deletions

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.97' app.version: '0.1.95'

View File

@@ -7,7 +7,7 @@
"name": "frontend", "name": "frontend",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@malio/layer-ui": "^1.4.6", "@malio/layer-ui": "^1.4.5",
"@nuxt/icon": "^2.2.1", "@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.1", "@nuxtjs/i18n": "^10.2.1",
"@pinia/nuxt": "^0.11.3", "@pinia/nuxt": "^0.11.3",
@@ -2222,9 +2222,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@malio/layer-ui": { "node_modules/@malio/layer-ui": {
"version": "1.4.6", "version": "1.4.5",
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.4.6/layer-ui-1.4.6.tgz", "resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.4.5/layer-ui-1.4.5.tgz",
"integrity": "sha512-stHqUAJ8E6a62Ka7QXlE177GhkIsjtmYNa/tNk1TVpbJ099okfLLivrlofEl7CCAqDeMaIepnW4q0vxJT+EFEA==", "integrity": "sha512-UfVkLJk3WWGoZE1eyei0pY45IUbZRzabJr2X6GNFabHd/8EmuXqwP+LxCl8wEAO4ODrNKsVLJdi0eL3Zekv4Dg==",
"dependencies": { "dependencies": {
"@nuxt/icon": "^2.2.1", "@nuxt/icon": "^2.2.1",
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -13,7 +13,7 @@
"dependencies": { "dependencies": {
"@nuxt/icon": "^2.2.1", "@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.1", "@nuxtjs/i18n": "^10.2.1",
"@malio/layer-ui": "^1.4.6", "@malio/layer-ui": "^1.4.5",
"@pinia/nuxt": "^0.11.3", "@pinia/nuxt": "^0.11.3",
"nuxt": "^4.3.0", "nuxt": "^4.3.0",
"nuxt-toast": "^1.4.0", "nuxt-toast": "^1.4.0",

View File

@@ -84,53 +84,105 @@
</NuxtLink> </NuxtLink>
</div> </div>
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle"> <AppDrawer v-model="isDrawerOpen" :title="drawerTitle">
<form class="space-y-4" @submit.prevent="handleSubmit"> <form class="space-y-4" @submit.prevent="handleSubmit">
<MalioInputText <div>
v-model="form.firstName" <label class="text-md font-semibold text-neutral-700" for="first-name">
label="Prénom *" Prénom <span class="text-red-600">*</span>
group-class="mt-2" </label>
:error="showFirstNameError ? 'Le prénom est obligatoire.' : ''" <input
/> id="first-name"
<MalioInputText v-model="form.firstName"
v-model="form.lastName" type="text"
label="Nom *" :class="firstNameFieldClass"
group-class="mt-2" />
:error="showLastNameError ? 'Le nom est obligatoire.' : ''" <p v-if="showFirstNameError" class="mt-1 text-sm text-red-600">
/> Le prénom est obligatoire.
<MalioSelect </p>
:model-value="form.siteId === '' ? null : form.siteId" </div>
:options="formSiteOptions" <div>
label="Site *" <label class="text-md font-semibold text-neutral-700" for="last-name">
min-width="" Nom <span class="text-red-600">*</span>
:error="showSiteError ? 'Le site est obligatoire.' : ''" </label>
@update:model-value="(v) => { form.siteId = v === null ? '' : Number(v) }" <input
/> id="last-name"
v-model="form.lastName"
type="text"
:class="lastNameFieldClass"
/>
<p v-if="showLastNameError" class="mt-1 text-sm text-red-600">
Le nom est obligatoire.
</p>
</div>
<div>
<label class="text-md font-semibold text-neutral-700" for="site">
Site <span class="text-red-600">*</span>
</label>
<select
id="site"
v-model="form.siteId"
:class="siteFieldClass"
>
<option value="">Aucun site</option>
<option v-for="site in sites" :key="site.id" :value="site.id">
{{ site.name }}
</option>
</select>
<p v-if="showSiteError" class="mt-1 text-sm text-red-600">
Le site est obligatoire.
</p>
</div>
<template v-if="!editingEmployee"> <template v-if="!editingEmployee">
<MalioSelect <div>
:model-value="form.contractNature" <label class="text-md font-semibold text-neutral-700" for="contract-nature">
:options="contractNatureFormOptions" Type de contrat <span class="text-red-600">*</span>
label="Type de contrat *" </label>
min-width="" <select
:error="showContractNatureError ? 'Le type de contrat est obligatoire.' : ''" id="contract-nature"
@update:model-value="(v) => { if (v !== null) form.contractNature = v as 'CDI' | 'CDD' | 'INTERIM' }" v-model="form.contractNature"
/> :class="contractNatureFieldClass"
<MalioSelect >
v-if="form.contractNature === 'INTERIM'" <option value="CDI">CDI</option>
:model-value="form.interimAgencyId === '' ? null : form.interimAgencyId" <option value="CDD">CDD</option>
:options="interimAgencyOptions" <option value="INTERIM">Intérim</option>
label="Agence d'intérim" </select>
min-width="" <p v-if="showContractNatureError" class="mt-1 text-sm text-red-600">
@update:model-value="(v) => { form.interimAgencyId = v === null ? '' : Number(v) }" Le type de contrat est obligatoire.
/> </p>
<MalioSelect </div>
:model-value="form.contractId === '' ? null : form.contractId" <div v-if="form.contractNature === 'INTERIM'">
:options="contractFormOptions" <label class="text-md font-semibold text-neutral-700" for="interim-agency">
label="Temps de travail *" Agence d'intérim
min-width="" </label>
:error="showContractError ? 'Le temps de travail est obligatoire.' : ''" <select
@update:model-value="(v) => { form.contractId = v === null ? '' : Number(v) }" id="interim-agency"
/> v-model="form.interimAgencyId"
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
>
<option value="">Aucune</option>
<option v-for="agency in interimAgencies" :key="agency.id" :value="agency.id">
{{ agency.name }}
</option>
</select>
</div>
<div>
<label class="text-md font-semibold text-neutral-700" for="contract">
Temps de travail <span class="text-red-600">*</span>
</label>
<select
id="contract"
v-model="form.contractId"
:class="contractFieldClass"
>
<option value="">Sélectionner un contrat</option>
<option v-for="contract in contracts" :key="contract.id" :value="contract.id">
{{ contract.name }}
</option>
</select>
<p v-if="showContractError" class="mt-1 text-sm text-red-600">
Le temps de travail est obligatoire.
</p>
</div>
<div> <div>
<label class="text-md font-semibold text-neutral-700" for="contract-start-date"> <label class="text-md font-semibold text-neutral-700" for="contract-start-date">
Début contrat <span class="text-red-600">*</span> Début contrat <span class="text-red-600">*</span>
@@ -139,7 +191,7 @@
id="contract-start-date" id="contract-start-date"
v-model="form.contractStartDate" v-model="form.contractStartDate"
type="date" type="date"
:class="[dateInputBaseClass, form.contractStartDate ? 'border-black' : 'border-m-muted', showContractStartDateError ? '!border-m-danger' : '']" :class="contractStartDateFieldClass"
/> />
<p v-if="showContractStartDateError" class="mt-1 text-sm text-red-600"> <p v-if="showContractStartDateError" class="mt-1 text-sm text-red-600">
La date de début est obligatoire. La date de début est obligatoire.
@@ -154,18 +206,22 @@
id="contract-end-date" id="contract-end-date"
v-model="form.contractEndDate" v-model="form.contractEndDate"
type="date" type="date"
:class="[dateInputBaseClass, form.contractEndDate ? 'border-black' : 'border-m-muted', showContractEndDateError ? '!border-m-danger' : '']" :class="contractEndDateFieldClass"
/> />
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600"> <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. La date de fin est obligatoire pour un CDD ou un Intérim.
</p> </p>
</div> </div>
<div class="flex h-10 items-center rounded-md border border-neutral-200 bg-neutral-50 px-3"> <div class="rounded-md border border-neutral-200 bg-neutral-50 p-3">
<MalioCheckbox <label class="inline-flex items-center gap-2 text-md font-semibold text-neutral-700" for="is-driver">
v-model="form.isDriver" <input
label="Chauffeur" id="is-driver"
group-class="flex items-center" v-model="form.isDriver"
/> type="checkbox"
class="h-4 w-4 rounded border-neutral-300 text-primary-500 focus:ring-primary-500"
/>
Chauffeur
</label>
</div> </div>
<WorkDaysHoursInput <WorkDaysHoursInput
v-if="requiresSchedule" v-if="requiresSchedule"
@@ -174,19 +230,23 @@
/> />
</template> </template>
<div class="flex justify-end gap-3 pt-2"> <div class="flex justify-end gap-3 pt-2">
<MalioButton <button
label="Annuler" type="button"
variant="tertiary" class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
@click="isDrawerOpen = false" @click="isDrawerOpen = false"
/> >
<MalioButton Annuler
</button>
<button
type="submit" type="submit"
label="Enregistrer" class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
:disabled="isSubmitting || !isFormValid" :class="submitButtonClass"
/> >
Enregistrer
</button>
</div> </div>
</form> </form>
</MalioDrawer> </AppDrawer>
<MalioDrawer v-model="isExportDrawerOpen" title="Export"> <MalioDrawer v-model="isExportDrawerOpen" title="Export">
<div class="space-y-4"> <div class="space-y-4">
@@ -432,23 +492,63 @@ const showContractEndDateError = computed(
() => !editingEmployee.value && validationTouched.contractEndDate && !isContractEndDateValid.value () => !editingEmployee.value && validationTouched.contractEndDate && !isContractEndDateValid.value
) )
const dateInputBaseClass = const baseInputClass =
'mt-2 h-10 w-full rounded-md border px-3 text-md text-black outline-none focus:border-2 focus:border-m-primary' 'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20'
const firstNameFieldClass = computed(() => {
if (showFirstNameError.value) {
return `${baseInputClass} border-red-500`
}
return `${baseInputClass} border-neutral-300`
})
const lastNameFieldClass = computed(() => {
if (showLastNameError.value) {
return `${baseInputClass} border-red-500`
}
return `${baseInputClass} border-neutral-300`
})
const siteFieldClass = computed(() => {
const baseSelectClass =
'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
if (showSiteError.value) {
return `${baseSelectClass} border-red-500`
}
return `${baseSelectClass} border-neutral-300`
})
const contractFieldClass = computed(() => {
const baseClass =
'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
if (showContractError.value) {
return `${baseClass} border-red-500`
}
return `${baseClass} border-neutral-300`
})
const contractNatureFieldClass = computed(() => {
const baseClass =
'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
if (showContractNatureError.value) {
return `${baseClass} border-red-500`
}
return `${baseClass} border-neutral-300`
})
const contractStartDateFieldClass = computed(() => {
if (showContractStartDateError.value) {
return `${baseInputClass} border-red-500`
}
return `${baseInputClass} border-neutral-300`
})
const contractEndDateFieldClass = computed(() => {
if (showContractEndDateError.value) {
return `${baseInputClass} border-red-500`
}
return `${baseInputClass} border-neutral-300`
})
const formSiteOptions = computed(() => const submitButtonClass = computed(() => {
sites.value.map((site) => ({ label: site.name, value: site.id })) if (isSubmitting.value || !isFormValid.value) {
) return 'opacity-50 cursor-not-allowed'
const interimAgencyOptions = computed(() => }
interimAgencies.value.map((agency) => ({ label: agency.name, value: agency.id })) return ''
) })
const contractFormOptions = computed(() =>
contracts.value.map((contract) => ({ label: contract.name, value: contract.id }))
)
const contractNatureFormOptions = [
{ label: 'CDI', value: 'CDI' },
{ label: 'CDD', value: 'CDD' },
{ label: 'Intérim', value: 'INTERIM' }
]
const loadEmployees = async () => { const loadEmployees = async () => {
isLoading.value = true isLoading.value = true