Compare commits

...

3 Commits

Author SHA1 Message Date
gitea-actions
b541f9ded8 chore: bump version to v0.1.101
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 38s
2026-05-04 07:59:16 +00:00
47f9bea57d feat : sélecteur d'exercice sur l'onglet RTT de la fiche employé
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Permet de consulter les exercices passés (table hebdomadaire RTT) en
réutilisant le pattern de l'onglet Congés. Plage bornée par
max(début historique contrat, RTT_START_DATE). Bouton + Payer les RTT
verrouillé sur exercices clos. Onglet masqué pour FORFAIT (inchangé).

Backend : rttStartDate désormais toujours exposé sur EmployeeRttSummary
pour que le sélecteur conserve sa borne lors de la navigation vers un
exercice passé. Le masquage existant des lignes Report continue de
fonctionner (comparaison mois-à-mois).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 09:58:50 +02:00
7cadcfa362 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>
2026-05-04 09:51:19 +02:00
15 changed files with 436 additions and 27 deletions

View File

@@ -69,6 +69,20 @@
- FORFAIT weekend/holiday bonus: each weekend or public holiday day worked gives bonus leave (full day if morning+afternoon, 0.5 if only one). Added to acquired days, no cap. PRESENCE mode only.
- **FORFAIT — jours de présence et N-1** : les congés posés et imputés sur le stock N-1 ne décrémentent **pas** les jours de présence affichés (`presenceDaysByMonth` et `presenceDaysToToday`). Implémenté dans `EmployeeLeaveSummaryProvider::computePresenceDaysByMonth` via un budget N-1 (= `previousYearTakenDays`) consommé chronologiquement avant comptage des absences. Pour les non-forfait, ce budget vaut toujours 0 → comportement inchangé.
## Onglet Congés (fiche employé)
- Calendrier annuel des congés (`frontend/components/employees/LeaveTab.vue`) — période = Janvier→Décembre pour FORFAIT, Juin(N-1)→Mai(N) pour les autres contrats. Règle pilotée par le **contrat courant** (cf. `EmployeeLeaveSummaryProvider::resolveYear`), même quand on consulte une année passée.
- **Sélecteur d'année** en pied de calendrier (zone scrollable, à gauche). Plage : de l'exercice courant jusqu'à `max(floor_contrat, floor_data_start_date)``floor_contrat` = premier exercice avec contrat ouvert (`employee.contractHistory[].startDate`) ; `floor_data_start_date` = exercice contenant `RTT_START_DATE` (env, ex. `2026-02-23` → exercice 2026). Le double plancher empêche de remonter avant la mise en service du logiciel. Format : `2026` pour FORFAIT, `Juin 2025 → Mai 2026` sinon.
- Changement d'année → recharge complète de l'onglet via `useEmployeeLeave.setSelectedLeaveYear(year)` (reload de `getEmployeeLeaveSummary?year=YYYY` + `listAbsences` + `listPublicHolidays`). Backend : filtre `?year=YYYY` validé 2000-2100, et `EmployeeLeaveSummary` expose `dataStartDate` (env `RTT_START_DATE`, injecté via `services.yaml`).
- Sur un exercice passé (`selectedYear !== currentYear`), les boutons crayon **Jours fractionnés** et **Année N-1 payés** sont **désactivés** : pas d'édition rétroactive des stocks de report.
- Doc : `doc/leave-tab.md`.
## Onglet RTT (fiche employé)
- Tableau hebdomadaire (`frontend/components/employees/RttTab.vue`) — exercice fixe Juin(N-1)→Mai(N). Onglet **masqué pour les FORFAIT** (`showRttTab`).
- **Sélecteur d'année** sous le tableau dans la zone scrollable. Même mécanique que l'onglet Congés (double plancher) : `max(floor_contrat, floor_rttStartDate)`. Format unique : `Juin 2025 → Mai 2026`.
- Changement d'année → recharge via `useEmployeeRtt.setSelectedRttYear(year)` (`getEmployeeRttSummary?year=YYYY`). `EmployeeRttSummary.rttStartDate` est déjà exposé (champ existant) — il sert à la fois au floor du sélecteur et au masquage des lignes Report avant la mise en service.
- Sur un exercice passé, le bouton **+ Payer les RTT** est désactivé (pas de paiement rétroactif).
- Doc : `doc/rtt-tab.md`.
## Récap. congés (écran)
- Accès via sidebar `Récap. congés`, conditionné au flag `User.hasLeaveRecapAccess` (défaut `false`) — activé au create/edit user. Le flag s'applique à tous les profils, y compris admin.
- Scope : `ROLE_ADMIN` → tous les employés, `ROLE_USER` (chef de site) → employés de ses sites, `ROLE_SELF` → sa ligne

View File

@@ -35,6 +35,10 @@ services:
arguments:
$rttStartDate: '%env(RTT_START_DATE)%'
App\State\EmployeeLeaveSummaryProvider:
arguments:
$dataStartDate: '%env(RTT_START_DATE)%'
App\Repository\Contract\AbsenceReadRepositoryInterface: '@App\Repository\AbsenceRepository'
App\Repository\Contract\EmployeeContractPeriodReadRepositoryInterface: '@App\Repository\EmployeeContractPeriodRepository'
App\Repository\Contract\EmployeeScopedRepositoryInterface: '@App\Repository\EmployeeRepository'

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.100'
app.version: '0.1.101'

60
doc/leave-tab.md Normal file
View File

@@ -0,0 +1,60 @@
# Onglet "Congés" — fiche employé
## Vue d'ensemble
L'onglet **Congés** de la fiche employé (`frontend/components/employees/LeaveTab.vue`) affiche :
- un bandeau de compteurs (acquis, pris, reste, en cours d'acquisition, N-1 ou samedis selon le contrat) ;
- un calendrier annuel coloré des congés posés (12 mois en grille 4×3) ;
- pour chaque mois, le nombre de jours de présence (`presenceDaysByMonth`) ;
- un sélecteur d'année en pied de calendrier.
## Période affichée
La période dépend du **type de contrat actuel** de l'employé :
| Type de contrat | Période affichée |
|-------------------|--------------------------------|
| FORFAIT | Janvier → Décembre (année civile) |
| Autres | Juin (Y-1) → Mai (Y) (exercice CP) |
Cette règle suit `EmployeeLeaveSummaryProvider::resolveYear()` côté backend : la sélection FORFAIT vs non-FORFAIT se fait toujours sur le contrat **courant**, pas sur celui qui était en vigueur à l'année consultée.
## Sélecteur d'année
Position : **en bas du calendrier**, à gauche, à l'intérieur de la zone scrollable. Il scrolle donc avec les mois et apparaît sous la grille.
Plage proposée :
- du plus récent (= année courante) au plus ancien ;
- **double plancher** : l'année minimum est `max(floor_historique_contrat, floor_data_start_date)`
- **floor_historique_contrat** : dérivé de `employee.contractHistory[].startDate` — premier exercice où l'employé avait un contrat ouvert
- **floor_data_start_date** : dérivé de l'env `RTT_START_DATE` (date de mise en service du logiciel, ex. `2026-02-23` → exercice 2026 / année forfait 2026). Aucune donnée historique n'existe avant cette date, donc on ne propose pas d'années antérieures même si le contrat de l'employé est plus ancien.
- la valeur est exposée par l'API `GET /employees/{id}/leave-summary` via le champ `dataStartDate` (peuplé depuis l'env serveur).
- en cas d'historique manquant **et** d'env absente, la plage se réduit à l'année courante.
Format des libellés :
- FORFAIT : `2026`, `2025`, `2024`
- Autres : `Juin 2025 → Mai 2026`, `Juin 2024 → Mai 2025`
Comportement :
- changer d'année recharge l'intégralité de l'onglet (`getEmployeeLeaveSummary?year=YYYY` + `listAbsences` + `listPublicHolidays`) ;
- les compteurs du bandeau reflètent l'année sélectionnée.
## Verrouillage des éditions sur années passées
Quand `selectedYear !== currentYear` (consultation d'une année antérieure) :
- le bouton crayon **Jours fractionnés** (non-FORFAIT) est désactivé ;
- le bouton crayon **Année N-1 payés** (FORFAIT) est désactivé.
Justification : modifier rétroactivement les stocks de report ou les jours fractionnés d'un exercice clos décalerait silencieusement les soldes de toutes les années postérieures. La consultation reste possible, l'édition non.
## Implémentation
- Composable : `frontend/composables/useEmployeeLeave.ts`
- État : `selectedLeaveYear`, computed `currentLeaveYear`, `availableLeaveYears`
- API : `setSelectedLeaveYear(year)`, `loadLeaveData()`, `resetLoaded()`
- `resetLoaded()` (appelé au changement d'employé) remet `selectedLeaveYear = null` pour que la valeur par défaut soit recalculée à partir du nouveau contrat.
- Composant : `frontend/components/employees/LeaveTab.vue`
- Props : `selectedYear`, `availableYears`, `currentYear`
- Event : `update-selected-year`
- Page : `frontend/pages/employees/[id].vue` (câble le composable au composant)
- Backend : `EmployeeLeaveSummaryProvider` reçoit `RTT_START_DATE` via `services.yaml` (argument `$dataStartDate`) et l'expose dans la réponse `EmployeeLeaveSummary.dataStartDate`. Le filtrage `?year=YYYY` était déjà accepté (validation 20002100).

52
doc/rtt-tab.md Normal file
View File

@@ -0,0 +1,52 @@
# Onglet "RTT" — fiche employé
## Vue d'ensemble
L'onglet **RTT** de la fiche employé (`frontend/components/employees/RttTab.vue`) affiche un tableau hebdomadaire détaillé des heures supplémentaires accumulées et payées sur un exercice :
- bandeau de navigation par mois (chevrons gauche/droite) ;
- table semaine par semaine : Heure / Base 25% / 25% / Total 25% / Base 50% / 50% / Total 50% / Total / Cumul ;
- ligne Report (carry N-1 ou cumul mois précédents) ;
- ligne Total mois, ligne Payé, ligne Reste ;
- bouton « + Payer les RTT » dans le bandeau ;
- sélecteur d'exercice en pied de tableau.
L'onglet est **masqué pour les contrats FORFAIT** (filtre `showRttTab` dans `useEmployeeDetailPage`). Les FORFAIT n'accumulent pas de RTT.
## Période affichée
Toujours **Juin (Y-1) → Mai (Y)**. Le champ `EmployeeRttSummary.year` correspond à `Y` (année de fin d'exercice) ; ex. `year=2026` = `01/06/2025 → 31/05/2026`.
## Sélecteur d'année
Position : sous la table, à l'intérieur de la zone scrollable, à gauche.
Plage proposée :
- du plus récent (= exercice courant) au plus ancien ;
- **double plancher** : `max(floor_historique_contrat, floor_data_start_date)`
- **floor_historique_contrat** : dérivé de `employee.contractHistory[].startDate` — premier exercice où l'employé avait un contrat ouvert
- **floor_data_start_date** : exercice contenant `RTT_START_DATE` (env, ex. `2026-02-23` → exercice 2026)
- la valeur est exposée par l'API `GET /employees/{id}/rtt-summary` via le champ `rttStartDate` (déjà existant — mais peuplé uniquement quand la date tombe dans l'exercice retourné, donc le composable utilise la première réponse pour borner la plage).
- format unique : `Juin 2025 → Mai 2026`, `Juin 2024 → Mai 2025`
Comportement :
- changer d'exercice recharge `getEmployeeRttSummary?year=YYYY` (le backend valide 20002100) ;
- la table redéploie les semaines de l'exercice sélectionné, navigation par mois conservée.
## Verrouillage des édition sur exercices passés
Quand `selectedYear !== currentYear` (consultation d'un exercice antérieur), le bouton **+ Payer les RTT** est désactivé. Justification : un paiement rétroactif sur un exercice clos décalerait les soldes courants et le report N-1 calculé.
La consultation reste possible, l'édition non.
## Implémentation
- Composable : `frontend/composables/useEmployeeRtt.ts`
- État : `selectedRttYear`, computed `currentRttYear`, `availableRttYears`
- API : `setSelectedRttYear(year)`, `loadRttData()`, `resetLoaded()`
- `resetLoaded()` (appelé au changement d'employé) remet `selectedRttYear = null`.
- Composant : `frontend/components/employees/RttTab.vue`
- Props : `selectedYear`, `availableYears`, `currentYear`
- Event : `update-selected-year`
- Renommage `currentYear` (computed local de l'année du mois affiché) → `displayedMonthYear` pour éviter la collision avec la nouvelle prop.
- Page : `frontend/pages/employees/[id].vue`
- Backend : aucun changement — `EmployeeRttSummaryProvider` accepte déjà `?year=YYYY` (validation 20002100) et expose `rttStartDate`.

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

@@ -11,7 +11,7 @@
<Icon name="mdi:chevron-left" size="24"/>
</button>
<span class="text-lg font-bold tracking-wide min-w-[170px] text-center">
{{ currentMonthLabel }} {{ currentYear }}
{{ currentMonthLabel }} {{ displayedMonthYear }}
</span>
<button
class="rounded px-2 py-1 font-bold hover:bg-primary-600 disabled:opacity-40 disabled:cursor-not-allowed flex items-center"
@@ -27,7 +27,8 @@
</p>
<div class="flex justify-center">
<button
class="rounded-md bg-primary-500 px-8 py-2 font-bold text-white hover:bg-primary-600"
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"
@click="openPaymentDrawer"
>
+ Payer les RTT
@@ -183,6 +184,22 @@
</tr>
</tbody>
</table>
<div class="mt-6 flex items-center gap-3">
<label for="rtt-year-select" class="text-md font-semibold text-primary-500 uppercase">
Exercice :
</label>
<select
id="rtt-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>
<!-- Payment Drawer -->
@@ -261,14 +278,36 @@
import type { EmployeeRttSummary, EmployeeRttWeekSummary } from '~/services/dto/employee-rtt-summary'
import AppDrawer from '~/components/AppDrawer.vue'
type RttYearOption = {
value: number
label: string
}
const props = defineProps<{
summary: EmployeeRttSummary | null
selectedYear: number | null
availableYears: RttYearOption[]
currentYear: number | null
}>()
const emit = defineEmits<{
(event: 'submit-rtt-payment', month: number, base25Minutes: number, bonus25Minutes: number, base50Minutes: number, bonus50Minutes: 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)
}
// --- Last complete week number ---
const lastCompleteWeek = computed(() => {
@@ -324,7 +363,7 @@ const currentMonth = computed(() => orderedMonths[currentMonthIndex.value])
const currentMonthLabel = computed(() => monthLabels[currentMonth.value])
const currentYear = computed(() => {
const displayedMonthYear = computed(() => {
if (!props.summary) return ''
return currentMonth.value >= 6 ? props.summary.year - 1 : props.summary.year
})

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

@@ -3,25 +3,94 @@ import type { EmployeeRttSummary } from '~/services/dto/employee-rtt-summary'
import type { Employee } from '~/services/dto/employee'
import { getEmployeeRttSummary, createRttPayment } from '~/services/employee-rtt-summary'
export type RttYearOption = {
value: number
label: string
}
export const useEmployeeRtt = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
const rttSummary = ref<EmployeeRttSummary | null>(null)
const isRttLoading = ref(false)
const rttDataLoaded = ref(false)
const selectedRttYear = ref<number | null>(null)
// Exercice RTT : Juin (Y-1) → Mai (Y). Toujours, peu importe le type de contrat
// (l'onglet RTT est masqué pour les FORFAIT côté page).
const computeRttYearForDate = (date: Date): number =>
date.getMonth() >= 5 ? date.getFullYear() + 1 : date.getFullYear()
const currentRttYear = computed<number | null>(() => {
if (!employee.value) return null
return computeRttYearForDate(new Date())
})
const availableRttYears = computed<RttYearOption[]>(() => {
if (!employee.value || currentRttYear.value === null) return []
const current = currentRttYear.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
}
// Hard floor : rttStartDate (env RTT_START_DATE) — pas d'historique avant.
let dataFloor: number | null = null
const dataStart = rttSummary.value?.rttStartDate
if (dataStart) {
const dataStartDate = new Date(`${dataStart.substring(0, 10)}T00:00:00`)
if (!Number.isNaN(dataStartDate.getTime())) {
dataFloor = computeRttYearForDate(dataStartDate)
}
}
const minYear = dataFloor !== null ? Math.max(contractFloor, dataFloor) : contractFloor
const years: RttYearOption[] = []
for (let y = current; y >= minYear; y -= 1) {
years.push({ value: y, label: `Juin ${y - 1} → Mai ${y}` })
}
return years
})
const initSelectedRttYear = () => {
if (selectedRttYear.value !== null) return
if (currentRttYear.value !== null) {
selectedRttYear.value = currentRttYear.value
}
}
const loadRttData = async () => {
if (!employee.value || isRttLoading.value) return
initSelectedRttYear()
if (selectedRttYear.value === null) return
isRttLoading.value = true
try {
const rttYear = new Date().getMonth() >= 5 ? new Date().getFullYear() + 1 : new Date().getFullYear()
rttSummary.value = await getEmployeeRttSummary(employee.value.id, rttYear)
rttSummary.value = await getEmployeeRttSummary(employee.value.id, selectedRttYear.value)
rttDataLoaded.value = true
} finally {
isRttLoading.value = false
}
}
const setSelectedRttYear = async (year: number) => {
if (selectedRttYear.value === year) return
selectedRttYear.value = year
rttDataLoaded.value = false
await loadRttData()
}
const resetLoaded = () => {
rttDataLoaded.value = false
selectedRttYear.value = null
}
const submitRttPayment = async (month: number, base25Minutes: number, bonus25Minutes: number, base50Minutes: number, bonus50Minutes: number) => {
@@ -35,6 +104,10 @@ export const useEmployeeRtt = (employee: Ref<Employee | null>, reloadEmployee: (
rttSummary,
isRttLoading,
rttDataLoaded,
selectedRttYear,
currentRttYear,
availableRttYears,
setSelectedRttYear,
loadRttData,
resetLoaded,
submitRttPayment

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',
@@ -504,6 +515,15 @@ export const documentationSections: DocSection[] = [
{ type: 'list', content: 'Saisie : mois, nombre de minutes, taux (25% ou 50%)\nPlusieurs paiements possibles par mois\nLes heures payées sont soustraites du solde disponible' },
],
},
{
id: 'rtt-selecteur-exercice',
title: 'Consulter un exercice passé',
requiredLevel: 'admin',
blocks: [
{ type: 'paragraph', content: 'Un sélecteur d\'exercice est disponible en bas du tableau RTT (zone scrollable, à gauche). Il permet de consulter les exercices passés (Juin → Mai). 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.' },
{ type: 'note', content: 'Sur un exercice passé, le bouton « + Payer les RTT » est désactivé. Aucun paiement rétroactif n\'est autorisé pour préserver la cohérence du report N-1.' },
],
},
{
id: 'rtt-semaines-mois',
title: 'Attribution des semaines aux mois',

View File

@@ -160,15 +160,28 @@
: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">
<div v-if="isRttLoading" class="mt-6 rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
Chargement...
</div>
<EmployeesRttTab v-else class="h-full" :summary="rttSummary" @submit-rtt-payment="submitRttPayment" />
<EmployeesRttTab
v-else
class="h-full"
:summary="rttSummary"
:selected-year="selectedRttYear"
:available-years="availableRttYears"
:current-year="currentRttYear"
@submit-rtt-payment="submitRttPayment"
@update-selected-year="setSelectedRttYear"
/>
</div>
<div v-else-if="activeTab === 'mileage'" class="h-full">
<div v-if="isMileageLoading" class="mt-6 rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
@@ -253,6 +266,14 @@ const {
leaveSummary,
rttSummary,
publicHolidays,
selectedLeaveYear,
currentLeaveYear,
availableLeaveYears,
setSelectedLeaveYear,
selectedRttYear,
currentRttYear,
availableRttYears,
setSelectedRttYear,
showLeaveTab,
showRttTab,
contractHistory,

View File

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

View File

@@ -41,4 +41,7 @@ final class EmployeeLeaveSummary
/** Cumul des jours de présence depuis le début de l'année de congé jusqu'à aujourd'hui (forfait). */
public float $presenceDaysToToday = 0.0;
/** Date de mise en service du logiciel (env RTT_START_DATE) — borne minimale pour les sélecteurs d'historique. */
public ?string $dataStartDate = null;
}

View File

@@ -45,6 +45,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
private const float CDI_NON_FORFAIT_4H_SATURDAY_ACCRUAL_PER_MONTH = 0.0;
private const float LONG_MALADIE_MONTHLY_ACCRUAL = 2.0;
private ?string $dataStartDate;
public function __construct(
private Security $security,
private RequestStack $requestStack,
@@ -58,7 +60,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
private PublicHolidayServiceInterface $publicHolidayService,
private SuspensionDaysCalculator $suspensionDaysCalculator,
private WorkHourRepository $workHourRepository,
) {}
string $dataStartDate = '',
) {
$this->dataStartDate = '' !== $dataStartDate ? $dataStartDate : null;
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): EmployeeLeaveSummary
{
@@ -86,6 +91,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$summary = new EmployeeLeaveSummary();
$summary->year = $year;
$summary->ruleCode = LeaveRuleCode::UNSUPPORTED->value;
$summary->dataStartDate = $this->dataStartDate;
$yearSummary = $this->computeYearSummary($employee, $year);
if (null === $yearSummary) {

View File

@@ -110,13 +110,10 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
$summary->currentYearRecoveryMinutes = array_sum(array_map(static fn ($d) => $d->totalMinutes, $currentByWeekStart));
$summary->availableMinutes = $summary->carryFromPreviousYearMinutes + $summary->currentYearRecoveryMinutes;
// Pass rttStartDate only if it falls within this exercise
if (null !== $this->rttStartDate) {
$startDate = new DateTimeImmutable($this->rttStartDate);
if ($startDate >= $periodFrom && $startDate <= $periodTo) {
// Always expose rttStartDate so the frontend can use it as a hard floor
// for the year selector. Frontend already uses month-level comparison
// to hide carry/report rows when the date is outside the exercise.
$summary->rttStartDate = $this->rttStartDate;
}
}
$summary->weeks = $this->buildWeekSummaries($weekRanges, $currentByWeekStart, $periodFrom, $periodTo);
// Post-process: distribute deficit weeks across cumulative balance (50% first, then 25%)