Files
SIRH/docs/plans/2026-03-09-rtt-paid-hours.md
2026-03-12 12:30:13 +01:00

14 KiB

RTT : Affichage en heures + Paiement RTT

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Afficher les RTT en heures (plus en jours), permettre le paiement RTT via drawer avec majoration 25%/50%, stocker en BDD et afficher par mois.

Architecture: Nouvelle entity EmployeeRttPayment (employee, year, month, minutes, rate). Le provider RTT agrège les paiements par mois et les soustrait du disponible. Le frontend ajoute un drawer de saisie et deux lignes par mois (25% / 50%).

Tech Stack: Symfony + Doctrine + API Platform (backend), Nuxt 4 + Vue 3 + TypeScript + Tailwind (frontend)


Contexte existant

  • Entity: EmployeeRttBalance dans src/Entity/EmployeeRttBalance.php - stocke openingMinutes par exercice
  • Provider: src/State/EmployeeRttSummaryProvider.php - calcule availableMinutes = carry + currentYearRecovery
  • Service: src/Service/Rtt/RttRecoveryComputationService.php - calcul semaine par semaine
  • Frontend: frontend/components/employees/RttTab.vue - affichage grille mois/semaines
  • DTO backend: src/ApiResource/EmployeeRttSummary.php - champs availableMinutes, weeks[]
  • DTO frontend: frontend/services/dto/employee-rtt-summary.ts
  • Les minutes sont la base de calcul. 1 jour = 7h = 420 minutes.
  • Exercice RTT : 1er juin N-1 -> 31 mai N (year = N)

Task 1: Migration - Créer la table employee_rtt_payments

Files:

  • Create: migrations/Version20260309140000.php

Step 1: Créer la migration

<?php
declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20260309140000 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Create employee_rtt_payments table for RTT paid hours tracking.';
    }

    public function up(Schema $schema): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE employee_rtt_payments (
                id SERIAL PRIMARY KEY,
                employee_id INT NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
                year INT NOT NULL,
                month INT NOT NULL,
                minutes INT NOT NULL,
                rate VARCHAR(10) NOT NULL,
                created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
            )
        SQL);
        $this->addSql('CREATE INDEX idx_rtt_payment_employee_year ON employee_rtt_payments (employee_id, year)');
        $this->addSql("COMMENT ON TABLE employee_rtt_payments IS 'Paiements RTT par employe, mois et taux de majoration.'");
        $this->addSql("COMMENT ON COLUMN employee_rtt_payments.rate IS 'Taux de majoration: 25 ou 50.'");
        $this->addSql("COMMENT ON COLUMN employee_rtt_payments.minutes IS 'Minutes RTT payees pour ce mois et ce taux.'");
    }

    public function down(Schema $schema): void
    {
        $this->addSql('DROP TABLE employee_rtt_payments');
    }
}

Step 2: Exécuter la migration

Run: make migrate ou docker compose exec php php bin/console doctrine:migrations:migrate --no-interaction Expected: Migration exécutée sans erreur

Step 3: Commit

git add migrations/Version20260309140000.php
git commit -m "feat(rtt): add employee_rtt_payments table"

Task 2: Entity + Repository EmployeeRttPayment

Files:

  • Create: src/Entity/EmployeeRttPayment.php
  • Create: src/Repository/EmployeeRttPaymentRepository.php

Step 1: Créer l'entity

<?php
declare(strict_types=1);

namespace App\Entity;

use App\Repository\EmployeeRttPaymentRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: EmployeeRttPaymentRepository::class)]
#[ORM\Table(name: 'employee_rtt_payments')]
#[ORM\Index(columns: ['employee_id', 'year'], name: 'idx_rtt_payment_employee_year')]
class EmployeeRttPayment
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id = null;

    #[ORM\ManyToOne(targetEntity: Employee::class)]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private ?Employee $employee = null;

    #[ORM\Column(type: 'integer')]
    private int $year = 0;

    #[ORM\Column(type: 'integer')]
    private int $month = 0;

    #[ORM\Column(type: 'integer', options: ['comment' => 'Minutes RTT payees pour ce mois et ce taux.'])]
    private int $minutes = 0;

    #[ORM\Column(length: 10, options: ['comment' => 'Taux de majoration: 25 ou 50.'])]
    private string $rate = '25';

    #[ORM\Column(type: 'datetime_immutable')]
    private DateTimeImmutable $createdAt;

    #[ORM\Column(type: 'datetime_immutable')]
    private DateTimeImmutable $updatedAt;

    public function __construct()
    {
        $now             = new DateTimeImmutable();
        $this->createdAt = $now;
        $this->updatedAt = $now;
    }

    // Getters & setters (getId, getEmployee/setEmployee, getYear/setYear,
    // getMonth/setMonth, getMinutes/setMinutes, getRate/setRate, touch)
    // Suivre le même pattern que EmployeeLeaveBalance
}

Step 2: Créer le repository

<?php
declare(strict_types=1);

namespace App\Repository;

use App\Entity\Employee;
use App\Entity\EmployeeRttPayment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<EmployeeRttPayment>
 */
final class EmployeeRttPaymentRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, EmployeeRttPayment::class);
    }

    /**
     * @return list<EmployeeRttPayment>
     */
    public function findByEmployeeAndYear(Employee $employee, int $year): array
    {
        return $this->createQueryBuilder('p')
            ->andWhere('p.employee = :employee')
            ->andWhere('p.year = :year')
            ->setParameter('employee', $employee)
            ->setParameter('year', $year)
            ->orderBy('p.month', 'ASC')
            ->getQuery()
            ->getResult();
    }
}

Step 3: Vérifier le lint

Run: docker compose exec php php -l src/Entity/EmployeeRttPayment.php && docker compose exec php php -l src/Repository/EmployeeRttPaymentRepository.php Expected: No syntax errors

Step 4: Commit

git add src/Entity/EmployeeRttPayment.php src/Repository/EmployeeRttPaymentRepository.php
git commit -m "feat(rtt): add EmployeeRttPayment entity and repository"

Task 3: API Resource + Provider + Processor pour le paiement RTT

Files:

  • Create: src/ApiResource/EmployeeRttPaymentInput.php
  • Create: src/State/EmployeeRttPaymentProvider.php
  • Create: src/State/EmployeeRttPaymentProcessor.php

Step 1: Créer l'API Resource

Endpoint: PATCH /employees/{id}/rtt-payments (ROLE_ADMIN) Body: { "month": 3, "minutes": 120, "rate": "25" }

// src/ApiResource/EmployeeRttPaymentInput.php
#[ApiResource(
    operations: [
        new Patch(
            uriTemplate: '/employees/{id}/rtt-payments',
            security: "is_granted('ROLE_ADMIN')",
            provider: EmployeeRttPaymentProvider::class,
            processor: EmployeeRttPaymentProcessor::class
        ),
    ],
    paginationEnabled: false
)]
final class EmployeeRttPaymentInput
{
    public int $month    = 0;
    public int $minutes  = 0;
    public string $rate  = '25';
    public ?int $year    = null;
}

Step 2: Créer le Provider (retourne un DTO vide, même pattern que EmployeeFractionedDaysProvider)

Step 3: Créer le Processor

Logique:

  • Valider rate in ['25', '50'], month in [1..12], minutes >= 0
  • Résoudre l'année (même logique exercice RTT que le provider existant)
  • Persister un EmployeeRttPayment

Step 4: Vérifier le lint

Step 5: Commit

git add src/ApiResource/EmployeeRttPaymentInput.php src/State/EmployeeRttPaymentProvider.php src/State/EmployeeRttPaymentProcessor.php
git commit -m "feat(rtt): add PATCH endpoint for RTT payment"

Task 4: Modifier le DTO + Provider RTT pour inclure les paiements par mois

Files:

  • Modify: src/ApiResource/EmployeeRttSummary.php
  • Modify: src/State/EmployeeRttSummaryProvider.php
  • Create: src/Dto/Rtt/RttMonthPayment.php
  • Modify: frontend/services/dto/employee-rtt-summary.ts

Step 1: Créer le DTO mois-paiement

// src/Dto/Rtt/RttMonthPayment.php
final class RttMonthPayment
{
    public function __construct(
        public int $month,
        public int $paidMinutes25 = 0,
        public int $paidMinutes50 = 0,
    ) {}
}

Step 2: Ajouter au summary backend

Dans EmployeeRttSummary.php, ajouter:

public int $totalPaidMinutes = 0;

/** @var list<RttMonthPayment> */
public array $monthPayments = [];

Et modifier availableMinutes pour soustraire les paiements:

$summary->availableMinutes = $summary->carryFromPreviousYearMinutes
    + $summary->currentYearRecoveryMinutes
    - $summary->totalPaidMinutes;

Step 3: Modifier le provider pour charger les paiements via le repository et les agréger par mois

Dans EmployeeRttSummaryProvider::provide():

  • Charger $payments = $this->rttPaymentRepository->findByEmployeeAndYear($employee, $year)
  • Agréger par mois + rate pour construire les RttMonthPayment
  • Calculer totalPaidMinutes = sum(minutes)
  • Soustraire du availableMinutes

Step 4: Mettre à jour le DTO frontend

Dans frontend/services/dto/employee-rtt-summary.ts:

export type RttMonthPayment = {
  month: number
  paidMinutes25: number
  paidMinutes50: number
}

export type EmployeeRttSummary = {
  // ... champs existants ...
  totalPaidMinutes: number
  monthPayments: RttMonthPayment[]
}

Step 5: Commit

git commit -m "feat(rtt): include paid hours in RTT summary by month"

Task 5: Frontend - Afficher les RTT en heures + lignes payées

Files:

  • Modify: frontend/components/employees/RttTab.vue

Step 1: Changer l'affichage du header de jours en heures

Ligne 4 actuelle:

<p>...: {{ formatDays(summary?.availableMinutes ?? 0) }}</p>

Remplacer par:

<p>...: {{ formatMinutes(summary?.availableMinutes ?? 0) }}</p>

Step 2: Ajouter les 2 lignes de paiement par mois

Après la ligne Heure payée existante (ligne 33-34), remplacer par 2 lignes distinctes:

<div class="py-[6px] pl-3 border-r border-b border-primary-500">Heure payée 25%</div>
<div class="py-[6px] pl-3 border-b border-primary-500">{{ formatMinutes(getMonthPaid25(month.month)) }}</div>
<div class="py-[6px] pl-3 border-r border-primary-500">Heure payée 50%</div>
<div class="py-[6px] pl-3">{{ formatMinutes(getMonthPaid50(month.month)) }}</div>

Step 3: Ajouter les helpers computed

const paymentsByMonth = computed(() => {
  const map = new Map<number, { paid25: number; paid50: number }>()
  for (const mp of props.summary?.monthPayments ?? []) {
    map.set(mp.month, { paid25: mp.paidMinutes25, paid50: mp.paidMinutes50 })
  }
  return map
})

const getMonthPaid25 = (month: number) => paymentsByMonth.value.get(month)?.paid25 ?? 0
const getMonthPaid50 = (month: number) => paymentsByMonth.value.get(month)?.paid50 ?? 0

Step 4: Commit

git commit -m "feat(rtt): display hours instead of days + paid hours per month"

Task 6: Frontend - Drawer de paiement RTT

Files:

  • Modify: frontend/components/employees/RttTab.vue
  • Modify: frontend/services/employee-rtt-summary.ts
  • Modify: frontend/composables/useEmployeeDetailPage.ts
  • Modify: frontend/pages/employees/[id].vue

Step 1: Ajouter le service API

Dans frontend/services/employee-rtt-summary.ts:

export const createRttPayment = async (
  employeeId: number,
  month: number,
  minutes: number,
  rate: '25' | '50',
  year?: number
) => {
  const api = useApi()
  const body: Record<string, unknown> = { month, minutes, rate }
  if (year) body.year = year
  return api.patch(`/employees/${employeeId}/rtt-payments`, body)
}

Step 2: Ajouter au composable

Dans useEmployeeDetailPage.ts:

  • Import createRttPayment
  • Ajouter submitRttPayment(month, minutes, rate) qui appelle l'API puis loadEmployee()
  • Exposer dans le return

Step 3: Passer l'event dans la page

Dans frontend/pages/employees/[id].vue:

  • Destructurer submitRttPayment
  • Ajouter @submit-rtt-payment="submitRttPayment" sur <EmployeesRttTab>

Step 4: Ajouter le drawer dans RttTab.vue

Même pattern que le drawer fractionnés dans LeaveTab:

  • Import AppDrawer
  • State: isPaymentDrawerOpen, paymentForm: { month, minutes, rate }
  • Bouton "Payer les RTT" ouvre le drawer
  • Formulaire avec:
    • Select mois (Janvier..Décembre)
    • Input number "Nombre d'heures" (step 0.5, converti en minutes au submit)
    • Select rate: "25%" / "50%"
    • Boutons Annuler / Enregistrer
  • Emit submit-rtt-payment au submit

Step 5: Commit

git commit -m "feat(rtt): add payment drawer with month/hours/rate fields"

Task 7: Documentation fonctionnelle

Files:

  • Modify: doc/functional-rules.md

Step 1: Mettre à jour la section RTT

Ajouter après les règles RTT existantes:

  • Paiement RTT: saisie RH via PATCH /employees/{id}/rtt-payments
  • Stocké dans employee_rtt_payments (employee, year, month, minutes, rate)
  • Les heures payées sont soustraites du disponible RTT
  • Affichage: 2 lignes par mois (25% et 50%)
  • L'affichage global RTT est en heures (plus en jours)

Step 2: Commit

git commit -m "docs: update functional rules with RTT payment feature"

Résumé des fichiers

Action Fichier
Create migrations/Version20260309140000.php
Create src/Entity/EmployeeRttPayment.php
Create src/Repository/EmployeeRttPaymentRepository.php
Create src/ApiResource/EmployeeRttPaymentInput.php
Create src/State/EmployeeRttPaymentProvider.php
Create src/State/EmployeeRttPaymentProcessor.php
Create src/Dto/Rtt/RttMonthPayment.php
Modify src/ApiResource/EmployeeRttSummary.php
Modify src/State/EmployeeRttSummaryProvider.php
Modify frontend/services/dto/employee-rtt-summary.ts
Modify frontend/services/employee-rtt-summary.ts
Modify frontend/components/employees/RttTab.vue
Modify frontend/composables/useEmployeeDetailPage.ts
Modify frontend/pages/employees/[id].vue
Modify doc/functional-rules.md