Gestion du changement de type de contrat + correction du calcule des RTT sur un contrat qui commence en milieu de semaine (#19)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled

| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [x] CHANGELOG modifié

Reviewed-on: #19
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #19.
This commit is contained in:
2026-05-22 06:42:33 +00:00
committed by Autin
parent b541f9ded8
commit abdaf809f8
40 changed files with 5021 additions and 153 deletions

View File

@@ -28,7 +28,7 @@
<div class="flex justify-center">
<button
class="rounded-md bg-primary-500 px-8 py-2 font-bold text-white hover:bg-primary-600 disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="isHistoricalYear"
:disabled="isPayDisabled"
@click="openPaymentDrawer"
>
+ Payer les RTT
@@ -276,6 +276,7 @@
<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 = {
@@ -288,6 +289,7 @@ const props = defineProps<{
selectedYear: number | null
availableYears: RttYearOption[]
currentYear: number | null
selectedPhase: ContractPhase | null
}>()
const emit = defineEmits<{
@@ -301,6 +303,20 @@ const isHistoricalYear = computed(() =>
&& props.selectedYear !== props.currentYear
)
const isLastExerciseOfPhase = computed(() => {
if (!props.selectedPhase || props.selectedPhase.isCurrent) return false
if (!props.selectedPhase.endDate) return false
const endDate = new Date(`${props.selectedPhase.endDate}T00:00:00`)
const endYear = endDate.getMonth() >= 5
? endDate.getFullYear() + 1
: endDate.getFullYear()
return props.selectedYear === endYear
})
const isPayDisabled = computed(() =>
isHistoricalYear.value && !isLastExerciseOfPhase.value
)
const handleYearChange = (event: Event) => {
const target = event.target as HTMLSelectElement
const value = Number(target.value)

View File

@@ -0,0 +1,83 @@
import type { Ref } from 'vue'
import type { Employee } from '~/services/dto/employee'
import type { ContractPhase } from '~/services/dto/contract-phase'
import { CONTRACT_TYPES } from '~/services/dto/contract'
const formatDateFr = (iso: string | null): string => {
if (!iso) return ''
const [y, m, d] = iso.split('-')
return `${d}/${m}/${y}`
}
const formatContractTypeLabel = (phase: ContractPhase): string => {
switch (phase.contractType) {
case CONTRACT_TYPES.FORFAIT:
return 'FORFAIT'
case CONTRACT_TYPES.H35:
return '35h'
case CONTRACT_TYPES.H39:
return '39h'
case CONTRACT_TYPES.INTERIM:
return 'Intérim'
case CONTRACT_TYPES.CUSTOM:
return `CUSTOM (${phase.weeklyHours ?? '?'}h)`
default:
return String(phase.contractType)
}
}
export const formatPhaseLabel = (phase: ContractPhase): string => {
const base = formatContractTypeLabel(phase)
const driver = phase.isDriver ? ' (driver)' : ''
const dates = phase.endDate
? `${formatDateFr(phase.startDate)}${formatDateFr(phase.endDate)}`
: `depuis ${formatDateFr(phase.startDate)}`
const suffix = phase.isCurrent ? ' (actuel)' : ''
return `${base}${driver}${dates}${suffix}`
}
export const useEmployeeContractPhase = (employee: Ref<Employee | null>) => {
const selectedPhaseId = ref<number | null>(null)
const availablePhases = computed<ContractPhase[]>(() => employee.value?.contractPhases ?? [])
const currentPhase = computed<ContractPhase | null>(() => {
return availablePhases.value.find((p) => p.isCurrent) ?? availablePhases.value[0] ?? null
})
const selectedPhase = computed<ContractPhase | null>(() => {
if (selectedPhaseId.value === null) return currentPhase.value
return availablePhases.value.find((p) => p.id === selectedPhaseId.value) ?? currentPhase.value
})
const isViewingPastPhase = computed<boolean>(() => {
if (!selectedPhase.value || !currentPhase.value) return false
return selectedPhase.value.id !== currentPhase.value.id
})
const phaseOptions = computed(() =>
availablePhases.value.map((p) => ({ value: p.id, label: formatPhaseLabel(p) }))
)
const showPicker = computed(() => availablePhases.value.length > 1)
const setSelectedPhase = (phaseId: number) => {
selectedPhaseId.value = phaseId
}
const resetToCurrent = () => {
selectedPhaseId.value = null
}
return {
selectedPhaseId,
selectedPhase,
currentPhase,
availablePhases,
phaseOptions,
showPicker,
isViewingPastPhase,
setSelectedPhase,
resetToCurrent,
}
}

View File

@@ -1,6 +1,7 @@
import type { Employee } from '~/services/dto/employee'
import { CONTRACT_TYPES } from '~/services/dto/contract'
import { getEmployee } from '~/services/employees'
import { useEmployeeContractPhase } from '~/composables/useEmployeeContractPhase'
export const useEmployeeDetailPage = () => {
const route = useRoute()
@@ -8,13 +9,21 @@ export const useEmployeeDetailPage = () => {
const isLoading = ref(false)
const activeTab = ref<'contract' | 'leave' | 'rtt' | 'mileage' | 'formation' | 'bonus' | 'observation'>('contract')
const phase = useEmployeeContractPhase(employee)
const showLeaveTab = computed(() => employee.value?.currentContractNature !== 'INTERIM')
const showRttTab = computed(() => employee.value?.contract?.type !== CONTRACT_TYPES.FORFAIT)
const isForfait = computed(() => employee.value?.contract?.type === CONTRACT_TYPES.FORFAIT)
const showRttTab = computed(() => phase.selectedPhase.value?.contractType !== CONTRACT_TYPES.FORFAIT)
const isForfait = computed(() => phase.selectedPhase.value?.contractType === CONTRACT_TYPES.FORFAIT)
// Jours à travailler du forfait : prorata exposé par le backend (218 sur année pleine,
// moins sur une entrée en cours d'année). Fallback 218 tant que le récap n'est pas chargé.
const forfaitWorkTargetDays = computed(() => {
const target = leave.leaveSummary.value?.forfaitWorkTargetDays
return (target === null || target === undefined) ? 218 : Math.round(target)
})
const employeeContractWorkLabel = computed(() => {
const contract = employee.value?.contract
if (!contract) return '-'
if (contract.type === CONTRACT_TYPES.FORFAIT) return 'Forfait - 218 jours'
if (contract.type === CONTRACT_TYPES.FORFAIT) return `Forfait - ${forfaitWorkTargetDays.value} jours`
if (contract.weeklyHours !== null && contract.weeklyHours !== undefined) return `${contract.weeklyHours} heures`
return contract.name || '-'
})
@@ -29,6 +38,7 @@ export const useEmployeeDetailPage = () => {
isLoading.value = true
try {
employee.value = await getEmployee(employeeId)
phase.resetToCurrent()
if (!showLeaveTab.value && activeTab.value === 'leave') {
activeTab.value = 'contract'
@@ -56,8 +66,9 @@ export const useEmployeeDetailPage = () => {
await bonus.loadBonusData()
} else if (activeTab.value === 'observation') {
await observation.loadObservationData()
} else if (isForfait.value && showLeaveTab.value) {
// Eager load: needed for the "X jours restants" header label on forfait employees.
} else if (showLeaveTab.value) {
// Eager load: the header shows présence (et jours à travailler/restant pour le forfait),
// qui proviennent du récap congés — nécessaire même quand on ouvre un autre onglet.
await leave.loadLeaveData()
}
} finally {
@@ -66,20 +77,46 @@ export const useEmployeeDetailPage = () => {
}
const contract = useEmployeeContract(employee, loadEmployee)
const leave = useEmployeeLeave(employee, loadEmployee)
const leave = useEmployeeLeave(employee, loadEmployee, phase.selectedPhase)
const formatDays = (n: number) => (Number.isInteger(n) ? String(n) : (Math.round(n * 100) / 100).toFixed(2).replace('.', ','))
// Forfait : « (présence · restant à travailler) ». restant = jours à travailler (prorata) présence.
const forfaitRemainingDaysLabel = computed(() => {
if (!isForfait.value) return ''
const presence = leave.leaveSummary.value?.presenceDaysToToday
if (presence === undefined || presence === null) return ''
const remaining = 218 - presence
return ` (${remaining} restants)`
const remaining = forfaitWorkTargetDays.value - presence
return ` (${formatDays(presence)} présence · ${formatDays(remaining)} restants)`
})
const rtt = useEmployeeRtt(employee, loadEmployee)
// Non-forfait : « (présence) » seul (pas de cible de jours à travailler).
const nonForfaitPresenceLabel = computed(() => {
if (isForfait.value) return ''
const presence = leave.leaveSummary.value?.presenceDaysToToday
if (presence === undefined || presence === null) return ''
return ` (${formatDays(presence)} présence)`
})
const rtt = useEmployeeRtt(employee, loadEmployee, phase.selectedPhase)
const mileage = useEmployeeMileage(employee, loadEmployee)
const formation = useEmployeeFormation(employee, loadEmployee)
const bonus = useEmployeeBonus(employee, loadEmployee)
const observation = useEmployeeObservation(employee, loadEmployee)
watch(() => phase.selectedPhase.value?.id, (newId, oldId) => {
if (newId === oldId || oldId === undefined) return
// Bascule onglet si on entre dans une phase qui ne supporte plus le tab actuel
if (!showRttTab.value && activeTab.value === 'rtt') {
activeTab.value = 'leave'
}
// Recharger l'onglet courant ; sinon recharger quand même le récap congés
// pour que le libellé de présence / jours à travailler du header reste à jour.
if (activeTab.value === 'leave' && showLeaveTab.value) {
leave.loadLeaveData()
} else if (activeTab.value === 'rtt' && showRttTab.value) {
rtt.loadRttData()
} else if (showLeaveTab.value) {
leave.loadLeaveData()
}
})
watch(activeTab, (tab) => {
if (tab === 'leave' && !leave.leaveDataLoaded.value && showLeaveTab.value) {
leave.loadLeaveData()
@@ -109,6 +146,8 @@ export const useEmployeeDetailPage = () => {
showRttTab,
employeeContractWorkLabel,
forfaitRemainingDaysLabel,
nonForfaitPresenceLabel,
...phase,
...contract,
...leave,
...rtt,

View File

@@ -1,5 +1,6 @@
import type { Ref } from 'vue'
import type { Absence } from '~/services/dto/absence'
import type { ContractPhase } from '~/services/dto/contract-phase'
import type { EmployeeLeaveSummary } from '~/services/dto/employee-leave-summary'
import type { Employee } from '~/services/dto/employee'
import { CONTRACT_TYPES } from '~/services/dto/contract'
@@ -12,7 +13,11 @@ export type LeaveYearOption = {
label: string
}
export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
export const useEmployeeLeave = (
employee: Ref<Employee | null>,
reloadEmployee: () => Promise<void>,
selectedPhase: Ref<ContractPhase | null>,
) => {
const employeeAbsences = ref<Absence[]>([])
const leaveSummary = ref<EmployeeLeaveSummary | null>(null)
const publicHolidays = ref<Record<string, string>>({})
@@ -20,17 +25,18 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
const leaveDataLoaded = ref(false)
const selectedLeaveYear = ref<number | null>(null)
const isForfaitContract = (emp: Employee | null) =>
emp?.contract?.type === CONTRACT_TYPES.FORFAIT
const isForfaitOnPhase = computed(() =>
selectedPhase.value?.contractType === CONTRACT_TYPES.FORFAIT
)
const computeLeaveYearForDate = (emp: Employee | null, date: Date): number => {
if (isForfaitContract(emp)) return date.getFullYear()
const computeLeaveYearForDate = (date: Date): number => {
if (isForfaitOnPhase.value) 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())
return computeLeaveYearForDate(new Date())
})
const formatLeaveYearLabel = (year: number, isForfait: boolean): string => {
@@ -39,23 +45,15 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
}
const availableLeaveYears = computed<LeaveYearOption[]>(() => {
if (!employee.value || currentLeaveYear.value === null) return []
const isForfait = isForfaitContract(employee.value)
const current = currentLeaveYear.value
if (!employee.value || !selectedPhase.value || currentLeaveYear.value === null) return []
const isForfait = isForfaitOnPhase.value
const phase = selectedPhase.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
}
// Plage = exercices intersectant la phase.
const phaseStartYear = computeLeaveYearForDate(new Date(`${phase.startDate}T00:00:00`))
const phaseEndYear = phase.endDate
? computeLeaveYearForDate(new Date(`${phase.endDate}T00:00:00`))
: currentLeaveYear.value
// 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.
@@ -64,14 +62,15 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
if (dataStart) {
const dataStartDate = new Date(`${dataStart.substring(0, 10)}T00:00:00`)
if (!Number.isNaN(dataStartDate.getTime())) {
dataFloor = computeLeaveYearForDate(employee.value, dataStartDate)
dataFloor = computeLeaveYearForDate(dataStartDate)
}
}
const minYear = dataFloor !== null ? Math.max(contractFloor, dataFloor) : contractFloor
const minYear = dataFloor !== null ? Math.max(phaseStartYear, dataFloor) : phaseStartYear
const maxYear = phaseEndYear
const years: LeaveYearOption[] = []
for (let y = current; y >= minYear; y -= 1) {
for (let y = maxYear; y >= minYear; y -= 1) {
years.push({ value: y, label: formatLeaveYearLabel(y, isForfait) })
}
return years
@@ -90,15 +89,18 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
if (selectedLeaveYear.value === null) return
isLeaveLoading.value = true
try {
const isForfait = isForfaitContract(employee.value)
const isForfait = isForfaitOnPhase.value
const leaveYear = selectedLeaveYear.value
const from = isForfait ? `${leaveYear}-01-01` : `${leaveYear - 1}-06-01`
const to = isForfait ? `${leaveYear}-12-31` : `${leaveYear}-05-31`
let from = isForfait ? `${leaveYear}-01-01` : `${leaveYear - 1}-06-01`
let to = isForfait ? `${leaveYear}-12-31` : `${leaveYear}-05-31`
const phase = selectedPhase.value
if (phase?.startDate && phase.startDate > from) from = phase.startDate
if (phase?.endDate && phase.endDate < to) to = phase.endDate
const holidayYears = isForfait ? [leaveYear] : [leaveYear - 1, leaveYear]
const [absences, summary, ...holidayResults] = await Promise.all([
listAbsences({ from, to, employeeId: employee.value.id }),
getEmployeeLeaveSummary(employee.value.id, leaveYear),
getEmployeeLeaveSummary(employee.value.id, leaveYear, selectedPhase.value?.id),
...holidayYears.map((y) => listPublicHolidays('metropole', y))
])
employeeAbsences.value = absences
@@ -122,6 +124,13 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
selectedLeaveYear.value = null
}
watch(() => selectedPhase.value?.id, () => {
// Reset l'année car la plage a peut-être changé.
selectedLeaveYear.value = null
leaveDataLoaded.value = false
// Le rechargement effectif est piloté par useEmployeeDetailPage.
})
const submitFractionedDays = async (days: number) => {
if (!employee.value) return
const year = leaveSummary.value?.year ?? undefined

View File

@@ -1,4 +1,5 @@
import type { Ref } from 'vue'
import type { ContractPhase } from '~/services/dto/contract-phase'
import type { EmployeeRttSummary } from '~/services/dto/employee-rtt-summary'
import type { Employee } from '~/services/dto/employee'
import { getEmployeeRttSummary, createRttPayment } from '~/services/employee-rtt-summary'
@@ -8,7 +9,11 @@ export type RttYearOption = {
label: string
}
export const useEmployeeRtt = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
export const useEmployeeRtt = (
employee: Ref<Employee | null>,
reloadEmployee: () => Promise<void>,
selectedPhase: Ref<ContractPhase | null>,
) => {
const rttSummary = ref<EmployeeRttSummary | null>(null)
const isRttLoading = ref(false)
const rttDataLoaded = ref(false)
@@ -25,22 +30,14 @@ export const useEmployeeRtt = (employee: Ref<Employee | null>, reloadEmployee: (
})
const availableRttYears = computed<RttYearOption[]>(() => {
if (!employee.value || currentRttYear.value === null) return []
const current = currentRttYear.value
if (!employee.value || !selectedPhase.value || currentRttYear.value === null) return []
const phase = selectedPhase.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 rttYear = computeRttYearForDate(date)
if (rttYear < contractFloor) contractFloor = rttYear
}
// Plage = exercices intersectant la phase.
const phaseStartYear = computeRttYearForDate(new Date(`${phase.startDate}T00:00:00`))
const phaseEndYear = phase.endDate
? computeRttYearForDate(new Date(`${phase.endDate}T00:00:00`))
: currentRttYear.value
// Hard floor : rttStartDate (env RTT_START_DATE) — pas d'historique avant.
let dataFloor: number | null = null
@@ -52,10 +49,11 @@ export const useEmployeeRtt = (employee: Ref<Employee | null>, reloadEmployee: (
}
}
const minYear = dataFloor !== null ? Math.max(contractFloor, dataFloor) : contractFloor
const minYear = dataFloor !== null ? Math.max(phaseStartYear, dataFloor) : phaseStartYear
const maxYear = phaseEndYear
const years: RttYearOption[] = []
for (let y = current; y >= minYear; y -= 1) {
for (let y = maxYear; y >= minYear; y -= 1) {
years.push({ value: y, label: `Juin ${y - 1} → Mai ${y}` })
}
return years
@@ -74,7 +72,11 @@ export const useEmployeeRtt = (employee: Ref<Employee | null>, reloadEmployee: (
if (selectedRttYear.value === null) return
isRttLoading.value = true
try {
rttSummary.value = await getEmployeeRttSummary(employee.value.id, selectedRttYear.value)
rttSummary.value = await getEmployeeRttSummary(
employee.value.id,
selectedRttYear.value,
selectedPhase.value?.id,
)
rttDataLoaded.value = true
} finally {
isRttLoading.value = false
@@ -93,6 +95,13 @@ export const useEmployeeRtt = (employee: Ref<Employee | null>, reloadEmployee: (
selectedRttYear.value = null
}
watch(() => selectedPhase.value?.id, () => {
// Reset l'année car la plage a peut-être changé.
selectedRttYear.value = null
rttDataLoaded.value = false
// Le rechargement effectif est piloté par useEmployeeDetailPage.
})
const submitRttPayment = async (month: number, base25Minutes: number, bonus25Minutes: number, base50Minutes: number, bonus50Minutes: number) => {
if (!employee.value) return
const year = rttSummary.value?.year ?? undefined

View File

@@ -301,6 +301,18 @@ export const documentationSections: DocSection[] = [
{ type: 'paragraph', content: 'Un employé conducteur apparaît uniquement sur l\'écran "Heures Conducteurs" et non sur l\'écran "Heures" classique.' },
],
},
{
id: 'contract-phase-view',
title: 'Vue contrat — sélecteur de phase',
requiredLevel: 'admin',
blocks: [
{ type: 'paragraph', content: 'Quand un employé change de type de contrat (ex. 39h → FORFAIT) ou enchaîne plusieurs CDD avec solde de tout compte, ses anciennes phases de contrat restent consultables via le sélecteur "Vue contrat" en haut de la fiche.' },
{ type: 'paragraph', content: 'Choisir une phase passée fait basculer les onglets Congés et RTT sur les règles de cette phase. L\'onglet RTT réapparaît si la phase n\'est pas un FORFAIT. Un bandeau jaune indique que vous êtes en mode historique.' },
{ type: 'paragraph', content: 'Sur une phase passée, vous pouvez :' },
{ type: 'list', content: 'Solder les RTT restants — bouton "+ Payer les RTT" actif uniquement sur le dernier exercice de la phase (celui contenant la date de fin)\nSolder les CP restants via le champ "Solde de tout compte" sur la période de contrat correspondante (onglet Contrat)' },
{ type: 'note', content: 'L\'édition d\'absences et des stocks de report (jours fractionnés, Année N-1) est désactivée en mode phase passée.' },
],
},
],
},
{
@@ -399,7 +411,7 @@ export const documentationSections: DocSection[] = [
blocks: [
{ type: 'paragraph', content: 'Le calendrier offre une vue d\'ensemble mensuelle des absences de tous les employés.' },
{ type: 'list', content: 'Code couleur par type d\'absence\nDemi-journée : affichage en dégradé diagonal\nJournée complète : fond plein\nJours fériés : fond bleu clair (cliquable pour créer une absence)\nFiltres par site et par employé\nNavigation par mois (précédent / suivant)' },
{ type: 'note', content: 'Seuls les employés ayant au moins un jour de contrat sur le mois affiché apparaissent. Un employé dont le contrat s\'est terminé avant le 1er du mois (ou qui commence après la fin du mois) est masqué.' },
{ type: 'note', content: 'Seuls les employés ayant au moins un jour de contrat sur le mois affiché apparaissent. Un employé dont le contrat s\'est terminé avant le 1er du mois (ou qui commence après la fin du mois) est masqué. L\'impression PDF applique la même règle : un salarié parti avant la période imprimée n\'apparaît pas dans le document.' },
{ type: 'note', content: 'Les absences peuvent être créées sur les jours fériés. Quand une absence est posée sur un férié, elle remplace l\'affichage « Férié » dans la cellule.' },
],
},
@@ -418,6 +430,7 @@ export const documentationSections: DocSection[] = [
blocks: [
{ type: 'paragraph', content: 'Pour les contrats CDI et CDD (hors forfait), l\'exercice de congés va du 1er juin (N-1) au 31 mai (N).' },
{ type: 'list', content: 'Acquisition annuelle : 25 jours + 5 samedis\nAcquisition mensuelle : 2,08 jours + 0,42 samedi par mois\nProratisation en cas de début/fin ou suspension en cours de mois\nContrat 4h : 10 jours annuels, 0 samedi, 0,83 jour/mois' },
{ type: 'paragraph', content: 'En haut de la fiche, l\'en-tête affiche le nombre de jours de présence du salarié sur l\'exercice. La présence est comptée à partir de la date de début de contrat : les jours antérieurs à l\'embauche ne sont pas comptés (utile pour un salarié arrivé en cours d\'année).' },
],
},
{
@@ -427,6 +440,9 @@ export const documentationSections: DocSection[] = [
blocks: [
{ type: 'paragraph', content: 'Pour les contrats forfait, l\'exercice suit l\'année civile (1er janvier au 31 décembre).' },
{ type: 'list', content: 'Calcul : jours ouvrés de l\'année 218 + bonus weekend/férié\nBonus : 1 jour par jour travaillé un weekend ou jour férié (0.5 si demi-journée)\nPas de samedis\nPas de jours en cours d\'acquisition' },
{ type: 'paragraph', content: 'Lorsqu\'un salarié passe en forfait en cours d\'année (ex. après une phase 39h), ses congés à poser pour cette année-là correspondent à ses jours de repos forfait calculés au prorata de la période, augmentés du reliquat de congés payés acquis sous son contrat précédent. Un nouveau salarié embauché directement en forfait en cours d\'année n\'a que les jours de repos proratisés. Les années complètes suivantes suivent le calcul forfait habituel.' },
{ type: 'note', content: 'Le reliquat CP de la phase précédente inclut les jours ouvrés nets (acquis + en cours jours ouvrés posés) et les samedis bruts (les samedis déjà posés ne réduisent pas le report). Les jours fractionnés sont exclus.' },
{ type: 'paragraph', content: 'En haut de la fiche, l\'en-tête forfait affiche les jours à travailler de l\'exercice (218 sur une année complète, proratisés en cas d\'entrée en cours d\'année), le nombre de jours de présence déjà effectués, et le nombre de jours restant à travailler.' },
],
},
{

View File

@@ -26,10 +26,28 @@
<p>Date d'entrée : {{ employee.entryDate ? employee.entryDate.split('-').reverse().join('/') : '-' }}</p>
</div>
<div class="text-right">
<p class="font-bold text-[22px]">{{ contractNatureLabel(employee.currentContractNature) }} {{ employeeContractWorkLabel }}{{ forfaitRemainingDaysLabel }}</p>
<p class="font-bold text-[22px]">{{ contractNatureLabel(employee.currentContractNature) }} {{ employeeContractWorkLabel }}{{ forfaitRemainingDaysLabel }}{{ nonForfaitPresenceLabel }}</p>
<p class="text-[18px]">{{ employee.site?.name ?? '-' }}</p>
</div>
</div>
<div v-if="showPicker" class="mt-3 flex items-center gap-3">
<MalioSelect
label="Contrat"
:model-value="selectedPhase?.id ?? null"
:options="phaseOptions"
group-class="w-[420px]"
min-width=""
@update:model-value="(v) => { if (v !== null) setSelectedPhase(Number(v)) }"
/>
</div>
<div
v-if="isViewingPastPhase && selectedPhase"
class="mt-3 rounded-md border border-amber-300 bg-amber-100 px-4 py-2 text-sm text-amber-900"
>
Vous consultez l'historique
<strong>{{ formatPhaseLabel(selectedPhase) }}</strong>.
Les paiements de solde sont possibles ; l'édition d'absences et des stocks de report est désactivée.
</div>
<div class="mt-[44px] border-b border-primary-500">
<div class="flex justify-center gap-16 text-2xl font-bold">
<button
@@ -179,6 +197,7 @@
:selected-year="selectedRttYear"
:available-years="availableRttYears"
:current-year="currentRttYear"
:selected-phase="selectedPhase"
@submit-rtt-payment="submitRttPayment"
@update-selected-year="setSelectedRttYear"
/>
@@ -253,6 +272,7 @@
import { ref } from 'vue'
import EmployeeYearlyHoursDrawer from '~/components/EmployeeYearlyHoursDrawer.vue'
import { usePdfPrinter } from '~/composables/usePdfPrinter'
import { formatPhaseLabel } from '~/composables/useEmployeeContractPhase'
const { printPdf } = usePdfPrinter()
const isYearlyHoursDrawerOpen = ref(false)
@@ -279,6 +299,7 @@ const {
contractHistory,
employeeContractWorkLabel,
forfaitRemainingDaysLabel,
nonForfaitPresenceLabel,
contractForm,
createContractForm,
isContractDrawerOpen,
@@ -342,7 +363,12 @@ const {
isObservationLoading,
submitCreateObservation,
submitUpdateObservation,
submitDeleteObservation
submitDeleteObservation,
selectedPhase,
showPicker,
phaseOptions,
setSelectedPhase,
isViewingPastPhase,
} = useEmployeeDetailPage()
const handleYearlyHoursPrint = async (payload: { year: number; month: number | null }) => {

View File

@@ -0,0 +1,13 @@
import type { ContractType } from './contract'
export type ContractPhase = {
id: number
contractType: ContractType
weeklyHours: number | null
isDriver: boolean
startDate: string
endDate: string | null
periodIds: number[]
isCurrent: boolean
contractNature: 'CDI' | 'CDD' | 'INTERIM'
}

View File

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

View File

@@ -1,5 +1,6 @@
import type { Site } from './site'
import type { Contract } from './contract'
import type { ContractPhase } from './contract-phase'
export type ContractSuspension = {
id: number
@@ -41,4 +42,5 @@ export type Employee = {
currentSuspensions?: ContractSuspension[]
currentInterimAgencyId?: number | null
currentInterimAgencyName?: string | null
contractPhases?: ContractPhase[]
}

View File

@@ -1,9 +1,10 @@
import type { EmployeeLeaveSummary } from './dto/employee-leave-summary'
export const getEmployeeLeaveSummary = async (employeeId: number, year?: number) => {
export const getEmployeeLeaveSummary = async (employeeId: number, year?: number, phaseId?: number) => {
const api = useApi()
const query: Record<string, string> = {}
if (year) query.year = String(year)
if (phaseId !== undefined) query.phaseId = String(phaseId)
return api.get<EmployeeLeaveSummary>(`/employees/${employeeId}/leave-summary`, query, { toast: false })
}

View File

@@ -1,8 +1,10 @@
import type { EmployeeRttSummary } from './dto/employee-rtt-summary'
export const getEmployeeRttSummary = async (employeeId: number, year?: number) => {
export const getEmployeeRttSummary = async (employeeId: number, year?: number, phaseId?: number) => {
const api = useApi()
const query = year ? { year } : {}
const query: Record<string, number> = {}
if (year) query.year = year
if (phaseId !== undefined) query.phaseId = phaseId
return api.get<EmployeeRttSummary>(`/employees/${employeeId}/rtt-summary`, query, { toast: false })
}