# Contract Phase View — Plan d'implémentation

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Permettre à la RH de consulter les onglets Congés et RTT d'un employé selon une **phase de contrat passée** (groupe d'`EmployeeContractPeriod` consécutifs partageant `contract.type` + `weeklyHours` + `isDriver`) via un picker global en haut de la fiche employé, sans changer le comportement par défaut sur la phase courante.

**Architecture :** Un nouveau service backend `EmployeeContractPhaseResolver` calcule les phases depuis `Employee::getContractPeriods()`. Les providers `EmployeeLeaveSummaryProvider` et `EmployeeRttSummaryProvider` acceptent un `?phaseId` qui cape période, exercice par défaut et règles de calcul à cette phase. Un picker frontend (`MalioSelect`) en haut de `pages/employees/[id].vue` pilote la phase consultée et un bandeau d'information signale les modes passés. Les composables `useEmployeeLeave` et `useEmployeeRtt` propagent `phaseId` sur leurs appels API et bornent leur sélecteur d'année interne.

**Tech Stack :** Symfony + API Platform + Doctrine (backend), PHPUnit (tests), Nuxt 4 + Vue 3 + TypeScript + Tailwind (frontend), `@malio/layer-ui` (composants UI), `make test` pour la suite PHPUnit, `cd frontend && npm run typecheck` pour la TS.

**Spec source :** `docs/superpowers/specs/2026-05-19-contract-phase-view-design.md`.

---

## File Map

**Backend - nouveaux fichiers**
- `src/Service/Contracts/EmployeeContractPhaseResolver.php` — service de calcul des phases
- `src/Dto/Contracts/ContractPhase.php` — DTO retourné par le resolver et exposé via API
- `tests/Service/Contracts/EmployeeContractPhaseResolverTest.php` — tests unitaires du resolver

**Backend - fichiers modifiés**
- `src/Entity/Employee.php` — nouveau getter `getContractPhases()` exposé en `employee:read`
- `src/State/EmployeeLeaveSummaryProvider.php` — accepte `?phaseId`, cape calculs sur la phase
- `src/State/EmployeeRttSummaryProvider.php` — accepte `?phaseId`, cape exercice sur la phase
- `src/State/EmployeeRttPaymentProcessor.php` — autorise paiement sur dernier exercice d'une phase passée
- `tests/State/EmployeeLeaveSummaryProviderTest.php` — cas de phase passée et transition
- `tests/State/EmployeeRttSummaryProviderTest.php` — nouveau (si manquant)
- `tests/State/EmployeeRttPaymentProcessorTest.php` — nouveau (si manquant)

**Frontend - nouveaux fichiers**
- `frontend/services/dto/contract-phase.ts` — type TS pour `ContractPhase`
- `frontend/composables/useEmployeeContractPhase.ts` — composable du picker

**Frontend - fichiers modifiés**
- `frontend/services/dto/employee.ts` — ajout du champ `contractPhases`
- `frontend/services/employee-leave-summary.ts` — paramètre optionnel `phaseId`
- `frontend/services/employee-rtt-summary.ts` — paramètre optionnel `phaseId`
- `frontend/composables/useEmployeeLeave.ts` — propagation `phaseId`, borne `availableLeaveYears` à la phase
- `frontend/composables/useEmployeeRtt.ts` — idem
- `frontend/composables/useEmployeeDetailPage.ts` — `showRttTab` driver par la phase sélectionnée
- `frontend/pages/employees/[id].vue` — picker + bandeau
- `frontend/components/employees/RttTab.vue` — désactivation conditionnelle de `+ Payer les RTT`

**Documentation**
- `doc/contract-phase-view.md` (nouveau)
- `doc/leave-tab.md` (mis à jour)
- `doc/rtt-tab.md` (mis à jour)
- `frontend/data/documentation-content.ts` (mis à jour)
- `CLAUDE.md` (mis à jour)

---

### Task 1: Backend — DTO `ContractPhase` + service `EmployeeContractPhaseResolver`

**Files:**
- Create: `src/Dto/Contracts/ContractPhase.php`
- Create: `src/Service/Contracts/EmployeeContractPhaseResolver.php`
- Test: `tests/Service/Contracts/EmployeeContractPhaseResolverTest.php`

- [ ] **Step 1.1: Créer le DTO `ContractPhase`**

```php
<?php

declare(strict_types=1);

namespace App\Dto\Contracts;

use App\Enum\ContractType;
use DateTimeImmutable;

final readonly class ContractPhase
{
    /**
     * @param list<int> $periodIds
     */
    public function __construct(
        public int $id,
        public ContractType $contractType,
        public ?int $weeklyHours,
        public bool $isDriver,
        public DateTimeImmutable $startDate,
        public ?DateTimeImmutable $endDate,
        public array $periodIds,
        public bool $isCurrent,
    ) {}
}
```

- [ ] **Step 1.2: Écrire le test du resolver — cas mono-période**

```php
<?php

declare(strict_types=1);

namespace App\Tests\Service\Contracts;

use App\Entity\Contract;
use App\Entity\Employee;
use App\Entity\EmployeeContractPeriod;
use App\Enum\ContractNature;
use App\Enum\ContractType;
use App\Enum\TrackingMode;
use App\Service\Contracts\EmployeeContractPhaseResolver;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;

final class EmployeeContractPhaseResolverTest extends TestCase
{
    public function testSinglePeriodYieldsSinglePhaseMarkedCurrent(): void
    {
        $employee = $this->buildEmployee([
            ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => null],
        ]);

        $phases = (new EmployeeContractPhaseResolver())->resolvePhases($employee);

        self::assertCount(1, $phases);
        self::assertSame(ContractType::H39, $phases[0]->contractType);
        self::assertTrue($phases[0]->isCurrent);
        self::assertNull($phases[0]->endDate);
    }

    /**
     * @param list<array{type: ContractType, hours: ?int, driver: bool, start: string, end: ?string}> $periodsSpec
     */
    private function buildEmployee(array $periodsSpec): Employee
    {
        $employee = new Employee();
        $id = 0;
        foreach ($periodsSpec as $spec) {
            $contract = new Contract();
            $contract->setName($spec['type']->value);
            $contract->setTrackingMode(
                ContractType::FORFAIT === $spec['type'] ? TrackingMode::PRESENCE->value : TrackingMode::TIME->value
            );
            $contract->setWeeklyHours($spec['hours']);

            $period = new EmployeeContractPeriod();
            $reflection = new \ReflectionProperty(EmployeeContractPeriod::class, 'id');
            $reflection->setValue($period, ++$id);
            $period->setEmployee($employee);
            $period->setContract($contract);
            $period->setStartDate(new DateTimeImmutable($spec['start']));
            $period->setEndDate(null !== $spec['end'] ? new DateTimeImmutable($spec['end']) : null);
            $period->setContractNatureEnum(ContractNature::CDI);
            $period->setIsDriver($spec['driver']);
            $employee->addContractPeriod($period);
        }

        return $employee;
    }
}
```

- [ ] **Step 1.3: Lancer le test pour vérifier qu'il échoue (resolver inexistant)**

Run: `make test`
Expected: FAIL avec `Class "App\Service\Contracts\EmployeeContractPhaseResolver" not found`

- [ ] **Step 1.4: Implémenter le resolver (version minimale qui groupe par signature)**

```php
<?php

declare(strict_types=1);

namespace App\Service\Contracts;

use App\Dto\Contracts\ContractPhase;
use App\Entity\Employee;
use App\Entity\EmployeeContractPeriod;
use DateTimeImmutable;

final readonly class EmployeeContractPhaseResolver
{
    /**
     * @return list<ContractPhase>
     */
    public function resolvePhases(Employee $employee): array
    {
        $periods = $employee->getContractPeriods()->toArray();
        usort(
            $periods,
            static fn (EmployeeContractPeriod $a, EmployeeContractPeriod $b): int => $a->getStartDate() <=> $b->getStartDate()
        );

        $today  = new DateTimeImmutable('today');
        $phases = [];
        $group  = [];
        $signature = null;

        foreach ($periods as $period) {
            $currentSignature = $this->signature($period);
            if ($signature !== null && $currentSignature !== $signature) {
                $phases[] = $this->buildPhase($group, $today);
                $group    = [];
            }
            $group[]   = $period;
            $signature = $currentSignature;
        }

        if ([] !== $group) {
            $phases[] = $this->buildPhase($group, $today);
        }

        // Plus récente d'abord.
        return array_reverse($phases);
    }

    private function signature(EmployeeContractPeriod $period): string
    {
        $contract = $period->getContract();
        $type     = $contract?->getType()->value ?? '';
        $hours    = $contract?->getWeeklyHours() ?? -1;
        $driver   = $period->getIsDriver() ? '1' : '0';

        return sprintf('%s|%d|%s', $type, $hours, $driver);
    }

    /**
     * @param non-empty-list<EmployeeContractPeriod> $group
     */
    private function buildPhase(array $group, DateTimeImmutable $today): ContractPhase
    {
        $first = $group[0];
        $last  = end($group);

        $endDate   = $last->getEndDate();
        $isCurrent = null === $endDate || $endDate >= $today;

        $contract = $first->getContract();

        return new ContractPhase(
            id: (int) $first->getId(),
            contractType: $contract?->getType() ?? throw new \LogicException('Phase requires a contract type'),
            weeklyHours: $contract?->getWeeklyHours(),
            isDriver: $first->getIsDriver(),
            startDate: $first->getStartDate(),
            endDate: $endDate,
            periodIds: array_map(static fn (EmployeeContractPeriod $p): int => (int) $p->getId(), $group),
            isCurrent: $isCurrent,
        );
    }
}
```

- [ ] **Step 1.5: Run tests for green**

Run: `make test -- --filter EmployeeContractPhaseResolverTest`
Expected: PASS (1 test).

- [ ] **Step 1.6: Ajouter le test "trois périodes même signature → une phase"**

Dans `EmployeeContractPhaseResolverTest.php`, ajouter :

```php
public function testThreeConsecutivePeriodsSameSignatureCollapseIntoSinglePhase(): void
{
    $employee = $this->buildEmployee([
        ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2021-05-31'],
        ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2021-06-01', 'end' => '2022-05-31'],
        ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2022-06-01', 'end' => null],
    ]);

    $phases = (new EmployeeContractPhaseResolver())->resolvePhases($employee);

    self::assertCount(1, $phases);
    self::assertCount(3, $phases[0]->periodIds);
    self::assertSame('2020-06-01', $phases[0]->startDate->format('Y-m-d'));
    self::assertNull($phases[0]->endDate);
}
```

- [ ] **Step 1.7: Run, expect PASS**

Run: `make test -- --filter EmployeeContractPhaseResolverTest`

- [ ] **Step 1.8: Ajouter test transition 39h → FORFAIT (deux phases)**

```php
public function testSwitchFromH39ToForfaitProducesTwoPhasesMostRecentFirst(): void
{
    $employee = $this->buildEmployee([
        ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2026-04-30'],
        ['type' => ContractType::FORFAIT, 'hours' => 39, 'driver' => false, 'start' => '2026-05-01', 'end' => null],
    ]);

    $phases = (new EmployeeContractPhaseResolver())->resolvePhases($employee);

    self::assertCount(2, $phases);
    self::assertSame(ContractType::FORFAIT, $phases[0]->contractType);
    self::assertTrue($phases[0]->isCurrent);
    self::assertSame(ContractType::H39, $phases[1]->contractType);
    self::assertFalse($phases[1]->isCurrent);
    self::assertSame('2026-04-30', $phases[1]->endDate?->format('Y-m-d'));
}
```

- [ ] **Step 1.9: Run, expect PASS**

- [ ] **Step 1.10: Ajouter test break par INTERIM (pas de fusion)**

```php
public function testInterimBetweenTwoH39PeriodsBreaksThePhases(): void
{
    $employee = $this->buildEmployee([
        ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2020-06-01', 'end' => '2023-12-31'],
        ['type' => ContractType::INTERIM, 'hours' => null, 'driver' => false, 'start' => '2024-01-01', 'end' => '2024-04-30'],
        ['type' => ContractType::H39, 'hours' => 39, 'driver' => false, 'start' => '2024-05-01', 'end' => null],
    ]);

    $phases = (new EmployeeContractPhaseResolver())->resolvePhases($employee);

    self::assertCount(3, $phases);
    self::assertSame(ContractType::H39, $phases[0]->contractType);
    self::assertSame(ContractType::INTERIM, $phases[1]->contractType);
    self::assertSame(ContractType::H39, $phases[2]->contractType);
}
```

- [ ] **Step 1.11: Run, expect PASS**

- [ ] **Step 1.12: Ajouter tests `weeklyHours` et `isDriver` qui cassent la signature**

```php
public function testCustomPhasesSplitOnWeeklyHoursChange(): void
{
    $employee = $this->buildEmployee([
        ['type' => ContractType::CUSTOM, 'hours' => 28, 'driver' => false, 'start' => '2024-01-01', 'end' => '2024-12-31'],
        ['type' => ContractType::CUSTOM, 'hours' => 30, 'driver' => false, 'start' => '2025-01-01', 'end' => null],
    ]);

    $phases = (new EmployeeContractPhaseResolver())->resolvePhases($employee);

    self::assertCount(2, $phases);
    self::assertSame(30, $phases[0]->weeklyHours);
    self::assertSame(28, $phases[1]->weeklyHours);
}

public function testPhasesSplitOnIsDriverChange(): void
{
    $employee = $this->buildEmployee([
        ['type' => ContractType::H35, 'hours' => 35, 'driver' => false, 'start' => '2023-01-01', 'end' => '2024-12-31'],
        ['type' => ContractType::H35, 'hours' => 35, 'driver' => true, 'start' => '2025-01-01', 'end' => null],
    ]);

    $phases = (new EmployeeContractPhaseResolver())->resolvePhases($employee);

    self::assertCount(2, $phases);
    self::assertTrue($phases[0]->isDriver);
    self::assertFalse($phases[1]->isDriver);
}
```

- [ ] **Step 1.13: Run, expect PASS pour tous les tests du resolver**

- [ ] **Step 1.14: Commit**

```bash
git add src/Dto/Contracts/ContractPhase.php src/Service/Contracts/EmployeeContractPhaseResolver.php tests/Service/Contracts/EmployeeContractPhaseResolverTest.php
git commit -m "feat(contracts) : add EmployeeContractPhaseResolver service"
```

---

### Task 2: Backend — exposer `contractPhases` sur l'API Employee

**Files:**
- Modify: `src/Entity/Employee.php` (ajout d'un getter virtuel)
- Modify: `tests/State/EmployeeWriteProcessorTest.php` (vérifier que la sérialisation reste valide)

- [ ] **Step 2.1: Ajouter le getter `getContractPhases` dans `Employee.php`**

À ajouter juste après `getContractHistory()` (autour de la ligne 430) :

```php
/**
 * @return list<array{
 *   id: int,
 *   contractType: string,
 *   weeklyHours: ?int,
 *   isDriver: bool,
 *   startDate: string,
 *   endDate: ?string,
 *   periodIds: list<int>,
 *   isCurrent: bool
 * }>
 */
#[Groups(['employee:read'])]
public function getContractPhases(): array
{
    $resolver = new \App\Service\Contracts\EmployeeContractPhaseResolver();

    return array_map(
        static fn (\App\Dto\Contracts\ContractPhase $phase): array => [
            'id'           => $phase->id,
            'contractType' => $phase->contractType->value,
            'weeklyHours'  => $phase->weeklyHours,
            'isDriver'     => $phase->isDriver,
            'startDate'    => $phase->startDate->format('Y-m-d'),
            'endDate'      => $phase->endDate?->format('Y-m-d'),
            'periodIds'    => $phase->periodIds,
            'isCurrent'    => $phase->isCurrent,
        ],
        $resolver->resolvePhases($this),
    );
}
```

Note : on instancie le resolver inline plutôt que de l'injecter (l'entité ne fait pas d'injection). Le resolver n'a pas de dépendance, donc c'est sûr.

- [ ] **Step 2.2: Ajouter use statements en haut de `Employee.php`**

Ajouter dans la section `use` :
```php
use App\Dto\Contracts\ContractPhase;
use App\Service\Contracts\EmployeeContractPhaseResolver;
```

Et simplifier le getter en remplaçant les FQCN par les imports.

- [ ] **Step 2.3: Vérifier que la suite PHPUnit passe encore**

Run: `make test`
Expected: tous verts.

- [ ] **Step 2.4: Vérifier le rendu de l'API (sanity check manuel)**

```bash
docker exec -t -u www-data php-sirh-fpm php bin/console debug:container --tag=api_platform.metadata.resource.metadata_collection_factory | head
```

(juste pour s'assurer qu'il n'y a pas de cache stale — pas d'attendu strict)

- [ ] **Step 2.5: Commit**

```bash
git add src/Entity/Employee.php
git commit -m "feat(employee) : expose contractPhases on read API"
```

---

### Task 3: Backend — `EmployeeLeaveSummaryProvider` accepte `?phaseId`

**Files:**
- Modify: `src/State/EmployeeLeaveSummaryProvider.php`
- Test: `tests/State/EmployeeLeaveSummaryProviderTest.php`

- [ ] **Step 3.1: Ajouter une dépendance vers le resolver**

Dans le constructeur de `EmployeeLeaveSummaryProvider`, injecter :
```php
private EmployeeContractPhaseResolver $phaseResolver,
```

et `use App\Service\Contracts\EmployeeContractPhaseResolver;` en haut.

- [ ] **Step 3.2: Écrire un test : phase passée 39h, le picker `?phaseId=N` retourne du `CDI_CDD_NON_FORFAIT`**

Dans `tests/State/EmployeeLeaveSummaryProviderTest.php`, après les tests existants, ajouter un test functional ou unit. Si l'existant utilise un harness simple (mock RequestStack), s'en inspirer. Sinon créer une fonction `withPhaseQuery(int $phaseId)` qui mock le RequestStack pour renvoyer `?phaseId=N`.

Test cible :
```php
public function testPastH39PhaseAppliesNonForfaitRuleCodeEvenWhenCurrentIsForfait(): void
{
    $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01'); // 39h → FORFAIT
    $phases   = (new EmployeeContractPhaseResolver())->resolvePhases($employee);
    $h39Phase = $phases[1]; // plus ancienne = 39h

    $provider = $this->buildProvider(request: ['phaseId' => (string) $h39Phase->id]);

    $summary = $provider->provide(/* Operation */ $this->operation(), ['id' => 1]);

    self::assertSame('CDI_CDD_NON_FORFAIT', $summary->ruleCode);
}
```

Adapter aux helpers de la classe de test existante. Si la classe n'a pas encore de helper builder, l'ajouter en `private function`.

- [ ] **Step 3.3: Run, expect FAIL (le provider ignore encore `phaseId`)**

Run: `make test -- --filter EmployeeLeaveSummaryProviderTest`

- [ ] **Step 3.4: Implémenter la résolution de la phase cible**

Dans `EmployeeLeaveSummaryProvider.php` :

a. Ajouter une méthode privée :
```php
private function resolveTargetPhase(Employee $employee): ContractPhase
{
    $raw = $this->requestStack->getCurrentRequest()?->query->get('phaseId');
    $phases = $this->phaseResolver->resolvePhases($employee);
    if ([] === $phases) {
        throw new UnprocessableEntityHttpException('Employee has no contract phase.');
    }

    if (null === $raw || '' === $raw) {
        // Phase courante par défaut = celle marquée isCurrent ou, à défaut, la plus récente.
        foreach ($phases as $phase) {
            if ($phase->isCurrent) {
                return $phase;
            }
        }
        return $phases[0];
    }

    if (!preg_match('/^\d+$/', (string) $raw)) {
        throw new UnprocessableEntityHttpException('phaseId must be a positive integer.');
    }
    $phaseId = (int) $raw;
    foreach ($phases as $phase) {
        if ($phase->id === $phaseId) {
            return $phase;
        }
    }
    throw new UnprocessableEntityHttpException('phaseId does not match any phase of this employee.');
}
```

b. Au début de `provide()`, après la résolution de l'employé, appeler `$phase = $this->resolveTargetPhase($employee);`.

c. Refactoriser pour propager `$phase` aux méthodes internes :
- `resolveYear(Employee $e)` → `resolveYear(Employee $e, ContractPhase $phase)` (signature change). Si `phaseId` fourni et pas de `year`, default = année calculée à partir de `$phase->endDate ?? today` selon que la phase est FORFAIT ou non.
- `resolveLeavePolicy(Employee $e, $from, $to)` → `resolveLeavePolicy(Employee $e, ContractPhase $phase, $from, $to)`. Utiliser `$phase->contractType` au lieu de `$employee->getContract()?->getType()` et `$phase->weeklyHours` au lieu de `$employee->getContract()?->getWeeklyHours()`.
- `resolvePeriodBounds(Employee $e, int $year)` → `resolvePeriodBounds(Employee $e, ContractPhase $phase, int $year)`. Caper avec :
  ```php
  if ($phase->startDate > $from) { $from = $phase->startDate; }
  if (null !== $phase->endDate && $phase->endDate < $to) { $to = $phase->endDate; }
  ```
- `resolveForfaitYearBounds` et `resolveLeavePeriodBounds` (internes) : recevoir la phase et appliquer le cap.
- `resolveAccrualCalculationEndDate(...)` et `resolveTakenCalculationEndDate(...)` : caper additionnellement sur `$phase->endDate` quand `!$phase->isCurrent`.
- `resolveFirstComputationYear(Employee $e)` → `resolveFirstComputationYear(Employee $e, ContractPhase $phase)` : ne pas remonter avant l'exercice contenant `$phase->startDate`.

Note : on remplace systématiquement `$employee->getContract()?->getType()` et `$employee->getContract()?->getWeeklyHours()` par les valeurs de la phase. La sémantique de `getContract()` ne change pas (elle reste le contrat courant) ; on l'évite simplement dans ce provider quand `phaseId` est explicite.

d. Côté `resolveYear` quand un `?year` explicite est hors phase : clamp silencieux dans la plage [premier exercice de la phase, dernier exercice de la phase]. Implementer une fonction helper `clampYearToPhase(int $year, ContractPhase $phase, bool $isForfait): int`.

- [ ] **Step 3.5: Run, expect PASS du nouveau test**

Run: `make test -- --filter EmployeeLeaveSummaryProviderTest`

- [ ] **Step 3.6: Ajouter test transition d'exercice (cap aux bornes de la phase)**

```php
public function testTransitionExerciseOnH39PhaseIsCappedAtPhaseEndDate(): void
{
    // 39h jusqu'au 2026-04-30, FORFAIT à partir du 2026-05-01.
    // L'exercice de congé Juin 2025 → Mai 2026 sur la phase 39h doit être borné à 2026-04-30.
    // Soldes acquis attendus = 11 mois × 2.0833 ≈ 22.92 jours (+5 samedis prorata).
    $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
    $phases   = (new EmployeeContractPhaseResolver())->resolvePhases($employee);
    $h39Phase = $phases[1];

    $provider = $this->buildProvider(request: ['phaseId' => (string) $h39Phase->id, 'year' => '2026']);

    $summary = $provider->provide($this->operation(), ['id' => 1]);

    self::assertSame('CDI_CDD_NON_FORFAIT', $summary->ruleCode);
    // L'exercice est capé à 11 mois plein, donc l'acquis devrait être ≈ 22.9j.
    self::assertEqualsWithDelta(22.92, $summary->acquiredDays, 0.5);
}
```

- [ ] **Step 3.7: Run, expect PASS**

- [ ] **Step 3.8: Ajouter test clamp silencieux de `?year` hors phase**

```php
public function testYearOutsidePhaseRangeIsSilentlyClampedToPhaseLastExercise(): void
{
    $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
    $phases   = (new EmployeeContractPhaseResolver())->resolvePhases($employee);
    $h39Phase = $phases[1];

    // 2030 est hors phase (la phase 39h se termine en 2026) → clamp à 2026.
    $provider = $this->buildProvider(request: ['phaseId' => (string) $h39Phase->id, 'year' => '2030']);

    $summary = $provider->provide($this->operation(), ['id' => 1]);

    self::assertSame(2026, $summary->year);
}
```

- [ ] **Step 3.9: Run, expect PASS**

- [ ] **Step 3.10: Tester que `phaseId` invalide → 422**

```php
public function testInvalidPhaseIdReturns422(): void
{
    $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
    $provider = $this->buildProvider(request: ['phaseId' => '99999']);

    $this->expectException(UnprocessableEntityHttpException::class);
    $provider->provide($this->operation(), ['id' => 1]);
}
```

- [ ] **Step 3.11: Run, expect PASS**

- [ ] **Step 3.12: Tester que comportement par défaut (pas de `phaseId`) est inchangé**

Un test existant sans `phaseId` doit toujours passer. Lancer toute la suite :

Run: `make test`
Expected: tous verts.

- [ ] **Step 3.13: Commit**

```bash
git add src/State/EmployeeLeaveSummaryProvider.php tests/State/EmployeeLeaveSummaryProviderTest.php
git commit -m "feat(leave) : phaseId support in EmployeeLeaveSummaryProvider"
```

---

### Task 4: Backend — `EmployeeRttSummaryProvider` accepte `?phaseId`

**Files:**
- Modify: `src/State/EmployeeRttSummaryProvider.php`
- Test: `tests/State/EmployeeRttSummaryProviderTest.php` (créer si manquant)

- [ ] **Step 4.1: Vérifier l'existence du test file**

```bash
ls tests/State/EmployeeRttSummaryProviderTest.php 2>/dev/null || echo "absent"
```

Si absent, créer le squelette avec `setUp` standard sur le modèle de `EmployeeLeaveSummaryProviderTest`.

- [ ] **Step 4.2: Injecter le `EmployeeContractPhaseResolver` dans le constructeur**

```php
private EmployeeContractPhaseResolver $phaseResolver,
```

et `use App\Service\Contracts\EmployeeContractPhaseResolver;`.

- [ ] **Step 4.3: Écrire un test "exercice de fin de phase 39h passée — données renvoyées avec cap"**

```php
public function testPastH39PhaseRttSummaryIsCappedAtPhaseEndDate(): void
{
    $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
    $phases   = (new EmployeeContractPhaseResolver())->resolvePhases($employee);
    $h39Phase = $phases[1];

    $provider = $this->buildProvider(request: ['phaseId' => (string) $h39Phase->id, 'year' => '2026']);

    $summary = $provider->provide($this->operation(), ['id' => 1]);

    self::assertSame(2026, $summary->year);
    // Les semaines de mai 2026 ne doivent pas apparaître (phase clôturée au 30/04).
    foreach ($summary->weeks as $week) {
        self::assertLessThanOrEqual('2026-04-30', $week->weekEnd);
    }
}
```

- [ ] **Step 4.4: Run, expect FAIL**

- [ ] **Step 4.5: Implémenter le support `phaseId`**

a. Ajouter `resolveTargetPhase(Employee $e): ContractPhase` (similaire à `EmployeeLeaveSummaryProvider`).

b. Dans `provide()`, appeler `$phase = $this->resolveTargetPhase($employee);`.

c. Modifier `resolveYear()` pour accepter la phase : si `phaseId` fourni et pas de `year`, default = année de l'exercice contenant `$phase->endDate ?? today`. Si `year` fourni hors phase, clamp silencieux.

d. Après `[$periodFrom, $periodTo] = $this->rttRecoveryService->resolveExerciseBounds($year);`, ajouter :
```php
if (!$phase->isCurrent && null !== $phase->endDate && $phase->endDate < $periodTo) {
    $periodTo = $phase->endDate;
}
if ($phase->startDate > $periodFrom) {
    $periodFrom = $phase->startDate;
}
```

e. Adapter la limite des semaines (`$limitDate`) : si `$phase->endDate` est passé et < `$limitDate`, alors `$limitDate = $phase->endDate`.

- [ ] **Step 4.6: Run, expect PASS**

- [ ] **Step 4.7: Test `?year` clamp silencieux pour RTT**

Test analogue au step 3.8 mais sur le summary RTT.

- [ ] **Step 4.8: Test `phaseId` invalide → 422**

Test analogue au step 3.10.

- [ ] **Step 4.9: Test comportement par défaut inchangé**

Run: `make test`

- [ ] **Step 4.10: Commit**

```bash
git add src/State/EmployeeRttSummaryProvider.php tests/State/EmployeeRttSummaryProviderTest.php
git commit -m "feat(rtt) : phaseId support in EmployeeRttSummaryProvider"
```

---

### Task 5: Backend — `EmployeeRttPaymentProcessor` autorise paiement sur dernier exo de phase clôturée

**Files:**
- Modify: `src/State/EmployeeRttPaymentProcessor.php`
- Test: `tests/State/EmployeeRttPaymentProcessorTest.php` (créer si manquant)

- [ ] **Step 5.1: Lire la garde actuelle**

```bash
cat src/State/EmployeeRttPaymentProcessor.php
```

Identifier la ligne où la création est rejetée si l'exercice n'est pas l'exercice courant. (Probablement un check `if ($year !== $currentExerciseYear)`.)

- [ ] **Step 5.2: Écrire le test "paiement autorisé sur exercice de fin de phase clôturée"**

```php
public function testPaymentAllowedOnLastExerciseOfClosedPhase(): void
{
    // Phase 39h clôturée le 2026-04-30, FORFAIT à partir du 2026-05-01.
    // L'exercice 2026 (Juin 2025 → Mai 2026) contient la date de fin de la phase 39h.
    // Le paiement RTT doit être accepté sur l'exercice 2026 même si l'exercice courant est plus récent.
    $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
    $today    = new DateTimeImmutable('2027-01-15'); // exercice courant = 2027
    $processor = $this->buildProcessorWithClock($today);

    $payment = new EmployeeRttPaymentInput();
    $payment->employeeId = $employee->getId();
    $payment->year       = 2026;
    $payment->month      = 4;
    $payment->base25Minutes = 60;

    $result = $processor->process($payment, $this->postOperation(), []);

    self::assertNotNull($result->getId());
}
```

Si la classe de test ou les helpers n'existent pas, créer un harness minimal sur le modèle de `EmployeeLeaveSummaryProviderTest`.

- [ ] **Step 5.3: Run, expect FAIL**

- [ ] **Step 5.4: Modifier le processor**

Remplacer la garde "exercice courant uniquement" par :
```php
// Autoriser si :
// - exercice courant, OU
// - exercice contenant l'endDate d'une phase clôturée de l'employé
$phases = $this->phaseResolver->resolvePhases($employee);
$isLastExerciseOfClosedPhase = false;
foreach ($phases as $phase) {
    if ($phase->isCurrent || null === $phase->endDate) {
        continue;
    }
    $exerciseYearForPhaseEnd = $this->resolveExerciseYearForDate($phase->endDate);
    if ($year === $exerciseYearForPhaseEnd) {
        $isLastExerciseOfClosedPhase = true;
        break;
    }
}

if ($year !== $currentExerciseYear && !$isLastExerciseOfClosedPhase) {
    throw new UnprocessableEntityHttpException('RTT payment is only allowed on the current exercise or the last exercise of a closed contract phase.');
}
```

Injecter `EmployeeContractPhaseResolver` dans le constructeur.

Note : `resolveExerciseYearForDate` = `(int) $date->format('n') >= 6 ? (int) $date->format('Y') + 1 : (int) $date->format('Y');`. À factoriser dans un helper si elle apparaît plusieurs fois.

- [ ] **Step 5.5: Run, expect PASS**

- [ ] **Step 5.6: Test "paiement refusé sur exercice antérieur d'une phase clôturée"**

```php
public function testPaymentRejectedOnEarlierExerciseOfClosedPhase(): void
{
    $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
    $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2027-01-15'));

    $payment = new EmployeeRttPaymentInput();
    $payment->employeeId = $employee->getId();
    $payment->year       = 2024; // exercice antérieur dans la phase 39h
    $payment->month      = 4;
    $payment->base25Minutes = 60;

    $this->expectException(UnprocessableEntityHttpException::class);
    $processor->process($payment, $this->postOperation(), []);
}
```

- [ ] **Step 5.7: Run, expect PASS**

- [ ] **Step 5.8: Vérifier que le test "exercice courant accepté" passe toujours**

(L'existant ou un nouveau test confirmant le comportement par défaut.)

Run: `make test`

- [ ] **Step 5.9: Commit**

```bash
git add src/State/EmployeeRttPaymentProcessor.php tests/State/EmployeeRttPaymentProcessorTest.php
git commit -m "feat(rtt) : allow payment on closed phase last exercise"
```

---

### Task 6: Frontend — TS DTO `ContractPhase` + ajout sur `Employee`

**Files:**
- Create: `frontend/services/dto/contract-phase.ts`
- Modify: `frontend/services/dto/employee.ts`

- [ ] **Step 6.1: Créer le type TS**

`frontend/services/dto/contract-phase.ts` :
```typescript
import type { ContractType } from './contract'

export type ContractPhase = {
  id: number
  contractType: ContractType
  weeklyHours: number | null
  isDriver: boolean
  startDate: string
  endDate: string | null
  periodIds: number[]
  isCurrent: boolean
}
```

- [ ] **Step 6.2: Ajouter `contractPhases` sur `Employee`**

Dans `frontend/services/dto/employee.ts`, ajouter le champ :
```typescript
contractPhases?: ContractPhase[]
```

Import en haut :
```typescript
import type { ContractPhase } from './contract-phase'
```

- [ ] **Step 6.3: Vérifier la compilation TS**

Run: `cd frontend && npx vue-tsc --noEmit`
Expected: aucune erreur.

- [ ] **Step 6.4: Commit**

```bash
git add frontend/services/dto/contract-phase.ts frontend/services/dto/employee.ts
git commit -m "feat(employee) : add contractPhases TS DTO"
```

---

### Task 7: Frontend — composable `useEmployeeContractPhase`

**Files:**
- Create: `frontend/composables/useEmployeeContractPhase.ts`

- [ ] **Step 7.1: Implémenter le composable**

`frontend/composables/useEmployeeContractPhase.ts` :
```typescript
import type { Ref } from 'vue'
import type { Employee } from '~/services/dto/employee'
import type { ContractPhase } from '~/services/dto/contract-phase'
import { CONTRACT_TYPES } from '~/services/dto/contract'

const formatDateFr = (iso: string | null): string => {
  if (!iso) return ''
  const [y, m, d] = iso.split('-')
  return `${d}/${m}/${y}`
}

const formatContractTypeLabel = (phase: ContractPhase): string => {
  switch (phase.contractType) {
    case CONTRACT_TYPES.FORFAIT:
      return 'FORFAIT'
    case CONTRACT_TYPES.H35:
      return '35h'
    case CONTRACT_TYPES.H39:
      return '39h'
    case CONTRACT_TYPES.INTERIM:
      return 'Intérim'
    case CONTRACT_TYPES.CUSTOM:
      return `CUSTOM (${phase.weeklyHours ?? '?'}h)`
    default:
      return String(phase.contractType)
  }
}

export const formatPhaseLabel = (phase: ContractPhase): string => {
  const base = formatContractTypeLabel(phase)
  const driver = phase.isDriver ? ' (driver)' : ''
  const dates = phase.endDate
    ? `${formatDateFr(phase.startDate)} → ${formatDateFr(phase.endDate)}`
    : `depuis ${formatDateFr(phase.startDate)}`
  const suffix = phase.isCurrent ? ' (actuel)' : ''
  return `${base}${driver} — ${dates}${suffix}`
}

export const useEmployeeContractPhase = (employee: Ref<Employee | null>) => {
  const selectedPhaseId = ref<number | null>(null)

  const availablePhases = computed<ContractPhase[]>(() => employee.value?.contractPhases ?? [])

  const currentPhase = computed<ContractPhase | null>(() => {
    return availablePhases.value.find((p) => p.isCurrent) ?? availablePhases.value[0] ?? null
  })

  const selectedPhase = computed<ContractPhase | null>(() => {
    if (selectedPhaseId.value === null) return currentPhase.value
    return availablePhases.value.find((p) => p.id === selectedPhaseId.value) ?? currentPhase.value
  })

  const isViewingPastPhase = computed<boolean>(() => {
    if (!selectedPhase.value || !currentPhase.value) return false
    return selectedPhase.value.id !== currentPhase.value.id
  })

  const phaseOptions = computed(() =>
    availablePhases.value.map((p) => ({ value: p.id, label: formatPhaseLabel(p) }))
  )

  const showPicker = computed(() => availablePhases.value.length > 1)

  const setSelectedPhase = (phaseId: number) => {
    selectedPhaseId.value = phaseId
  }

  const resetToCurrent = () => {
    selectedPhaseId.value = null
  }

  return {
    selectedPhaseId,
    selectedPhase,
    currentPhase,
    availablePhases,
    phaseOptions,
    showPicker,
    isViewingPastPhase,
    setSelectedPhase,
    resetToCurrent,
  }
}
```

- [ ] **Step 7.2: Vérifier la compilation TS**

Run: `cd frontend && npx vue-tsc --noEmit`

- [ ] **Step 7.3: Commit**

```bash
git add frontend/composables/useEmployeeContractPhase.ts
git commit -m "feat(employee) : add useEmployeeContractPhase composable"
```

---

### Task 8: Frontend — propagation `phaseId` dans les services API

**Files:**
- Modify: `frontend/services/employee-leave-summary.ts`
- Modify: `frontend/services/employee-rtt-summary.ts`

- [ ] **Step 8.1: Trouver la signature actuelle**

```bash
grep -n "getEmployeeLeaveSummary\|getEmployeeRttSummary" frontend/services/employee-leave-summary.ts frontend/services/employee-rtt-summary.ts
```

- [ ] **Step 8.2: Ajouter le paramètre `phaseId` optionnel**

Pour `getEmployeeLeaveSummary` :
```typescript
export const getEmployeeLeaveSummary = async (
  employeeId: number,
  year?: number,
  phaseId?: number,
): Promise<EmployeeLeaveSummary> => {
  const params = new URLSearchParams()
  if (year !== undefined) params.set('year', String(year))
  if (phaseId !== undefined) params.set('phaseId', String(phaseId))
  const qs = params.toString()
  return await api(`/employees/${employeeId}/leave-summary${qs ? `?${qs}` : ''}`)
}
```

Adapter à la signature exacte existante (qui peut utiliser axios/ofetch, ou un util `api`). Le but est juste d'ajouter `phaseId` à la query string.

Idem pour `getEmployeeRttSummary`.

- [ ] **Step 8.3: Vérifier la compilation TS**

Run: `cd frontend && npx vue-tsc --noEmit`

- [ ] **Step 8.4: Commit**

```bash
git add frontend/services/employee-leave-summary.ts frontend/services/employee-rtt-summary.ts
git commit -m "feat(api) : phaseId query parameter on leave/rtt endpoints"
```

---

### Task 9: Frontend — `useEmployeeLeave` propage `phaseId` et borne le sélecteur d'année

**Files:**
- Modify: `frontend/composables/useEmployeeLeave.ts`

- [ ] **Step 9.1: Accepter une référence à la phase sélectionnée**

Modifier la signature :
```typescript
export const useEmployeeLeave = (
  employee: Ref<Employee | null>,
  reloadEmployee: () => Promise<void>,
  selectedPhase: Ref<ContractPhase | null>,
) => { ... }
```

(L'orchestration de la phase vit dans `useEmployeeDetailPage` qui passe la ref ici — voir Task 11.)

- [ ] **Step 9.2: Remplacer `isForfaitContract` par "is forfait sur la phase sélectionnée"**

```typescript
const isForfaitOnPhase = computed(() =>
  selectedPhase.value?.contractType === CONTRACT_TYPES.FORFAIT
)
```

Et adapter toutes les utilisations (`isForfaitContract(employee.value)` → `isForfaitOnPhase.value`).

- [ ] **Step 9.3: Borner `availableLeaveYears` à la phase**

```typescript
const availableLeaveYears = computed<LeaveYearOption[]>(() => {
  if (!employee.value || !selectedPhase.value || currentLeaveYear.value === null) return []
  const isForfait = isForfaitOnPhase.value
  const phase = selectedPhase.value

  // Plage = exercices intersectant la phase.
  const phaseStartYear = computeLeaveYearForDate(employee.value, new Date(`${phase.startDate}T00:00:00`))
  const phaseEndYear = phase.endDate
    ? computeLeaveYearForDate(employee.value, new Date(`${phase.endDate}T00:00:00`))
    : currentLeaveYear.value

  // Hard floor data-start-date
  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(phaseStartYear, dataFloor) : phaseStartYear
  const maxYear = phaseEndYear

  const years: LeaveYearOption[] = []
  for (let y = maxYear; y >= minYear; y -= 1) {
    years.push({ value: y, label: formatLeaveYearLabel(y, isForfait) })
  }
  return years
})
```

- [ ] **Step 9.4: Propager `phaseId` dans `loadLeaveData`**

```typescript
const summary = await getEmployeeLeaveSummary(
  employee.value.id,
  leaveYear,
  selectedPhase.value?.id,
)
```

- [ ] **Step 9.5: Reagir au changement de phase**

Ajouter un watcher :
```typescript
watch(() => selectedPhase.value?.id, () => {
  // Reset l'année car la plage a peut-être changé.
  selectedLeaveYear.value = null
  leaveDataLoaded.value = false
  // Le rechargement effectif est piloté par useEmployeeDetailPage.
})
```

- [ ] **Step 9.6: Vérifier la compilation TS**

- [ ] **Step 9.7: Commit**

```bash
git add frontend/composables/useEmployeeLeave.ts
git commit -m "feat(leave) : phase-aware leave tab loading"
```

---

### Task 10: Frontend — `useEmployeeRtt` propage `phaseId` et borne l'exercice

**Files:**
- Modify: `frontend/composables/useEmployeeRtt.ts`

- [ ] **Step 10.1: Accepter `selectedPhase` en paramètre**

Comme Task 9.1.

- [ ] **Step 10.2: Borner `availableRttYears` aux exercices intersectant la phase**

Analogue à Task 9.3 mais pour le format Juin→Mai uniquement (toujours non-forfait pour la phase RTT-visible).

- [ ] **Step 10.3: Propager `phaseId` dans `loadRttData`**

```typescript
const summary = await getEmployeeRttSummary(
  employee.value.id,
  selectedRttYear.value,
  selectedPhase.value?.id,
)
```

- [ ] **Step 10.4: Reagir au changement de phase**

Watcher analogue à Task 9.5.

- [ ] **Step 10.5: Vérifier la compilation TS**

- [ ] **Step 10.6: Commit**

```bash
git add frontend/composables/useEmployeeRtt.ts
git commit -m "feat(rtt) : phase-aware RTT tab loading"
```

---

### Task 11: Frontend — `useEmployeeDetailPage` orchestre le picker

**Files:**
- Modify: `frontend/composables/useEmployeeDetailPage.ts`

- [ ] **Step 11.1: Importer et instancier le composable phase**

```typescript
import { useEmployeeContractPhase } from '~/composables/useEmployeeContractPhase'
```

Dans `useEmployeeDetailPage`, après la création de `employee`, instancier la phase :
```typescript
const phase = useEmployeeContractPhase(employee)
```

- [ ] **Step 11.2: Adapter `showRttTab` pour driver par la phase**

```typescript
const showRttTab = computed(() => phase.selectedPhase.value?.contractType !== CONTRACT_TYPES.FORFAIT)
```

Idem pour `isForfait` :
```typescript
const isForfait = computed(() => phase.selectedPhase.value?.contractType === CONTRACT_TYPES.FORFAIT)
```

- [ ] **Step 11.3: Passer `phase.selectedPhase` aux composables enfants**

Modifier les appels :
```typescript
const leave = useEmployeeLeave(employee, loadEmployee, phase.selectedPhase)
const rtt = useEmployeeRtt(employee, loadEmployee, phase.selectedPhase)
```

- [ ] **Step 11.4: Reset la phase au changement d'employé**

Dans `loadEmployee`, après `employee.value = await getEmployee(...)`, ajouter :
```typescript
phase.resetToCurrent()
```

- [ ] **Step 11.5: Recharger l'onglet actif quand la phase change**

Ajouter un watcher :
```typescript
watch(() => phase.selectedPhase.value?.id, (newId, oldId) => {
  if (newId === oldId || oldId === undefined) return
  // Bascule onglet si on entre dans une phase qui ne supporte plus le tab actuel
  if (!showRttTab.value && activeTab.value === 'rtt') {
    activeTab.value = 'leave'
  }
  // Recharger l'onglet courant
  if (activeTab.value === 'leave' && showLeaveTab.value) {
    leave.loadLeaveData()
  } else if (activeTab.value === 'rtt' && showRttTab.value) {
    rtt.loadRttData()
  }
})
```

- [ ] **Step 11.6: Exposer `phase` dans le return**

```typescript
return {
  ...
  ...phase,
  ...
}
```

- [ ] **Step 11.7: Vérifier la compilation TS**

- [ ] **Step 11.8: Commit**

```bash
git add frontend/composables/useEmployeeDetailPage.ts
git commit -m "feat(employee) : wire contract phase into detail page composable"
```

---

### Task 12: Frontend — picker + bandeau sur `pages/employees/[id].vue`

**Files:**
- Modify: `frontend/pages/employees/[id].vue`

- [ ] **Step 12.1: Lire la structure de la page**

```bash
cat frontend/pages/employees/[id].vue | head -120
```

Repérer où sont rendus le nom de l'employé et la barre d'onglets.

- [ ] **Step 12.2: Ajouter le picker dans le template**

Entre le bloc nom/avatar et la barre d'onglets, insérer :
```vue
<div v-if="showPicker" class="mb-3 flex items-center gap-3">
  <label class="text-sm font-bold text-primary-500">Vue contrat</label>
  <MalioSelect
    :model-value="selectedPhase?.id ?? null"
    :options="phaseOptions"
    class="w-[420px]"
    @update:model-value="setSelectedPhase"
  />
</div>

<div
  v-if="isViewingPastPhase && selectedPhase"
  class="mb-3 rounded-md border border-warning-300 bg-warning-100 px-4 py-2 text-sm text-warning-900"
>
  Vous consultez l'historique
  <strong>{{ formatPhaseLabel(selectedPhase) }}</strong>.
  Les paiements de solde sont possibles ; l'édition d'absences et des stocks de report est désactivée.
</div>
```

- [ ] **Step 12.3: Destructurer le composable dans le `<script setup>`**

Le `useEmployeeDetailPage()` expose déjà tout via le spread `...phase`. S'assurer que `selectedPhase`, `showPicker`, `phaseOptions`, `setSelectedPhase`, `isViewingPastPhase` sont importés du retour.

Importer `formatPhaseLabel` :
```typescript
import { formatPhaseLabel } from '~/composables/useEmployeeContractPhase'
```

- [ ] **Step 12.4: Tester en local**

Démarrer le stack :
```bash
make start
make dev-nuxt
```

Ouvrir un employé qui a au moins 2 phases (peut nécessiter de simuler en BDD un switch 39h→FORFAIT). Vérifier :
1. Le picker est visible.
2. Sélectionner la phase 39h passée → onglet RTT réapparaît, période Juin→Mai.
3. Repasser sur FORFAIT → onglet RTT disparaît.
4. Bandeau jaune visible en mode phase passée.

Si pas d'employé multi-phase en BDD : créer un fixture ou modifier un employé existant pour le test.

- [ ] **Step 12.5: Commit**

```bash
git add frontend/pages/employees/[id].vue
git commit -m "feat(employee) : contract phase picker + past-mode banner"
```

---

### Task 13: Frontend — `RttTab.vue` désactive "+ Payer les RTT" sur exercices antérieurs d'une phase clôturée

**Files:**
- Modify: `frontend/components/employees/RttTab.vue`

- [ ] **Step 13.1: Identifier le bouton et la condition de désactivation actuelle**

```bash
grep -n "Payer les RTT\|disabled" frontend/components/employees/RttTab.vue
```

- [ ] **Step 13.2: Étendre la condition de désactivation**

Le bouton est désactivé si :
- exercice passé sur phase courante (existant), OU
- phase passée ET l'exercice consulté n'est pas l'exercice contenant `phase.endDate`.

Ajouter un computed :
```typescript
const isLastExerciseOfPhase = computed(() => {
  if (!props.selectedPhase || props.selectedPhase.isCurrent) return false
  if (!props.selectedPhase.endDate) return false
  const endYear = new Date(`${props.selectedPhase.endDate}T00:00:00`).getMonth() >= 5
    ? new Date(`${props.selectedPhase.endDate}T00:00:00`).getFullYear() + 1
    : new Date(`${props.selectedPhase.endDate}T00:00:00`).getFullYear()
  return props.selectedYear === endYear
})

const isPayDisabled = computed(() =>
  (props.selectedYear !== props.currentYear && !isLastExerciseOfPhase.value)
)
```

Adapter la prop `:disabled="isPayDisabled"` sur le bouton existant.

- [ ] **Step 13.3: Ajouter la prop `selectedPhase` au composant**

Dans `defineProps`, ajouter `selectedPhase: ContractPhase | null`.

- [ ] **Step 13.4: La passer depuis `pages/employees/[id].vue`**

Dans le template :
```vue
<RttTab :selected-phase="selectedPhase" ... />
```

- [ ] **Step 13.5: Vérifier la compilation TS**

- [ ] **Step 13.6: Commit**

```bash
git add frontend/components/employees/RttTab.vue frontend/pages/employees/[id].vue
git commit -m "feat(rtt) : enable pay button on closed phase last exercise"
```

---

### Task 14: Documentation — nouveau fichier `doc/contract-phase-view.md`

**Files:**
- Create: `doc/contract-phase-view.md`

- [ ] **Step 14.1: Créer le fichier**

```markdown
# Vue contrat — sélecteur de phase

## Objectif

Permettre à la RH de consulter les onglets Congés et RTT d'un employé selon une phase de contrat passée (ex. un 39h CDI avant un switch FORFAIT, ou un CDD clôturé avec solde de tout compte) sans changer le comportement par défaut sur la phase courante.

## Concept de "phase"

Une **phase** = groupe d'`EmployeeContractPeriod` consécutifs (triés par `startDate`) partageant la signature `(contract.type, weeklyHours, isDriver)`. Le service `EmployeeContractPhaseResolver` (`src/Service/Contracts/EmployeeContractPhaseResolver.php`) calcule ces phases à la volée depuis `Employee::getContractPeriods()`.

Une transition de signature (35h → 39h, 39h → FORFAIT, driver false→true, weeklyHours 28→30, etc.) ouvre une nouvelle phase. Un type différent entre deux périodes de même signature empêche leur fusion (39h → INTERIM → 39h = 3 phases).

## Picker UI

- Position : en haut de la fiche employé, sous le nom et au-dessus des onglets.
- Libellé : `Vue contrat`.
- Caché si l'employé n'a qu'une seule phase.
- Sélection non persistée — chaque ouverture de fiche démarre sur la phase courante.

## Bandeau d'information

Affiché quand le picker est sur une phase passée. Indique que le mode lecture est partiel — les paiements de solde restent possibles, l'édition d'absences et des stocks de report est désactivée.

## Effet sur les onglets

| Onglet | Effet |
|---|---|
| Congés | Recharge avec les règles de la phase. Période Juin→Mai pour non-forfait, Jan→Déc pour FORFAIT. Exercices bornés à la phase. |
| RTT | Visible ssi `phase.contractType !== FORFAIT`. Exercices bornés à la phase. |
| Heures, Frais, Formation, Contrat, Calendrier | Non impactés. |

## Paiements de solde sur phase passée

- **RTT** : `+ Payer les RTT` activé sur le **dernier exercice de la phase** uniquement (l'exercice contenant `phase.endDate`). Les exercices antérieurs restent en lecture seule.
- **CP** : utiliser le mécanisme existant `EmployeeContractPeriod.paidLeaveSettledClosureDate` pour solder. Pas de nouveau channel.

## Transition d'exercice

Quand un exercice chevauche deux phases (ex. switch 39h→FORFAIT au 01/05/2026 fait que l'exercice Juin 2025 → Mai 2026 est à cheval) :
- Vu depuis la phase 39h, l'exercice est borné à `phase.endDate` (30/04/2026).
- Vu depuis la phase FORFAIT, la période civile 2026 est bornée à `phase.startDate` (01/05/2026).

## API

Les endpoints suivants acceptent `?phaseId=N` :
- `GET /employees/{id}/leave-summary`
- `GET /employees/{id}/rtt-summary`

Quand absent, ils utilisent la phase courante (comportement inchangé).

`Employee.contractPhases` (lecture, groupe `employee:read`) liste les phases au format `{id, contractType, weeklyHours, isDriver, startDate, endDate, periodIds, isCurrent}`.

## Tests

- `tests/Service/Contracts/EmployeeContractPhaseResolverTest.php` (unit)
- `tests/State/EmployeeLeaveSummaryProviderTest.php` (functional, phaseId)
- `tests/State/EmployeeRttSummaryProviderTest.php` (functional, phaseId)
- `tests/State/EmployeeRttPaymentProcessorTest.php` (functional, dernier exo phase clôturée)
```

- [ ] **Step 14.2: Commit**

```bash
git add doc/contract-phase-view.md
git commit -m "docs : add contract-phase-view documentation"
```

---

### Task 15: Documentation — mise à jour `doc/leave-tab.md` et `doc/rtt-tab.md`

**Files:**
- Modify: `doc/leave-tab.md`
- Modify: `doc/rtt-tab.md`

- [ ] **Step 15.1: Ajouter une section au `doc/leave-tab.md`**

Après "Verrouillage des éditions sur années passées", insérer :

```markdown
## Sélecteur de phase de contrat

Quand l'employé a plusieurs phases de contrat (`Employee.contractPhases.length > 1`), le picker `Vue contrat` en haut de la fiche permet de consulter une phase passée. L'onglet Congés bascule alors sur les règles de la phase choisie :
- Période Juin→Mai pour les phases non-forfait, Jan→Déc pour FORFAIT.
- Sélecteur d'année interne borné aux exercices intersectant la phase.
- Bornes d'exercice cappées sur `phase.endDate` côté backend (l'exercice de transition affiche les soldes acquis jusqu'à la date de fin de phase, pas au-delà).
- Boutons crayon `Jours fractionnés` / `Année N-1 payés` désactivés (lecture seule sur phase passée).

Cf. `doc/contract-phase-view.md` pour les détails complets.
```

- [ ] **Step 15.2: Ajouter une section au `doc/rtt-tab.md`**

Après "Verrouillage des édition sur exercices passés", insérer :

```markdown
## Sélecteur de phase de contrat

L'onglet RTT est visible quand la **phase de contrat sélectionnée** n'est pas FORFAIT (et non pas le contrat courant). Concrètement, sur un employé passé en FORFAIT après une période 39h :
- En vue `FORFAIT` (défaut), l'onglet RTT est masqué.
- En vue `39h` (phase passée sélectionnée via le picker `Vue contrat`), l'onglet RTT redevient visible avec les exercices Juin→Mai bornés à la phase.

Le bouton `+ Payer les RTT` est activé uniquement sur le **dernier exercice de la phase passée** (l'exercice contenant `phase.endDate`). Les exercices antérieurs sont en lecture seule.

Cf. `doc/contract-phase-view.md` pour les détails complets.
```

- [ ] **Step 15.3: Commit**

```bash
git add doc/leave-tab.md doc/rtt-tab.md
git commit -m "docs : leave/rtt tabs reference phase selector"
```

---

### Task 16: Documentation in-app — `frontend/data/documentation-content.ts`

**Files:**
- Modify: `frontend/data/documentation-content.ts`

- [ ] **Step 16.1: Repérer la structure**

```bash
grep -n "requiredLevel:\s*'admin'" frontend/data/documentation-content.ts | head -5
```

Identifier où ajouter un nouvel article niveau `admin` dans la section "Fiche employé".

- [ ] **Step 16.2: Ajouter l'article**

Dans l'array des articles de la section pertinente, insérer un nouvel article :
```typescript
{
  id: 'contract-phase-view',
  title: 'Vue contrat — sélecteur de phase',
  requiredLevel: 'admin',
  blocks: [
    {
      kind: 'paragraph',
      content: "Quand un employé change de type de contrat (ex. 39h → FORFAIT) ou enchaîne plusieurs CDD avec solde de tout compte, ses anciennes phases de contrat restent consultables via le sélecteur 'Vue contrat' en haut de la fiche."
    },
    {
      kind: 'paragraph',
      content: "Choisir une phase passée fait basculer les onglets Congés et RTT sur les règles de cette phase. L'onglet RTT réapparaît si la phase n'est pas un FORFAIT. Un bandeau jaune indique que vous êtes en mode historique."
    },
    {
      kind: 'paragraph',
      content: "Sur une phase passée, vous pouvez :"
    },
    {
      kind: 'list',
      items: [
        "Solder les RTT restants — bouton '+ Payer les RTT' actif uniquement sur le dernier exercice de la phase (celui contenant la date de fin).",
        "Solder les CP restants via le champ 'Solde de tout compte' sur la période de contrat correspondante (onglet Contrat).",
      ]
    },
    {
      kind: 'paragraph',
      content: "L'édition d'absences et des stocks de report (jours fractionnés, Année N-1) est désactivée en mode phase passée."
    },
  ],
},
```

Adapter aux conventions de noms exacts du fichier (`title`, `id`, `requiredLevel`).

- [ ] **Step 16.3: Vérifier la compilation TS**

- [ ] **Step 16.4: Commit**

```bash
git add frontend/data/documentation-content.ts
git commit -m "docs(in-app) : add contract phase view help article"
```

---

### Task 17: `CLAUDE.md` — bloc descriptif de la fonctionnalité

**Files:**
- Modify: `CLAUDE.md`

- [ ] **Step 17.1: Ajouter un bloc après "Onglet RTT (fiche employé)"**

```markdown
## Vue contrat (sélecteur de phase)
- Picker `Vue contrat` en haut de la fiche employé (`pages/employees/[id].vue`). Caché si l'employé n'a qu'une phase.
- Phase = groupe d'`EmployeeContractPeriod` consécutifs partageant la signature `(contract.type, weeklyHours, isDriver)`. Résolu par `App\Service\Contracts\EmployeeContractPhaseResolver`.
- Exposé via `Employee.contractPhases` (`employee:read`). Endpoints `GET /employees/{id}/leave-summary` et `GET /employees/{id}/rtt-summary` acceptent `?phaseId=N` ; défaut = phase courante.
- Sélectionner une phase passée :
  - Onglet **Congés** : période et règles de la phase (Juin→Mai non-forfait, Jan→Déc FORFAIT). Exercices bornés à la phase, exercice de transition capé sur `phase.endDate`.
  - Onglet **RTT** : visible ssi `phase.contractType !== FORFAIT`. `+ Payer les RTT` actif uniquement sur l'exercice contenant `phase.endDate`.
  - Bandeau jaune affiché en mode phase passée. Édition d'absences et des stocks de report (jours fractionnés, Année N-1 payés) désactivée.
- Sélection non persistée — chaque ouverture de fiche démarre sur la phase courante.
- CP : solder via `EmployeeContractPeriod.paidLeaveSettledClosureDate` (mécanisme existant). RTT : créer un `EmployeeRttPayment` sur le dernier exercice de la phase.
- Doc complète : `doc/contract-phase-view.md`.
```

- [ ] **Step 17.2: Commit**

```bash
git add CLAUDE.md
git commit -m "docs(claude-md) : document contract phase view selector"
```

---

## Self-Review (post-rédaction)

**Spec coverage :**
- ✅ Concept de phase, signature `(type, weeklyHours, isDriver)`, exemples → Tasks 1, 14.
- ✅ Service `EmployeeContractPhaseResolver` → Task 1.
- ✅ Computed field `contractPhases` sur Employee → Task 2.
- ✅ Endpoints acceptent `?phaseId` → Tasks 3 et 4.
- ✅ `EmployeeLeaveSummaryProvider` : `resolveTargetPhase`, cap période, cap accrual/taken, clamp year → Task 3.
- ✅ `EmployeeRttSummaryProvider` : idem → Task 4.
- ✅ `EmployeeRttPaymentProcessor` : autorise dernier exo phase clôturée → Task 5.
- ✅ TS DTO + composable phase → Tasks 6, 7.
- ✅ `useEmployeeLeave` / `useEmployeeRtt` propagent phaseId, bornent year → Tasks 9, 10.
- ✅ `useEmployeeDetailPage` orchestre, `showRttTab` driver par phase → Task 11.
- ✅ Picker + bandeau frontend → Task 12.
- ✅ `RttTab.vue` désactivation conditionnelle du pay button → Task 13.
- ✅ Documentation `doc/`, `documentation-content.ts`, `CLAUDE.md` → Tasks 14-17.
- ✅ Tests pour resolver, leave provider, rtt provider, payment processor → Tasks 1, 3, 4, 5.

**Placeholder scan :** aucune occurrence de "TBD", "TODO", "à raffiner", "similar to". Les snippets de code sont explicites. Les noms de méthodes/props/fields cités dans les tasks tardives correspondent à ce qui est défini dans les tasks antérieures (`resolvePhases`, `selectedPhase`, `phaseOptions`, `setSelectedPhase`, `isViewingPastPhase`, `formatPhaseLabel`).

**Type consistency :**
- `ContractPhase` PHP DTO et TS type sont alignés (mêmes champs).
- `selectedPhase: Ref<ContractPhase | null>` partagé entre `useEmployeeDetailPage`, `useEmployeeLeave`, `useEmployeeRtt`.
- `phaseId` est `number` en TS / `int` en PHP, query param `string` validé par regex `\d+`.

**Scope :** un seul plan, un feature cohérent. Pas de décomposition supplémentaire nécessaire.
