feat : ajout de l'export récap congés et RTT

This commit is contained in:
2026-03-16 16:26:06 +01:00
parent cef364fcec
commit 86cdec50c6
9 changed files with 421 additions and 7 deletions

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\State\LeaveRecapPrintProvider;
#[ApiResource(
operations: [
new Get(
uriTemplate: '/leave-recap/print',
provider: LeaveRecapPrintProvider::class,
security: "is_granted('ROLE_ADMIN')"
),
]
)]
final class LeaveRecapPrint {}

View File

@@ -24,6 +24,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
paginationEnabled: false,
security: "is_granted('ROLE_ADMIN')",
processor: EmployeeWriteProcessor::class,
order: ['site.name' => 'ASC', 'displayOrder' => 'ASC', 'lastName' => 'ASC', 'firstName' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: EmployeeRepository::class)]
#[ORM\Table(name: 'employees')]

View File

@@ -87,8 +87,7 @@ final class EmployeeRepository extends ServiceEntityRepository implements Employ
->addSelect('s')
->leftJoin('e.contract', 'c')
->addSelect('c')
->orderBy('s.displayOrder', 'ASC')
->addOrderBy('s.name', 'ASC')
->orderBy('s.name', 'ASC')
->addOrderBy('e.displayOrder', 'ASC')
->addOrderBy('e.lastName', 'ASC')
->addOrderBy('e.firstName', 'ASC')

View File

@@ -126,7 +126,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
* previousYearRemainingDays: float
* }
*/
private function computeYearSummary(Employee $employee, int $targetYear): ?array
public function computeYearSummary(Employee $employee, int $targetYear): ?array
{
$firstYear = $this->resolveFirstComputationYear($employee);
if ($targetYear < $firstYear) {
@@ -286,6 +286,16 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
return $targetSummary;
}
public function resolveLeaveYearForToday(Employee $employee): int
{
$today = new DateTimeImmutable('today');
if (ContractType::FORFAIT === $employee->getContract()?->getType()) {
return (int) $today->format('Y');
}
return $this->resolveCurrentLeaveYear($today);
}
private function resolveEffectivePeriodStart(
Employee $employee,
DateTimeImmutable $from,

View File

@@ -0,0 +1,230 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\Employee;
use App\Enum\ContractNature;
use App\Enum\ContractType;
use App\Enum\LeaveRuleCode;
use App\Enum\TrackingMode;
use App\Repository\EmployeeLeaveBalanceRepository;
use App\Repository\EmployeeRepository;
use App\Repository\EmployeeRttBalanceRepository;
use App\Repository\EmployeeRttPaymentRepository;
use App\Service\PublicHolidayServiceInterface;
use App\Service\Rtt\RttRecoveryComputationService;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Dompdf\Dompdf;
use Dompdf\Options;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
use Twig\Environment;
class LeaveRecapPrintProvider implements ProviderInterface
{
public function __construct(
private Environment $twig,
private EmployeeRepository $employeeRepository,
private EmployeeLeaveBalanceRepository $leaveBalanceRepository,
private PublicHolidayServiceInterface $publicHolidayService,
private RttRecoveryComputationService $rttRecoveryService,
private EmployeeRttBalanceRepository $rttBalanceRepository,
private EmployeeRttPaymentRepository $rttPaymentRepository,
private EntityManagerInterface $entityManager,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
{
$today = new DateTimeImmutable('today');
$employees = $this->employeeRepository->findForPrintBySiteIds([]);
$siteGroups = [];
foreach ($employees as $employee) {
if (!$employee->getHasActiveContract()) {
continue;
}
$site = $employee->getSite();
$siteId = $site ? $site->getId() : 0;
if (!isset($siteGroups[$siteId])) {
$siteGroups[$siteId] = [
'name' => $site ? $site->getName() : 'Sans site',
'color' => $site?->getColor() ?? '#ffd7d7',
'employees' => [],
];
}
$siteGroups[$siteId]['employees'][] = $this->buildEmployeeRow($employee, $today);
$this->entityManager->clear();
}
// Re-load Twig environment after clear
$options = new Options();
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
$html = $this->twig->render('leave-recap/print.html.twig', [
'today' => $today,
'siteGroups' => $siteGroups,
]);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$filename = sprintf('recap_conges_%s.pdf', $today->format('Y-m-d'));
return new Response($dompdf->output(), Response::HTTP_OK, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="'.$filename.'"',
]);
}
private function buildEmployeeRow(Employee $employee, DateTimeImmutable $today): array
{
$contract = $employee->getContract();
$contractName = $contract?->getName();
$isForfait = ContractType::FORFAIT === $contract?->getType();
$nature = ContractNature::tryFrom($employee->getCurrentContractNature());
$isInterim = ContractNature::INTERIM === $nature;
$acquiredDays = 0.0;
$cpN = '-';
$acquiredSaturdays = '-';
$rtt = '-';
if (!$isInterim) {
$leaveYear = $this->resolveLeaveYear($employee, $today);
$ruleCode = $isForfait ? LeaveRuleCode::FORFAIT_218 : LeaveRuleCode::CDI_CDD_NON_FORFAIT;
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $leaveYear);
if (null !== $balance) {
$acquiredDays = $balance->getOpeningDays();
$acquiredSaturdays = $isForfait ? '-' : (string) $balance->getOpeningSaturdays();
}
if ($isForfait) {
try {
$cpN = (string) $this->computeForfaitAcquiredDays($employee, $today);
} catch (Throwable) {
$cpN = '-';
}
}
if (!$isForfait && TrackingMode::PRESENCE->value !== $contract?->getTrackingMode()) {
try {
$rtt = $this->formatMinutes($this->computeAvailableRttMinutes($employee, $today));
} catch (Throwable) {
$rtt = '-';
}
}
}
return [
'lastName' => $employee->getLastName(),
'firstName' => $employee->getFirstName(),
'contractName' => $contractName,
'acquiredDays' => $acquiredDays,
'cpN' => $cpN,
'acquiredSaturdays' => $acquiredSaturdays,
'rtt' => $rtt,
];
}
private function resolveLeaveYear(Employee $employee, DateTimeImmutable $today): int
{
if (ContractType::FORFAIT === $employee->getContract()?->getType()) {
return (int) $today->format('Y');
}
$month = (int) $today->format('n');
$year = (int) $today->format('Y');
return $month >= 6 ? $year + 1 : $year;
}
private function computeAvailableRttMinutes(Employee $employee, DateTimeImmutable $today): int
{
$month = (int) $today->format('n');
$year = (int) $today->format('Y');
$exerciseYear = $month >= 6 ? $year + 1 : $year;
// Carry from previous exercise
$carry = 0;
$balance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $exerciseYear);
if (null !== $balance) {
$carry = $balance->getTotalOpeningMinutes();
} else {
$previousTotal = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $exerciseYear - 1);
$carry = $previousTotal->totalMinutes;
}
// Current exercise
$current = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $exerciseYear);
// Paid RTT
$paid = 0;
$payments = $this->rttPaymentRepository->findByEmployeeAndYear($employee, $exerciseYear);
foreach ($payments as $payment) {
$paid += $payment->getBase25Minutes() + $payment->getBase50Minutes();
}
return $carry + $current->totalMinutes - $paid;
}
private function computeForfaitAcquiredDays(Employee $employee, DateTimeImmutable $today): float
{
$year = (int) $today->format('Y');
$from = new DateTimeImmutable(sprintf('%d-01-01', $year));
$to = new DateTimeImmutable(sprintf('%d-12-31', $year));
$contractStartRaw = $employee->getCurrentContractStartDate();
if (null !== $contractStartRaw && '' !== trim($contractStartRaw)) {
$contractStart = DateTimeImmutable::createFromFormat('!Y-m-d', trim($contractStartRaw));
if ($contractStart instanceof DateTimeImmutable && $contractStart > $from) {
$from = $contractStart;
}
}
$contractEndRaw = $employee->getCurrentContractEndDate();
if (null !== $contractEndRaw && '' !== trim($contractEndRaw)) {
$contractEnd = DateTimeImmutable::createFromFormat('!Y-m-d', trim($contractEndRaw));
if ($contractEnd instanceof DateTimeImmutable && $contractEnd < $to) {
$to = $contractEnd;
}
}
$holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
$businessDays = 0;
for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) {
$weekDay = (int) $cursor->format('N');
if ($weekDay <= 5 && !isset($holidays[$cursor->format('Y-m-d')])) {
++$businessDays;
}
}
return (float) max(0, $businessDays - 218);
}
private function formatMinutes(int $minutes): string
{
if (0 === $minutes) {
return '0 h';
}
$sign = $minutes < 0 ? '- ' : '';
$abs = abs($minutes);
$h = intdiv($abs, 60);
$m = $abs % 60;
return 0 === $m ? "{$sign}{$h} h" : "{$sign}{$h} h {$m} m";
}
}