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
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\AbsenceRequest;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
/**
* @implements ProviderInterface<AbsenceRequest>
*/
final readonly class AbsenceRequestProvider implements ProviderInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private Security $security,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): AbsenceRequest|array|null
{
$user = $this->security->getUser();
assert($user instanceof User);
$repo = $this->entityManager->getRepository(AbsenceRequest::class);
$isAdmin = $this->security->isGranted('ROLE_ADMIN');
// Single item: owner or admin only
if (isset($uriVariables['id'])) {
$request = $repo->find($uriVariables['id']);
if (null === $request) {
return null;
}
if (!$isAdmin && $request->getUser() !== $user) {
return null;
}
return $request;
}
$qb = $repo->createQueryBuilder('a')
->orderBy('a.createdAt', 'DESC')
;
if (!$isAdmin) {
$qb->andWhere('a.user = :user')->setParameter('user', $user);
}
$filters = $context['filters'] ?? [];
if (isset($filters['status'])) {
$qb->andWhere('a.status = :status')->setParameter('status', $filters['status']);
}
if (isset($filters['type'])) {
$qb->andWhere('a.type = :type')->setParameter('type', $filters['type']);
}
if (isset($filters['year']) && is_numeric($filters['year'])) {
$year = (int) $filters['year'];
$qb->andWhere('a.startDate <= :yearEnd')
->andWhere('a.endDate >= :yearStart')
->setParameter('yearStart', sprintf('%d-01-01', $year))
->setParameter('yearEnd', sprintf('%d-12-31', $year))
;
}
if ($isAdmin && isset($filters['user'])) {
$qb->andWhere('a.user = :filterUser')
->setParameter('filterUser', self::extractId($filters['user']))
;
}
return $qb->getQuery()->getResult();
}
private static function extractId(string $value): int
{
return is_numeric($value) ? (int) $value : (int) basename($value);
}
}