feat : sélecteur d'année sur l'onglet Congés de la fiche employé

Permet de consulter les exercices passés (calendrier + compteurs) sur
l'onglet Congés. La plage proposée est bornée par max(début historique
contrat, RTT_START_DATE) pour ne pas remonter avant la mise en service
du logiciel. Édition des stocks N-1 et fractionnés verrouillée sur
exercices clos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 09:51:19 +02:00
parent 3ec0d4b074
commit 7cadcfa362
10 changed files with 231 additions and 12 deletions

View File

@@ -39,6 +39,8 @@
</div>
<button
class="flex items-center"
:class="isHistoricalYear ? 'opacity-40 cursor-not-allowed' : ''"
:disabled="isHistoricalYear"
@click="openPaidLeaveDrawer"
>
<Icon name="mdi:edit-box" size="24"/>
@@ -51,6 +53,8 @@
</div>
<button
class="flex items-center"
:class="isHistoricalYear ? 'opacity-40 cursor-not-allowed' : ''"
:disabled="isHistoricalYear"
@click="openFractionedDrawer"
>
<Icon name="mdi:edit-box" size="24"/>
@@ -90,6 +94,22 @@
</div>
</div>
</div>
<div class="mt-6 flex items-center gap-3">
<label for="leave-year-select" class="text-md font-semibold text-primary-500 uppercase">
{{ isForfaitRule ? 'Année :' : 'Exercice :' }}
</label>
<select
id="leave-year-select"
:value="selectedYear ?? ''"
:disabled="!availableYears.length"
class="border border-primary-500 rounded-md px-3 py-1 text-md font-semibold text-primary-500 bg-white focus:outline-none focus:ring-2 focus:ring-secondary-500/20 disabled:opacity-50"
@change="handleYearChange"
>
<option v-for="option in availableYears" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
<AppDrawer v-model="isFractionedDrawerOpen" title="Jours fractionnés">
<form class="space-y-4" @submit.prevent="handleSubmitFractioned">
@@ -173,17 +193,39 @@ type DayLeaveState = {
colors: string[]
}
type LeaveYearOption = {
value: number
label: string
}
const props = defineProps<{
absences: Absence[]
summary: EmployeeLeaveSummary | null
publicHolidays: Record<string, string>
selectedYear: number | null
availableYears: LeaveYearOption[]
currentYear: number | null
}>()
const emit = defineEmits<{
(event: 'update-fractioned-days', days: number): void
(event: 'update-paid-leave-days', days: number): void
(event: 'update-selected-year', year: number): void
}>()
const isHistoricalYear = computed(() =>
props.selectedYear !== null
&& props.currentYear !== null
&& props.selectedYear !== props.currentYear
)
const handleYearChange = (event: Event) => {
const target = event.target as HTMLSelectElement
const value = Number(target.value)
if (Number.isNaN(value)) return
emit('update-selected-year', value)
}
const isFractionedDrawerOpen = ref(false)
const fractionedForm = reactive({days: 0})
@@ -239,6 +281,7 @@ const currentYearTakenDays = computed(() => {
})
const displayedYear = computed(() => {
if (props.selectedYear) return props.selectedYear
if (props.summary?.year) return props.summary.year
const today = new Date()
const year = today.getFullYear()

View File

@@ -7,27 +7,91 @@ import { listAbsences } from '~/services/absences'
import { getEmployeeLeaveSummary, updateFractionedDays, updatePaidLeaveDays } from '~/services/employee-leave-summary'
import { listPublicHolidays } from '~/services/public-holidays'
export type LeaveYearOption = {
value: number
label: string
}
export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
const employeeAbsences = ref<Absence[]>([])
const leaveSummary = ref<EmployeeLeaveSummary | null>(null)
const publicHolidays = ref<Record<string, string>>({})
const isLeaveLoading = ref(false)
const leaveDataLoaded = ref(false)
const selectedLeaveYear = ref<number | null>(null)
const getLeaveYear = () => {
const now = new Date()
const isForfait = employee.value?.contract?.type === CONTRACT_TYPES.FORFAIT
return isForfait
? now.getFullYear()
: (now.getMonth() >= 5 ? now.getFullYear() + 1 : now.getFullYear())
const isForfaitContract = (emp: Employee | null) =>
emp?.contract?.type === CONTRACT_TYPES.FORFAIT
const computeLeaveYearForDate = (emp: Employee | null, date: Date): number => {
if (isForfaitContract(emp)) return date.getFullYear()
return date.getMonth() >= 5 ? date.getFullYear() + 1 : date.getFullYear()
}
const currentLeaveYear = computed<number | null>(() => {
if (!employee.value) return null
return computeLeaveYearForDate(employee.value, new Date())
})
const formatLeaveYearLabel = (year: number, isForfait: boolean): string => {
if (isForfait) return String(year)
return `Juin ${year - 1} → Mai ${year}`
}
const availableLeaveYears = computed<LeaveYearOption[]>(() => {
if (!employee.value || currentLeaveYear.value === null) return []
const isForfait = isForfaitContract(employee.value)
const current = currentLeaveYear.value
const startDates: string[] = []
for (const period of employee.value.contractHistory ?? []) {
if (period.startDate) startDates.push(period.startDate)
}
if (employee.value.entryDate) startDates.push(employee.value.entryDate)
let contractFloor = current
for (const raw of startDates) {
const date = new Date(`${raw.substring(0, 10)}T00:00:00`)
if (Number.isNaN(date.getTime())) continue
const leaveYear = computeLeaveYearForDate(employee.value, date)
if (leaveYear < contractFloor) contractFloor = leaveYear
}
// Hard floor : data-start-date (env RTT_START_DATE) — le logiciel n'a pas
// d'historique avant cette date, inutile de proposer des années antérieures.
let dataFloor: number | null = null
const dataStart = leaveSummary.value?.dataStartDate
if (dataStart) {
const dataStartDate = new Date(`${dataStart.substring(0, 10)}T00:00:00`)
if (!Number.isNaN(dataStartDate.getTime())) {
dataFloor = computeLeaveYearForDate(employee.value, dataStartDate)
}
}
const minYear = dataFloor !== null ? Math.max(contractFloor, dataFloor) : contractFloor
const years: LeaveYearOption[] = []
for (let y = current; y >= minYear; y -= 1) {
years.push({ value: y, label: formatLeaveYearLabel(y, isForfait) })
}
return years
})
const initSelectedLeaveYear = () => {
if (selectedLeaveYear.value !== null) return
if (currentLeaveYear.value !== null) {
selectedLeaveYear.value = currentLeaveYear.value
}
}
const loadLeaveData = async () => {
if (!employee.value || isLeaveLoading.value) return
initSelectedLeaveYear()
if (selectedLeaveYear.value === null) return
isLeaveLoading.value = true
try {
const isForfait = employee.value.contract?.type === CONTRACT_TYPES.FORFAIT
const leaveYear = getLeaveYear()
const isForfait = isForfaitContract(employee.value)
const leaveYear = selectedLeaveYear.value
const from = isForfait ? `${leaveYear}-01-01` : `${leaveYear - 1}-06-01`
const to = isForfait ? `${leaveYear}-12-31` : `${leaveYear}-05-31`
const holidayYears = isForfait ? [leaveYear] : [leaveYear - 1, leaveYear]
@@ -46,8 +110,16 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
}
}
const setSelectedLeaveYear = async (year: number) => {
if (selectedLeaveYear.value === year) return
selectedLeaveYear.value = year
leaveDataLoaded.value = false
await loadLeaveData()
}
const resetLoaded = () => {
leaveDataLoaded.value = false
selectedLeaveYear.value = null
}
const submitFractionedDays = async (days: number) => {
@@ -70,6 +142,10 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
publicHolidays,
isLeaveLoading,
leaveDataLoaded,
selectedLeaveYear,
currentLeaveYear,
availableLeaveYears,
setSelectedLeaveYear,
loadLeaveData,
resetLoaded,
submitFractionedDays,

View File

@@ -457,6 +457,17 @@ export const documentationSections: DocSection[] = [
{ type: 'paragraph', content: 'Compteurs visibles sur l\'onglet Congé de la fiche employé : acquis, en cours d\'acquisition, pris, restant.' },
],
},
{
id: 'onglet-conges-fiche-employe',
title: 'Onglet Congés (fiche employé)',
requiredLevel: 'admin',
blocks: [
{ type: 'paragraph', content: 'L\'onglet "Congés" sur la fiche employé affiche un calendrier annuel des congés posés (12 mois en grille 4×3) ainsi que les compteurs (acquis, pris, reste, en cours d\'acquisition, N-1 ou samedis selon le contrat).' },
{ type: 'paragraph', content: 'La période affichée dépend du type de contrat actuel : Janvier → Décembre pour FORFAIT, Juin (N-1) → Mai (N) pour les autres contrats.' },
{ type: 'paragraph', content: 'Un sélecteur d\'année est disponible en bas du calendrier (zone scrollable, à gauche). Il permet de consulter les exercices passés. La plage proposée part de l\'exercice courant et remonte jusqu\'au plus récent entre (a) le premier exercice où l\'employé avait un contrat ouvert et (b) l\'exercice de mise en service du logiciel — il est inutile de remonter plus loin, aucune donnée n\'a été saisie avant.' },
{ type: 'note', content: 'Sur un exercice passé, les boutons d\'édition "Jours fractionnés" et "Année N-1 payés" sont désactivés. La consultation reste possible, mais on n\'autorise pas la modification rétroactive d\'un exercice clos.' },
],
},
{
id: 'ecran-recap-conges',
title: 'Écran Récap. congés',

View File

@@ -160,8 +160,12 @@
:absences="employeeAbsences"
:summary="leaveSummary"
:public-holidays="publicHolidays"
:selected-year="selectedLeaveYear"
:available-years="availableLeaveYears"
:current-year="currentLeaveYear"
@update-fractioned-days="submitFractionedDays"
@update-paid-leave-days="submitPaidLeaveDays"
@update-selected-year="setSelectedLeaveYear"
/>
</div>
<div v-else-if="showRttTab && activeTab === 'rtt'" class="h-full">
@@ -253,6 +257,10 @@ const {
leaveSummary,
rttSummary,
publicHolidays,
selectedLeaveYear,
currentLeaveYear,
availableLeaveYears,
setSelectedLeaveYear,
showLeaveTab,
showRttTab,
contractHistory,

View File

@@ -16,5 +16,6 @@ export type EmployeeLeaveSummary = {
previousYearPaidDays: number
presenceDaysByMonth: Record<string, number>
presenceDaysToToday: number
dataStartDate: string | null
}