feat : Ajout du système de RTT sur la page employé avec le repport annuel des heures
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s

This commit is contained in:
2026-03-13 10:26:33 +01:00
parent 1858817649
commit 4a2c3a8eed
29 changed files with 1595 additions and 391 deletions

View File

@@ -0,0 +1,563 @@
# Refonte onglet RTT — Plan d'implémentation
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remplacer la vue annuelle RTT par une vue mensuelle avec tableau détaillé par semaine (base/25%/50%) et un système de paiement à 4 champs.
**Architecture:** Enrichir `RttRecoveryComputationService` pour retourner le détail base/bonus par palier. Modifier l'entité `EmployeeRttPayment` pour stocker 4 valeurs. Réécrire le composant `RttTab.vue` avec navigation mensuelle et tableau 7 colonnes.
**Tech Stack:** Symfony + API Platform + Doctrine (backend), Nuxt 4 + Vue 3 + TypeScript + Tailwind (frontend), PostgreSQL.
**Spec:** `docs/superpowers/specs/2026-03-13-rtt-tab-redesign.md`
---
## Task 1: Enrichir le retour de `RttRecoveryComputationService::computeRecoveryByWeek`
**Files:**
- Create: `src/Dto/Rtt/WeekRecoveryDetail.php`
- Modify: `src/Service/Rtt/RttRecoveryComputationService.php:97-206`
Actuellement `computeRecoveryByWeek` retourne `array<string, int>` (weekKey => totalMinutes). Il faut retourner `array<string, WeekRecoveryDetail>` avec le détail ventilé.
- [ ] **Step 1: Créer le DTO `WeekRecoveryDetail`**
```php
// src/Dto/Rtt/WeekRecoveryDetail.php
<?php
declare(strict_types=1);
namespace App\Dto\Rtt;
final class WeekRecoveryDetail
{
public function __construct(
public int $overtimeMinutes = 0,
public int $base25Minutes = 0,
public int $bonus25Minutes = 0,
public int $base50Minutes = 0,
public int $bonus50Minutes = 0,
public int $totalMinutes = 0,
) {}
}
```
- [ ] **Step 2: Modifier `computeRecoveryByWeek` pour retourner `array<string, WeekRecoveryDetail>`**
Changer le retour de la méthode. Les variables internes existent déjà (`weeklyOvertimeTotalMinutes`, `weeklyOvertime25Minutes`, `weeklyOvertime50Minutes`). Il faut calculer en plus les bases séparées.
La logique de ventilation des heures de base entre palier 25% et palier 50% :
- `base25Minutes` = heures sup dans la tranche 25% = `min(overtimeMinutes, max(0, overtime25StartMinutes - overtimeReferenceMinutes))`... En fait, c'est plus simple :
- `base25Minutes` = `min(weeklyOvertimeTotalMinutes, max(0, 43*60 - overtime25StartMinutes))` quand overtimeTotal > 0
- Plus simplement : `base25Minutes` = heures entre le seuil 25% et 43h, `base50Minutes` = heures au-dessus de 43h
Reprenons la logique existante (lignes 189-202) :
- `overtimeReferenceMinutes` = seuil à partir duquel on compte les heures sup (max(35, weeklyHours) * 60 réparti sur les jours)
- `overtime25StartMinutes` = seuil à partir duquel les heures sup sont à 25% (39h si contrat >= 39h, sinon 35h)
- `weeklyOvertimeTotalMinutes` = max(0, worked - overtimeReference) — total heures sup brutes
- `weeklyOvertime25Minutes` = bonus 25% = round(min(worked, 43*60) - overtime25Start) * 0.25
- `weeklyOvertime50Minutes` = bonus 50% = round(max(0, worked - 43*60)) * 0.5
Pour la ventilation :
- `base25Minutes` = min(weeklyOvertimeTotalMinutes, max(0, 43*60 - overtime25StartMinutes)) — Non, c'est la tranche 25% en termes d'heures travaillées, pas en termes d'heures sup.
En fait :
- Les heures sup brutes = `weeklyOvertimeTotalMinutes` = `worked - overtimeReference`
- Les heures dans le palier 25% = heures entre `overtime25Start` et `min(worked, 43*60)` = c'est `max(0, min(worked, 43*60) - overtime25Start)`. C'est la base sur laquelle le 25% est calculé.
- Les heures dans le palier 50% = heures au-dessus de 43h = `max(0, worked - 43*60)`. C'est la base sur laquelle le 50% est calculé.
Modifier les lignes 191-202 :
```php
$weeklyOvertimeTotalMinutes = $isWeekPresenceTracking
? 0
: max(0, $weeklyTotalMinutes - $overtimeReferenceMinutes);
$base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, min($weeklyTotalMinutes, 43 * 60) - $overtime25StartMinutes);
$bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base25 * 0.25);
$base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, $weeklyTotalMinutes - 43 * 60);
$bonus50 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base50 * 0.5);
$results[$weekKey] = new WeekRecoveryDetail(
overtimeMinutes: $weeklyOvertimeTotalMinutes,
base25Minutes: $base25,
bonus25Minutes: $bonus25,
base50Minutes: $base50,
bonus50Minutes: $bonus50,
totalMinutes: ($isWeekPresenceTracking || $disableOvertimeBonuses)
? 0
: $weeklyOvertimeTotalMinutes + $bonus25 + $bonus50,
);
```
Les cas "zéro" (weekStart vide, limitDate dépassée, etc.) retournent `new WeekRecoveryDetail()` (tout à 0).
- [ ] **Step 3: Adapter `computeTotalRecoveryForExercise` pour retourner un `WeekRecoveryDetail` agrégé**
Cette méthode retournait `int`. Elle doit maintenant retourner un `WeekRecoveryDetail` qui agrège toutes les semaines (somme par champ). Le rollover et le provider en ont besoin pour la ventilation du carry-over.
```php
public function computeTotalRecoveryForExercise(Employee $employee, int $exerciseYear): WeekRecoveryDetail
{
[$from, $to] = $this->resolveExerciseBounds($exerciseYear);
$weeks = $this->buildWeeksForExercise($from, $to);
$weekRanges = array_map(
static fn (array $week): array => [
'month' => (int) $week['month'],
'weekNumber' => (int) $week['weekNumber'],
'start' => $week['start'],
'end' => $week['end'],
],
$weeks
);
$byWeek = $this->computeRecoveryByWeek($employee, $weekRanges, $from, $to, null);
$total = new WeekRecoveryDetail();
foreach ($byWeek as $detail) {
$total = new WeekRecoveryDetail(
overtimeMinutes: $total->overtimeMinutes + $detail->overtimeMinutes,
base25Minutes: $total->base25Minutes + $detail->base25Minutes,
bonus25Minutes: $total->bonus25Minutes + $detail->bonus25Minutes,
base50Minutes: $total->base50Minutes + $detail->base50Minutes,
bonus50Minutes: $total->bonus50Minutes + $detail->bonus50Minutes,
totalMinutes: $total->totalMinutes + $detail->totalMinutes,
);
}
return $total;
}
```
- [ ] **Step 4: Vérifier que le code compile**
Run: `docker exec php-sirh-fpm php bin/console cache:clear`
---
## Task 2: Modifier l'entité `EmployeeRttBalance` (carry-over ventilé) + rollover
**Files:**
- Modify: `src/Entity/EmployeeRttBalance.php`
- Modify: `src/Repository/EmployeeRttBalanceRepository.php`
- Modify: `src/Command/RttRolloverCommand.php`
Le carry-over doit être ventilé sur les mêmes 4 colonnes que le tableau (base25, bonus25, base50, bonus50) pour pouvoir afficher une ligne "Report" dans le mois de juin.
- [ ] **Step 1: Remplacer `openingMinutes` par 4 champs dans `EmployeeRttBalance`**
Remplacer la propriété `$openingMinutes` par :
```php
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 base 25% en minutes.', 'default' => 0])]
private int $openingBase25Minutes = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 bonus 25% en minutes.', 'default' => 0])]
private int $openingBonus25Minutes = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 base 50% en minutes.', 'default' => 0])]
private int $openingBase50Minutes = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 bonus 50% en minutes.', 'default' => 0])]
private int $openingBonus50Minutes = 0;
```
Ajouter les getters/setters. Supprimer `getOpeningMinutes`/`setOpeningMinutes`. Ajouter un helper `getTotalOpeningMinutes()` qui retourne la somme des 4 champs.
- [ ] **Step 2: Adapter `RttRolloverCommand`**
`computeTotalRecoveryForExercise` retourne maintenant un `WeekRecoveryDetail`. Utiliser les 4 champs :
```php
$carry = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $previousYear);
$balance = new EmployeeRttBalance()
->setEmployee($employee)
->setYear($targetYear)
->setOpeningBase25Minutes($carry->base25Minutes)
->setOpeningBonus25Minutes($carry->bonus25Minutes)
->setOpeningBase50Minutes($carry->base50Minutes)
->setOpeningBonus50Minutes($carry->bonus50Minutes)
->setIsLocked(false)
;
```
- [ ] **Step 3: Adapter `EmployeeRttSummaryProvider::resolveCarryMinutes`**
Cette méthode retournait `int`. La renommer en `resolveCarry` et retourner un `WeekRecoveryDetail` :
```php
private function resolveCarry(Employee $employee, int $year): WeekRecoveryDetail
{
$balance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $year);
if (null !== $balance) {
return new WeekRecoveryDetail(
base25Minutes: $balance->getOpeningBase25Minutes(),
bonus25Minutes: $balance->getOpeningBonus25Minutes(),
base50Minutes: $balance->getOpeningBase50Minutes(),
bonus50Minutes: $balance->getOpeningBonus50Minutes(),
totalMinutes: $balance->getTotalOpeningMinutes(),
);
}
return $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $year - 1);
}
```
Adapter le provider pour utiliser le carry ventilé dans le summary :
- `carryFromPreviousYearMinutes` = carry->totalMinutes
- Ajouter les 4 champs de carry dans `EmployeeRttSummary` pour le frontend
- [ ] **Step 4: Ajouter les champs carry dans `EmployeeRttSummary`**
```php
public int $carryBase25Minutes = 0;
public int $carryBonus25Minutes = 0;
public int $carryBase50Minutes = 0;
public int $carryBonus50Minutes = 0;
```
- [ ] **Step 5: Générer et exécuter la migration**
Run: `docker exec php-sirh-fpm php bin/console doctrine:migrations:diff`
Run: `docker exec php-sirh-fpm php bin/console doctrine:migrations:migrate --no-interaction`
Note : faire la migration après la Task 3 (EmployeeRttPayment) pour regrouper les changements dans une seule migration.
---
## Task 3: Modifier l'entité `EmployeeRttPayment` et la migration
**Files:**
- Modify: `src/Entity/EmployeeRttPayment.php`
- Modify: `src/Repository/EmployeeRttPaymentRepository.php`
- [ ] **Step 1: Remplacer `minutes` + `rate` par 4 champs dans l'entité**
Remplacer les propriétés `$minutes` et `$rate` par :
```php
#[ORM\Column(type: 'integer', options: ['comment' => 'Base heures palier 25% en minutes.', 'default' => 0])]
private int $base25Minutes = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Bonus 25% en minutes.', 'default' => 0])]
private int $bonus25Minutes = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Base heures palier 50% en minutes.', 'default' => 0])]
private int $base50Minutes = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Bonus 50% en minutes.', 'default' => 0])]
private int $bonus50Minutes = 0;
```
Ajouter les getters/setters correspondants. Supprimer `getMinutes`/`setMinutes`/`getRate`/`setRate`.
- [ ] **Step 2: Adapter le repository**
Remplacer `findOneByEmployeeYearMonthRate` par `findOneByEmployeeYearMonth` (plus besoin du rate) :
```php
public function findOneByEmployeeYearMonth(Employee $employee, int $year, int $month): ?EmployeeRttPayment
{
return $this->findOneBy([
'employee' => $employee,
'year' => $year,
'month' => $month,
]);
}
```
- [ ] **Step 3: Générer et vérifier la migration**
Run: `docker exec php-sirh-fpm php bin/console doctrine:migrations:diff`
Vérifier que la migration :
- Ajoute `base25_minutes`, `bonus25_minutes`, `base50_minutes`, `bonus50_minutes`
- Supprime `minutes` et `rate`
Run: `docker exec php-sirh-fpm php bin/console doctrine:migrations:migrate --no-interaction`
---
## Task 3: Adapter le DTO `RttMonthPayment` et `EmployeeRttWeekSummary`
**Files:**
- Modify: `src/Dto/Rtt/RttMonthPayment.php`
- Modify: `src/Dto/Rtt/EmployeeRttWeekSummary.php`
- [ ] **Step 1: Modifier `RttMonthPayment`**
Remplacer `paidMinutes25` et `paidMinutes50` par les 4 champs :
```php
final class RttMonthPayment
{
public function __construct(
public int $month,
public int $paidBase25Minutes = 0,
public int $paidBonus25Minutes = 0,
public int $paidBase50Minutes = 0,
public int $paidBonus50Minutes = 0,
) {}
}
```
- [ ] **Step 2: Enrichir `EmployeeRttWeekSummary`**
Ajouter les champs de détail :
```php
final class EmployeeRttWeekSummary
{
public function __construct(
public int $month,
public int $weekNumber,
public string $weekStart,
public string $weekEnd,
public int $overtimeMinutes = 0,
public int $base25Minutes = 0,
public int $bonus25Minutes = 0,
public int $base50Minutes = 0,
public int $bonus50Minutes = 0,
public int $totalMinutes = 0,
) {}
}
```
Supprimer l'ancien champ `recoveryMinutes`.
---
## Task 4: Adapter le provider et le processor backend
**Files:**
- Modify: `src/State/EmployeeRttSummaryProvider.php`
- Modify: `src/ApiResource/EmployeeRttSummary.php`
- Modify: `src/ApiResource/EmployeeRttPaymentInput.php`
- Modify: `src/State/EmployeeRttPaymentProcessor.php`
- [ ] **Step 1: Adapter `EmployeeRttSummaryProvider::provide`**
Le mapping des semaines (ligne 87-96) doit utiliser les nouveaux champs du `WeekRecoveryDetail` :
```php
$summary->weeks = array_map(
static function (array $week) use ($currentByWeekStart) {
$detail = $currentByWeekStart[$week['start']->format('Y-m-d')] ?? new WeekRecoveryDetail();
return new EmployeeRttWeekSummary(
month: (int) $week['month'],
weekNumber: (int) $week['weekNumber'],
weekStart: $week['start']->format('Y-m-d'),
weekEnd: $week['end']->format('Y-m-d'),
overtimeMinutes: $detail->overtimeMinutes,
base25Minutes: $detail->base25Minutes,
bonus25Minutes: $detail->bonus25Minutes,
base50Minutes: $detail->base50Minutes,
bonus50Minutes: $detail->bonus50Minutes,
totalMinutes: $detail->totalMinutes,
);
},
$weekRanges
);
```
Le `currentYearRecoveryMinutes` doit sommer les `totalMinutes` :
```php
$summary->currentYearRecoveryMinutes = array_sum(
array_map(static fn (WeekRecoveryDetail $d) => $d->totalMinutes, $currentByWeekStart)
);
```
Adapter l'agrégation des paiements (lignes 98-121) pour les 4 champs :
```php
foreach ($payments as $payment) {
$m = $payment->getMonth();
if (!isset($monthBuckets[$m])) {
$monthBuckets[$m] = ['base25' => 0, 'bonus25' => 0, 'base50' => 0, 'bonus50' => 0];
}
$monthBuckets[$m]['base25'] += $payment->getBase25Minutes();
$monthBuckets[$m]['bonus25'] += $payment->getBonus25Minutes();
$monthBuckets[$m]['base50'] += $payment->getBase50Minutes();
$monthBuckets[$m]['bonus50'] += $payment->getBonus50Minutes();
}
foreach ($monthBuckets as $m => $bucket) {
$monthPayments[] = new RttMonthPayment($m, $bucket['base25'], $bucket['bonus25'], $bucket['base50'], $bucket['bonus50']);
$totalPaidMinutes += $bucket['base25'] + $bucket['bonus25'] + $bucket['base50'] + $bucket['bonus50'];
}
```
- [ ] **Step 2: Adapter `EmployeeRttPaymentInput`**
```php
final class EmployeeRttPaymentInput
{
public int $month = 0;
public int $base25Minutes = 0;
public int $bonus25Minutes = 0;
public int $base50Minutes = 0;
public int $bonus50Minutes = 0;
public ?int $year = null;
}
```
- [ ] **Step 3: Adapter `EmployeeRttPaymentProcessor`**
Supprimer la validation du `rate`. Adapter le upsert :
```php
$payment = $this->rttPaymentRepository->findOneByEmployeeYearMonth($employee, $year, $data->month);
if (null === $payment) {
$payment = new EmployeeRttPayment();
$payment->setEmployee($employee);
$payment->setYear($year);
$payment->setMonth($data->month);
$this->entityManager->persist($payment);
}
$payment->setBase25Minutes($data->base25Minutes);
$payment->setBonus25Minutes($data->bonus25Minutes);
$payment->setBase50Minutes($data->base50Minutes);
$payment->setBonus50Minutes($data->bonus50Minutes);
$payment->touch();
$this->entityManager->flush();
```
- [ ] **Step 4: Vérifier**
Run: `docker exec php-sirh-fpm php bin/console cache:clear`
---
## Task 5: Adapter le frontend — DTOs et service
**Files:**
- Modify: `frontend/services/dto/employee-rtt-summary.ts`
- Modify: `frontend/services/employee-rtt-summary.ts`
- [ ] **Step 1: Mettre à jour les types TS**
```typescript
export type EmployeeRttWeekSummary = {
month: number
weekNumber: number
weekStart: string
weekEnd: string
overtimeMinutes: number
base25Minutes: number
bonus25Minutes: number
base50Minutes: number
bonus50Minutes: number
totalMinutes: number
}
export type RttMonthPayment = {
month: number
paidBase25Minutes: number
paidBonus25Minutes: number
paidBase50Minutes: number
paidBonus50Minutes: number
}
export type EmployeeRttSummary = {
year: number
carryFromPreviousYearMinutes: number
carryBase25Minutes: number
carryBonus25Minutes: number
carryBase50Minutes: number
carryBonus50Minutes: number
currentYearRecoveryMinutes: number
totalPaidMinutes: number
availableMinutes: number
weeks: EmployeeRttWeekSummary[]
monthPayments: RttMonthPayment[]
}
```
- [ ] **Step 2: Adapter le service `createRttPayment`**
```typescript
export const createRttPayment = async (
employeeId: number,
month: number,
base25Minutes: number,
bonus25Minutes: number,
base50Minutes: number,
bonus50Minutes: number,
year?: number
) => {
const api = useApi()
const body: Record<string, unknown> = { month, base25Minutes, bonus25Minutes, base50Minutes, bonus50Minutes }
if (year) body.year = year
return api.patch(`/employees/${employeeId}/rtt-payments`, body)
}
```
---
## Task 6: Réécrire `RttTab.vue`
**Files:**
- Modify: `frontend/components/employees/RttTab.vue`
- [ ] **Step 1: Réécrire le composant complet**
Structure du template :
1. En-tête avec navigation mensuelle (flèches `<` `>`) et "RTT À LA DATE DU JOUR : X heure"
2. Tableau 7 colonnes : Semaine | Heure | Base | 25% | Base | 50% | Total
3. Si mois de juin (premier mois de l'exercice) et carry > 0 : ligne "Report" avec les 4 valeurs carry (colonne Heure = "-")
4. 5 lignes semaines (padding si < 5)
5. Ligne Total (somme par colonne, incluant le report si présent)
6. Ligne Payé (valeurs négatives, "-" pour colonne Heure)
7. Ligne Reste (Total - |Payé|, "-" pour colonne Heure)
8. Bouton "+ Payer les RRT"
9. Drawer de paiement avec 5 champs
Script setup :
- `currentMonthIndex` : ref (0-11) pour la navigation dans `orderedMonthIndexes` (toujours [5,6,7,8,9,10,11,0,1,2,3,4] = juin à mai)
- Initialiser `currentMonthIndex` au mois courant dans l'exercice
- `currentMonth` : computed qui retourne le numéro de mois (1-12) basé sur l'index
- `weeksForMonth` : computed filtrant les semaines du summary pour le mois courant, paddé à 5
- `monthPayment` : computed trouvant le paiement du mois dans `summary.monthPayments`
- Totaux par colonne : computed sommant les semaines
- `formatMinutes` : existant, réutiliser (format `Xh` ou `Xh Ym`)
- Navigation : `prevMonth` / `nextMonth` modifiant `currentMonthIndex` avec bornes [0, 11]
Drawer de paiement :
- Champs : Mois (select), Base 25% (number en heures), Heures 25% (number en heures), Base 50% (number en heures), Heures 50% (number en heures)
- Si paiement existant pour le mois sélectionné : pré-remplir en convertissant minutes → heures
- Emit : `submit-rtt-payment` avec les 4 valeurs converties en minutes + le mois
- [ ] **Step 2: Adapter le composant parent**
Chercher où `RttTab` est utilisé et adapter l'event handler `submit-rtt-payment` pour passer les 4 champs au lieu de `(month, minutes, rate)`.
Run: `grep -rn "submit-rtt-payment" frontend/` pour trouver le parent.
---
## Task 7: Test de bout en bout
- [ ] **Step 1: Vérifier le cache et la migration**
```bash
docker exec php-sirh-fpm php bin/console cache:clear
docker exec php-sirh-fpm php bin/console doctrine:migrations:migrate --no-interaction
```
- [ ] **Step 2: Tester l'API**
Vérifier que `GET /api/employees/{id}/rtt-summary` retourne les nouveaux champs par semaine.
Vérifier que `PATCH /api/employees/{id}/rtt-payments` accepte les 4 champs.
- [ ] **Step 3: Tester le frontend**
- Navigation mensuelle (flèches, mois courant par défaut)
- Tableau : vérifier les valeurs par semaine
- Paiement : créer, modifier, vérifier pré-remplissage
- "RTT À LA DATE DU JOUR" : vérifier le cumul

View File

@@ -0,0 +1,117 @@
# Refonte onglet RTT employé
## Contexte
L'onglet RTT actuel affiche une grille annuelle de 12 mois avec les minutes de récupération par semaine. Il doit être remplacé par une vue mensuelle détaillée avec navigation, un tableau ventilé par palier de majoration (25% / 50%), et un système de paiement à 4 champs.
## Maquette de référence
Fichier : `RTT.png` à la racine du projet.
## Structure de la vue
### En-tête
- Navigation mensuelle : `< MOIS ANNÉE >` (flèches gauche/droite)
- Navigation limitée aux mois de l'exercice (juin N-1 à mai N)
- Mois courant affiché par défaut à l'ouverture
- En haut à droite : `RTT À LA DATE DU JOUR : X heure` (cumul annuel toutes semaines confondues)
### Tableau
7 colonnes :
| Semaine | Heure | Base | 25% | Base | 50% | Total |
|---------|-------|------|-----|------|-----|-------|
- **Semaine** : label "Semaine 1" à "Semaine 5" (toujours 5 lignes, vide si le mois n'a que 4 semaines)
- **Heure** : heures supplémentaires brutes de la semaine
- **Base** (1er) : heures de base dans le palier 25% (heures entre 35h et 39h pour un contrat 39h)
- **25%** : bonus = base 25% × 0.25
- **Base** (2e) : heures de base dans le palier 50% (heures au-delà de 43h)
- **50%** : bonus = base 50% × 0.50
- **Total** : somme de toutes les bases + tous les bonus
### Lignes de synthèse
- **Total** : somme des 5 semaines par colonne
- **Payé** : montants payés pour ce mois (affichés en négatif). Colonne "Heure" = "-"
- **Reste** : Total - |Payé| par colonne. Colonne "Heure" = "-"
### Bouton
`+ Payer les RRT` en bas, centré. Ouvre un drawer.
## Drawer de paiement
Champs :
1. **Mois** (select) : liste des mois de l'exercice
2. **Base 25%** (number, en heures)
3. **Heures 25%** (number, en heures)
4. **Base 50%** (number, en heures)
5. **Heures 50%** (number, en heures)
Si des paiements existent pour le mois sélectionné, le formulaire est pré-rempli pour modification.
Boutons : Annuler / Enregistrer.
## Rattachement semaine → mois
Règle existante conservée : une semaine est rattachée au mois de son **samedi** (voir `RttRecoveryComputationService::buildWeeksForExercise`).
## Backend
### Modification de `EmployeeRttSummary`
Le provider retourne les données pour un mois donné (paramètre query `?month=X`) en plus du cumul annuel.
Nouvelles données par semaine :
- `overtimeMinutes` : heures sup brutes
- `base25Minutes` : base palier 25%
- `bonus25Minutes` : bonus 25%
- `base50Minutes` : base palier 50%
- `bonus50Minutes` : bonus 50%
- `totalMinutes` : somme base + bonus
### Modification de `EmployeeRttPayment`
Remplacer les champs `minutes` (int) + `rate` (int 25/50) par :
- `base25Minutes` (int)
- `bonus25Minutes` (int)
- `base50Minutes` (int)
- `bonus50Minutes` (int)
Migration Doctrine nécessaire.
### Modification de `EmployeeRttPaymentInput`
Adapter les champs pour correspondre aux 4 nouvelles valeurs.
### Modification de `RttRecoveryComputationService`
`computeRecoveryByWeek` retourne déjà les minutes totales. Il faut enrichir le retour pour ventiler base/bonus par palier. La logique de calcul des paliers existe déjà en interne, il suffit de l'exposer.
## Frontend
### Stockage vs affichage
- Backend : stockage en **minutes** (inchangé)
- Frontend : conversion minutes ↔ heures à l'affichage et à la saisie
### Réécriture de `RttTab.vue`
- Supprimer la grille annuelle de 12 mois
- Navigation mensuelle avec état réactif (mois courant)
- Tableau HTML avec les 7 colonnes décrites
- 5 lignes semaines + Total + Payé + Reste
- Formatage en "Xh" ou "Xh Ym" (ex: "6h 30m")
### Modification du DTO TypeScript
Adapter `EmployeeRttSummary` et `EmployeeRttWeekSummary` pour les nouveaux champs.
## Unités de conversion
- Affichage : heures et minutes (ex: "6h 30m", "30 m")
- Saisie paiement : en heures décimales (number input)
- Stockage : minutes entières (int)