# 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
