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
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:
83
frontend/composables/useEmployeeContractPhase.ts
Normal file
83
frontend/composables/useEmployeeContractPhase.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user