feat : ajout des icons dans la sidebar et redirection après login sur le calendrier
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
This commit is contained in:
484
docs/plans/2026-03-09-rtt-paid-hours.md
Normal file
484
docs/plans/2026-03-09-rtt-paid-hours.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# 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
|
||||
<?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**
|
||||
|
||||
```bash
|
||||
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
|
||||
<?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
|
||||
<?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**
|
||||
|
||||
```bash
|
||||
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" }`
|
||||
|
||||
```php
|
||||
// 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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```php
|
||||
// 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:
|
||||
```php
|
||||
public int $totalPaidMinutes = 0;
|
||||
|
||||
/** @var list<RttMonthPayment> */
|
||||
public array $monthPayments = [];
|
||||
```
|
||||
|
||||
Et modifier `availableMinutes` pour soustraire les paiements:
|
||||
```php
|
||||
$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`:
|
||||
```typescript
|
||||
export type RttMonthPayment = {
|
||||
month: number
|
||||
paidMinutes25: number
|
||||
paidMinutes50: number
|
||||
}
|
||||
|
||||
export type EmployeeRttSummary = {
|
||||
// ... champs existants ...
|
||||
totalPaidMinutes: number
|
||||
monthPayments: RttMonthPayment[]
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
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:
|
||||
```html
|
||||
<p>...: {{ formatDays(summary?.availableMinutes ?? 0) }}</p>
|
||||
```
|
||||
|
||||
Remplacer par:
|
||||
```html
|
||||
<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:
|
||||
|
||||
```html
|
||||
<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**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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` |
|
||||
Reference in New Issue
Block a user