[#SIRH] Exports heures annuelles: afficher tous les jours contractés + week-ends plus foncés
- YearlyHoursExportBuilder: ne plus sauter les jours de semaine vides/non saisis. Tous les jours sous contrat sont affichés (jusqu'à aujourd'hui); seuls les jours hors contrat restent omis. Corrige les lignes manquantes signalées par la RH. - Templates print/print-all: gris des samedis/dimanches foncé (#f3f3f3 -> #c0c0c0). - Docs (functional-rules, in-app, CLAUDE.md). NB: l'export tous-salariés sur l'année peut dépasser memory_limit=256M (Dompdf) — limitation pré-existante (déjà le cas avant ce changement), non corrigée ici. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
- **Agence d'intérim** (`InterimAgency` entity, table `interim_agencies`): optionnelle sur `EmployeeContractPeriod` quand nature = INTERIM. Pas de CRUD UI — gérée en BDD. API lecture seule `GET /interim_agencies`. Affichée "Intérim (NomAgence)" sur la liste employés et l'historique contrat.
|
- **Agence d'intérim** (`InterimAgency` entity, table `interim_agencies`): optionnelle sur `EmployeeContractPeriod` quand nature = INTERIM. Pas de CRUD UI — gérée en BDD. API lecture seule `GET /interim_agencies`. Affichée "Intérim (NomAgence)" sur la liste employés et l'historique contrat.
|
||||||
- Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver`
|
- Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver`
|
||||||
- **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). Idem pour le **mode de suivi (TIME/PRESENCE), les heures hebdo et le libellé de contrat** sur la vue Jour : résolus à la date filtrée via `WorkHourDayContext` (`trackingMode`/`weeklyHours`/`contractType`/`contractName`, peuplés depuis `EmployeeContractResolver::resolveForEmployeeAndDate`), pas via `employee.contract` (résolu à aujourd'hui). Côté front, `resolveDayContract()` (`useHoursPage.ts`) pilote l'affichage et `handleSave` (heures vs présence par date).
|
- **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). Idem pour le **mode de suivi (TIME/PRESENCE), les heures hebdo et le libellé de contrat** sur la vue Jour : résolus à la date filtrée via `WorkHourDayContext` (`trackingMode`/`weeklyHours`/`contractType`/`contractName`, peuplés depuis `EmployeeContractResolver::resolveForEmployeeAndDate`), pas via `employee.contract` (résolu à aujourd'hui). Côté front, `resolveDayContract()` (`useHoursPage.ts`) pilote l'affichage et `handleSave` (heures vs présence par date).
|
||||||
|
- **Exports heures annuelles** (par salarié `EmployeeYearlyHoursPrintProvider` + tous `EmployeeYearlyHoursBulkPrintProvider`, via `YearlyHoursExportBuilder`) : **tous les jours sous contrat sont affichés**, même vides ou non saisis (jusqu'à aujourd'hui). Seuls les jours hors contrat sont omis (`buildSegments` : un seul filtre `!$hasData && null === $contract`). Ne pas réintroduire de saut des jours de semaine vides. Samedis/dimanches grisés (`#c0c0c0`) dans les templates `employee-yearly-hours/print*.html.twig`. NB : l'export *tous employés* sur l'année peut dépasser `memory_limit=256M` (Dompdf) — limitation pré-existante, voir avec l'infra si besoin.
|
||||||
- **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai.
|
- **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai.
|
||||||
- **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`.
|
- **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`.
|
||||||
- Absences: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots
|
- Absences: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ Documents complementaires:
|
|||||||
- résolu à la date filtrée (période de contrat couvrant ce jour), pas à aujourd'hui
|
- résolu à la date filtrée (période de contrat couvrant ce jour), pas à aujourd'hui
|
||||||
- masqué si aucun contrat à cette date (cas rarissime en vue jour puisque l'employé est alors déjà filtré)
|
- masqué si aucun contrat à cette date (cas rarissime en vue jour puisque l'employé est alors déjà filtré)
|
||||||
- **Vue Jour (Heures) — contrat à la date affichée** : le mode de suivi (saisie d'heures vs cases de présence), le libellé de contrat et la logique de sauvegarde sont résolus selon la période de contrat valable à la date filtrée (champs `trackingMode`/`weeklyHours`/`contractType`/`contractName` portés par `WorkHourDayContext`, alimentés par `EmployeeContractResolver::resolveForEmployeeAndDate`), et non selon le contrat courant de l'employé. Un salarié passé 39h/35h → Forfait conserve donc la saisie d'heures sur ses dates antérieures à la bascule, et bascule en cases de présence à partir de la date de passage en forfait. La vue Semaine était déjà résolue par date.
|
- **Vue Jour (Heures) — contrat à la date affichée** : le mode de suivi (saisie d'heures vs cases de présence), le libellé de contrat et la logique de sauvegarde sont résolus selon la période de contrat valable à la date filtrée (champs `trackingMode`/`weeklyHours`/`contractType`/`contractName` portés par `WorkHourDayContext`, alimentés par `EmployeeContractResolver::resolveForEmployeeAndDate`), et non selon le contrat courant de l'employé. Un salarié passé 39h/35h → Forfait conserve donc la saisie d'heures sur ses dates antérieures à la bascule, et bascule en cases de présence à partir de la date de passage en forfait. La vue Semaine était déjà résolue par date.
|
||||||
|
- **Exports heures annuelles (par salarié et tous salariés)** : affichent **tous les jours sous contrat**, même vides ou non saisis, jusqu'à la date du jour ; seuls les jours hors contrat (avant embauche, après départ, suspension) sont omis. Les samedis et dimanches sont grisés (gris foncé), les jours fériés en bleu.
|
||||||
|
|
||||||
## 4) Absences
|
## 4) Absences
|
||||||
|
|
||||||
|
|||||||
@@ -640,7 +640,7 @@ export const documentationSections: DocSection[] = [
|
|||||||
requiredLevel: 'admin',
|
requiredLevel: 'admin',
|
||||||
blocks: [
|
blocks: [
|
||||||
{ type: 'paragraph', content: 'Génère un PDF par employé avec le détail jour par jour de ses heures sur une année.' },
|
{ type: 'paragraph', content: 'Génère un PDF par employé avec le détail jour par jour de ses heures sur une année.' },
|
||||||
{ type: 'list', content: 'Accessible depuis la fiche employé (bouton imprimante)\nChoix de l\'année civile (janvier à décembre)\nColonnes adaptées au mode de suivi (TIME, PRESENCE, conducteur)\nSections séparées en cas de changement de contrat en cours d\'année\nLes jours fériés apparaissent toujours (ligne bleue) avec la mention « Férié : {nom} » dans la colonne Absence ; le total reprend les heures contractuelles créditées (hors Forfait)' },
|
{ type: 'list', content: 'Accessible depuis la fiche employé (bouton imprimante)\nChoix de l\'année civile (janvier à décembre)\nColonnes adaptées au mode de suivi (TIME, PRESENCE, conducteur)\nSections séparées en cas de changement de contrat en cours d\'année\nTous les jours sous contrat sont affichés, même vides ou non saisis (jusqu\'à la date du jour) ; seuls les jours hors contrat (avant embauche, après départ) sont omis\nLes samedis et dimanches sont grisés (gris foncé), les jours fériés en bleu avec « Férié : {nom} »' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -264,10 +264,9 @@ class YearlyHoursExportBuilder
|
|||||||
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
||||||
$isWeekend = $isoDay >= 6;
|
$isWeekend = $isoDay >= 6;
|
||||||
|
|
||||||
if (!$hasData && !$isWeekend && !$isHoliday) {
|
// Tous les jours contractés sont affichés, même vides ou non saisis (lignes
|
||||||
continue;
|
// « manquantes » signalées par la RH). Seuls les jours hors contrat (avant
|
||||||
}
|
// embauche, après départ, suspension) sont omis.
|
||||||
|
|
||||||
if (!$hasData && null === $contract) {
|
if (!$hasData && null === $contract) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
td.time { text-align: center; }
|
td.time { text-align: center; }
|
||||||
td.presence { text-align: center; }
|
td.presence { text-align: center; }
|
||||||
td.total { text-align: center; font-weight: bold; }
|
td.total { text-align: center; font-weight: bold; }
|
||||||
tr.weekend td { background: #f3f3f3; color: #555; }
|
tr.weekend td { background: #c0c0c0; color: #333; }
|
||||||
tr.weekend td.date { color: #333; }
|
tr.weekend td.date { color: #333; }
|
||||||
tr.holiday td { background: #e1f5fe; }
|
tr.holiday td { background: #e1f5fe; }
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
td.time { text-align: center; }
|
td.time { text-align: center; }
|
||||||
td.presence { text-align: center; }
|
td.presence { text-align: center; }
|
||||||
td.total { text-align: center; font-weight: bold; }
|
td.total { text-align: center; font-weight: bold; }
|
||||||
tr.weekend td { background: #f3f3f3; color: #555; }
|
tr.weekend td { background: #c0c0c0; color: #333; }
|
||||||
tr.weekend td.date { color: #333; }
|
tr.weekend td.date { color: #333; }
|
||||||
tr.holiday td { background: #e1f5fe; }
|
tr.holiday td { background: #e1f5fe; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user