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:
@@ -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)
|
||||
|
||||
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
|
||||
|
||||
@@ -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.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
13
frontend/services/dto/contract-phase.ts
Normal file
13
frontend/services/dto/contract-phase.ts
Normal 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'
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export type EmployeeLeaveSummary = {
|
||||
previousYearPaidDays: number
|
||||
presenceDaysByMonth: Record<string, number>
|
||||
presenceDaysToToday: number
|
||||
forfaitWorkTargetDays: number | null
|
||||
dataStartDate: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user