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:
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AbsenceBalance;
|
||||
use App\Entity\User;
|
||||
use App\Enum\AbsenceType;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AbsenceBalance>
|
||||
*/
|
||||
class AbsenceBalanceRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AbsenceBalance::class);
|
||||
}
|
||||
|
||||
public function findOneForPeriod(User $user, AbsenceType $type, string $period): ?AbsenceBalance
|
||||
{
|
||||
return $this->findOneBy([
|
||||
'user' => $user,
|
||||
'type' => $type,
|
||||
'period' => $period,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AbsencePolicy;
|
||||
use App\Enum\AbsenceType;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AbsencePolicy>
|
||||
*/
|
||||
class AbsencePolicyRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AbsencePolicy::class);
|
||||
}
|
||||
|
||||
public function findOneByType(AbsenceType $type): ?AbsencePolicy
|
||||
{
|
||||
return $this->findOneBy(['type' => $type]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AbsenceRequest;
|
||||
use App\Entity\User;
|
||||
use App\Enum\AbsenceStatus;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<AbsenceRequest>
|
||||
*/
|
||||
class AbsenceRequestRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AbsenceRequest::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user already has a PENDING or APPROVED absence that overlaps
|
||||
* the given date range. Two ranges overlap when start_a <= end_b and
|
||||
* end_a >= start_b.
|
||||
*/
|
||||
public function hasOverlap(
|
||||
User $user,
|
||||
DateTimeInterface $startDate,
|
||||
DateTimeInterface $endDate,
|
||||
?int $excludeId = null,
|
||||
): bool {
|
||||
$qb = $this->createQueryBuilder('a')
|
||||
->select('COUNT(a.id)')
|
||||
->andWhere('a.user = :user')
|
||||
->andWhere('a.status IN (:statuses)')
|
||||
->andWhere('a.startDate <= :endDate')
|
||||
->andWhere('a.endDate >= :startDate')
|
||||
->setParameter('user', $user)
|
||||
->setParameter('statuses', [AbsenceStatus::Pending, AbsenceStatus::Approved])
|
||||
->setParameter('startDate', $startDate->format('Y-m-d'))
|
||||
->setParameter('endDate', $endDate->format('Y-m-d'))
|
||||
;
|
||||
|
||||
if (null !== $excludeId) {
|
||||
$qb->andWhere('a.id != :excludeId')->setParameter('excludeId', $excludeId);
|
||||
}
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absences (approved or pending) overlapping a date range, all employees —
|
||||
* used by the admin calendar view.
|
||||
*
|
||||
* @return AbsenceRequest[]
|
||||
*/
|
||||
public function findInRange(DateTimeInterface $from, DateTimeInterface $to): array
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->andWhere('a.status IN (:statuses)')
|
||||
->andWhere('a.startDate <= :to')
|
||||
->andWhere('a.endDate >= :from')
|
||||
->setParameter('statuses', [AbsenceStatus::Pending, AbsenceStatus::Approved])
|
||||
->setParameter('from', $from->format('Y-m-d'))
|
||||
->setParameter('to', $to->format('Y-m-d'))
|
||||
->orderBy('a.startDate', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user