Retour RH: vue jour par date, RTT mi-semaine, récap salaire & exports, panier de nuit (#21)
Auto Tag Develop / tag (push) Successful in 11s

## Correctifs RH (branche fix/retour-rh)

### Vue Jour (Heures)
- Mode saisie/présence, libellé de contrat et sauvegarde résolus **à la date affichée** (et non au contrat courant). Corrige les salariés passés 39h/35h → Forfait.

### RTT — heures supplémentaires
- Proratisation du **plafond 25%/50%** pour les embauches en milieu de semaine (la bande +25% se décale au lieu de rester bloquée à 43h). Témoin Dylan : 4h à 25% + 3h à 50%.

### Récap salaire (PDF mensuel)
- Forfait : congés imputés **N-1** non affichés et comptés en présence.
- Colonne « Heures payés » **scindée 25% / 50%** (en-tête fusionné).
- **Exclusion des salariés sans contrat** sur le mois (ex. Marine, contrat terminé).

### Exports heures annuelles (par salarié + tous)
- **Tous les jours sous contrat** affichés, même vides/non saisis (corrige les lignes manquantes).
- Samedis/dimanches en **gris plus foncé**.

### Panier de nuit
- **Ne s'applique pas aux conducteurs** (vue semaine + récap salaire).

## Tests
- 11 tests ajoutés. Suite verte hors un test legacy pré-existant dépendant de la date (`EmployeeRttSummaryProviderTest::testNoQueryParamsKeepsLegacyYearDefaulting`, non modifié par cette branche).

## À noter (hors scope)
- L'export heures annuelles *tous salariés* peut dépasser `memory_limit=256M` (Dompdf) — limitation **pré-existante**, non corrigée ici.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #21
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #21.
This commit is contained in:
2026-06-02 06:26:40 +00:00
committed by Autin
parent 8b34a429cc
commit a41bd632cf
21 changed files with 933 additions and 42 deletions
@@ -236,13 +236,19 @@ final readonly class RttRecoveryComputationService
? $this->computeWeeklyCustomReferenceMinutes($weekDays, $employeeContractsByDate)
: $this->computeWeeklyOvertimeReferenceMinutes($weekDays, $employeeContractsByDate);
$overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($weekDays, $employeeContractsByDate);
// Plafond séparant 25 %/50 % : seuil de départ proraté + largeur de la bande +25 %
// (4h pour un 39h, 8h pour un 35h). Il se décale ainsi avec une embauche en milieu
// de semaine au lieu de rester bloqué à 43h, ce qui ouvre la tranche 50 %.
$overtime50StartMinutes = $overtime25StartMinutes + $this->resolveOvertime25BandWidthMinutes($weekAnchorContract);
$weeklyOvertimeTotalMinutes = $isWeekPresenceTracking
? 0
: $weeklyTotalMinutes - $overtimeReferenceMinutes;
$base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : max(0, min($weeklyTotalMinutes, 43 * 60) - $overtime25StartMinutes);
[$rawBase25, $rawBase50] = $this->computeOvertimeBaseMinutes($weeklyTotalMinutes, $overtime25StartMinutes, $overtime50StartMinutes);
$base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : $rawBase25;
$bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : (int) round($base25 * 0.25);
$base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : max(0, $weeklyTotalMinutes - 43 * 60);
$base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : $rawBase50;
$bonus50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : (int) round($base50 * 0.5);
if ($isWeekPresenceTracking || $disableOvertimeBonuses) {
@@ -452,18 +458,31 @@ final readonly class RttRecoveryComputationService
return $total;
}
private function computeOvertime25BonusMinutes(int $weeklyTotalMinutes, int $startMinutes): int
/**
* Largeur (en minutes) de la tranche +25 % pour le contrat d'ancrage de la semaine :
* 4h pour un 39h (39→43), 8h pour un 35h (35→43). Ajoutée au seuil de départ proraté
* pour obtenir le plafond 25 %/50 %.
*/
private function resolveOvertime25BandWidthMinutes(?Contract $contract): int
{
$trancheMinutes = max(0, min($weeklyTotalMinutes, 43 * 60) - $startMinutes);
$hours = $contract?->getWeeklyHours();
$startHours = (null !== $hours && $hours >= 39) ? 39 : 35;
return (int) round($trancheMinutes * 0.25);
return (43 - $startHours) * 60;
}
private function computeOvertime50BonusMinutes(int $weeklyTotalMinutes): int
/**
* Répartit les heures supplémentaires hebdomadaires entre les bases 25 % et 50 %.
* La tranche 25 % court du seuil de départ au plafond ; au-delà du plafond, c'est du 50 %.
*
* @return array{int, int} [base25Minutes, base50Minutes]
*/
private function computeOvertimeBaseMinutes(int $weeklyTotalMinutes, int $overtime25StartMinutes, int $overtime50StartMinutes): array
{
$trancheMinutes = max(0, $weeklyTotalMinutes - (43 * 60));
$base25 = max(0, min($weeklyTotalMinutes, $overtime50StartMinutes) - $overtime25StartMinutes);
$base50 = max(0, $weeklyTotalMinutes - $overtime50StartMinutes);
return (int) round($trancheMinutes * 0.5);
return [$base25, $base50];
}
private function hasDisabledOvertimeBonuses(?Contract $contract, ContractNature $contractNature): bool