feat(absences) : fondation backend du module de gestion des absences

Module type Payfit (étapes 1+2 de la spec V1) : demande d'absence, validation
admin, soldes à jour.

- Enums : AbsenceType, AbsenceStatus, HalfDay, ContractType, FamilySituation
- Entités : AbsencePolicy, AbsenceBalance, AbsenceRequest + champs RH sur User
- Services : PublicHolidayProvider (fériés FR métropole en PHP pur, Computus),
  AbsenceDayCalculator (décompte jours ouvrés/ouvrables + demi-journées, TDD),
  AbsenceBalanceService (périodes + pending/taken/recrédit)
- API Platform : providers/processors (création, approve/reject/cancel) + RBAC
  me/admin, contrôleurs preview (dry-run), upload/download justificatif, calendrier
- Migrations : une par table + colonnes RH user (DEFAULT puis DROP DEFAULT)
- Fixtures : 5 policies par défaut, salariés démo, soldes et demandes
- Tests unitaires : PublicHolidayProvider, AbsenceDayCalculator (12 tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-05-21 14:45:14 +02:00
parent 325a7b07f9
commit de98924fd3
32 changed files with 2554 additions and 3 deletions

View File

@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\AbsenceBalance;
use App\Entity\AbsencePolicy;
use App\Entity\AbsenceRequest;
use App\Entity\Client;
use App\Entity\ClientTicket;
use App\Entity\MailConfiguration;
@@ -19,6 +22,10 @@ use App\Entity\TimeEntry;
use App\Entity\User;
use App\Entity\Workflow;
use App\Entity\ZimbraConfiguration;
use App\Enum\AbsenceStatus;
use App\Enum\AbsenceType;
use App\Enum\ContractType;
use App\Enum\FamilySituation;
use App\Enum\RecurrenceType;
use App\Enum\StatusCategory;
use DateTimeImmutable;
@@ -722,6 +729,106 @@ class AppFixtures extends Fixture
$mailConfig->setEnabled(false);
$manager->persist($mailConfig);
// =============================================
// Absence management — policies, employees, balances, requests
// =============================================
// Default policies for the 5 absence types (legal defaults, editable by admin)
$policyData = [
// [type, daysPerYear, daysPerEvent, justifRequired, noticeDays, workingDaysOnly]
[AbsenceType::PaidLeave, 25.0, null, false, 30, true],
[AbsenceType::MarriagePacs, null, 4.0, true, 0, true],
[AbsenceType::ParentalLeave, null, null, true, 30, true],
[AbsenceType::Bereavement, null, 3.0, true, 0, true],
[AbsenceType::SickLeave, null, null, true, 0, true],
];
foreach ($policyData as [$type, $daysPerYear, $daysPerEvent, $justif, $notice, $workingDaysOnly]) {
$policy = new AbsencePolicy();
$policy->setType($type);
$policy->setDaysPerYear($daysPerYear);
$policy->setDaysPerEvent($daysPerEvent);
$policy->setJustificationRequired($justif);
$policy->setNoticeDays($notice);
$policy->setCountWorkingDaysOnly($workingDaysOnly);
$policy->setActive(true);
$manager->persist($policy);
}
// Mark internal users as employees
$admin->setIsEmployee(true);
$admin->setHireDate(new DateTimeImmutable('2020-01-15'));
$admin->setContractType(ContractType::Cdi);
$admin->setFamilySituation(FamilySituation::Married);
$admin->setNbChildren(2);
$userAlice->setIsEmployee(true);
$userAlice->setHireDate(new DateTimeImmutable('2022-09-01'));
$userAlice->setContractType(ContractType::Cdi);
$userAlice->setFamilySituation(FamilySituation::Single);
$userBob->setIsEmployee(true);
$userBob->setHireDate(new DateTimeImmutable('2023-03-10'));
$userBob->setContractType(ContractType::Cdd);
$userBob->setWorkTimeRatio(0.8);
$userBob->setFamilySituation(FamilySituation::Pacsed);
$userBob->setNbChildren(1);
// Paid-leave balances for the current reference period (June 1st → May 31st)
$cpPeriod = '2025-2026';
$balanceData = [
// [user, acquired, taken, pending]
[$admin, 22.5, 5.0, 0.0],
[$userAlice, 18.0, 2.0, 5.0],
[$userBob, 14.0, 0.0, 0.0],
];
foreach ($balanceData as [$bUser, $acquired, $taken, $pending]) {
$balance = new AbsenceBalance();
$balance->setUser($bUser);
$balance->setType(AbsenceType::PaidLeave);
$balance->setPeriod($cpPeriod);
$balance->setAcquired($acquired);
$balance->setTaken($taken);
$balance->setPending($pending);
$manager->persist($balance);
}
// Demo requests
$approvedCp = new AbsenceRequest();
$approvedCp->setUser($admin);
$approvedCp->setType(AbsenceType::PaidLeave);
$approvedCp->setStartDate(new DateTimeImmutable('2026-04-13'));
$approvedCp->setEndDate(new DateTimeImmutable('2026-04-17'));
$approvedCp->setCountedDays(5.0);
$approvedCp->setReason('Vacances de printemps');
$approvedCp->setStatus(AbsenceStatus::Approved);
$approvedCp->setCreatedAt(new DateTimeImmutable('2026-03-10'));
$approvedCp->setReviewedAt(new DateTimeImmutable('2026-03-11'));
$approvedCp->setReviewedBy($admin);
$manager->persist($approvedCp);
$pendingCp = new AbsenceRequest();
$pendingCp->setUser($userAlice);
$pendingCp->setType(AbsenceType::PaidLeave);
$pendingCp->setStartDate(new DateTimeImmutable('2026-06-15'));
$pendingCp->setEndDate(new DateTimeImmutable('2026-06-19'));
$pendingCp->setCountedDays(5.0);
$pendingCp->setStatus(AbsenceStatus::Pending);
$pendingCp->setCreatedAt(new DateTimeImmutable('-2 days'));
$manager->persist($pendingCp);
$pendingMarriage = new AbsenceRequest();
$pendingMarriage->setUser($userBob);
$pendingMarriage->setType(AbsenceType::MarriagePacs);
$pendingMarriage->setStartDate(new DateTimeImmutable('2026-07-06'));
$pendingMarriage->setEndDate(new DateTimeImmutable('2026-07-09'));
$pendingMarriage->setCountedDays(4.0);
$pendingMarriage->setReason('Mariage');
$pendingMarriage->setStatus(AbsenceStatus::Pending);
$pendingMarriage->setCreatedAt(new DateTimeImmutable('-1 day'));
$manager->persist($pendingMarriage);
$manager->flush();
}
}