feat : modification des exports PDF et affichage du type de contrat sur l'écran des heures
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled

This commit is contained in:
2026-04-17 08:58:58 +02:00
parent be7c16778a
commit 1095421424
19 changed files with 768 additions and 83 deletions

View File

@@ -34,5 +34,6 @@ final class WeeklySummaryRow
public int $weeklyDinnerCount = 0,
public int $weeklyOvernightCount = 0,
public bool $hasContractForWeek = true,
public ?string $contractNature = null,
) {}
}

View File

@@ -9,6 +9,8 @@ use ApiPlatform\State\ProviderInterface;
use App\Dto\WorkHours\WorkMetrics;
use App\Entity\Employee;
use App\Entity\WorkHour;
use App\Enum\ContractNature;
use App\Enum\ContractType;
use App\Enum\TrackingMode;
use App\Repository\AbsenceRepository;
use App\Repository\EmployeeRepository;
@@ -62,8 +64,22 @@ class EmployeeYearlyHoursPrintProvider implements ProviderInterface
}
$year = (int) $yearRaw;
$from = new DateTimeImmutable("{$year}-01-01");
$to = new DateTimeImmutable("{$year}-12-31");
$monthRaw = (string) $request->query->get('month', '');
$month = null;
if ('' !== $monthRaw) {
if (!preg_match('/^(?:0?[1-9]|1[0-2])$/', $monthRaw)) {
throw new UnprocessableEntityHttpException('month must be between 1 and 12.');
}
$month = (int) $monthRaw;
}
if (null !== $month) {
$from = new DateTimeImmutable(sprintf('%d-%02d-01', $year, $month));
$to = $from->modify('last day of this month');
} else {
$from = new DateTimeImmutable("{$year}-01-01");
$to = new DateTimeImmutable("{$year}-12-31");
}
$days = $this->buildDays($from, $to);
$workHours = $this->workHourRepository->findByDateRangeAndEmployees($from, $to, [$employee]);
@@ -83,28 +99,39 @@ class EmployeeYearlyHoursPrintProvider implements ProviderInterface
$absenceData,
);
$employeeName = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? ''));
$employeeName = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? ''));
$contractLabel = $this->buildContractLabel($employee);
$options = new Options();
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
$html = $this->twig->render('employee-yearly-hours/print.html.twig', [
'employeeName' => $employeeName,
'year' => $year,
'segments' => $segments,
'employeeName' => $employeeName,
'contractLabel' => $contractLabel,
'year' => $year,
'month' => $month,
'segments' => $segments,
]);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$filename = sprintf(
'%s_%s_%d.pdf',
$this->sanitizeFilename($employee->getLastName() ?? ''),
$this->sanitizeFilename($employee->getFirstName() ?? ''),
$year,
);
$filename = null !== $month
? sprintf(
'%s_%s_%d-%02d.pdf',
$this->sanitizeFilename($employee->getLastName() ?? ''),
$this->sanitizeFilename($employee->getFirstName() ?? ''),
$year,
$month,
)
: sprintf(
'%s_%s_%d.pdf',
$this->sanitizeFilename($employee->getLastName() ?? ''),
$this->sanitizeFilename($employee->getFirstName() ?? ''),
$year,
);
return new Response($dompdf->output(), Response::HTTP_OK, [
'Content-Type' => 'application/pdf',
@@ -112,6 +139,36 @@ class EmployeeYearlyHoursPrintProvider implements ProviderInterface
]);
}
private function buildContractLabel(Employee $employee): ?string
{
$contract = $employee->getContract();
if (null === $contract) {
return null;
}
$natureRaw = $employee->getCurrentContractNature();
$nature = ContractNature::tryFrom($natureRaw) ?? ContractNature::CDI;
$natureLabel = match ($nature) {
ContractNature::CDI => 'CDI',
ContractNature::CDD => 'CDD',
ContractNature::INTERIM => 'Intérim',
};
$contractType = $contract->getType();
if (ContractType::FORFAIT === $contractType) {
return $natureLabel.' Forfait';
}
$weeklyHours = $contract->getWeeklyHours();
if (null !== $weeklyHours && $weeklyHours > 0) {
return sprintf('%s %d heures', $natureLabel, $weeklyHours);
}
$name = $contract->getName();
return null !== $name && '' !== $name ? $natureLabel.' '.$name : $natureLabel;
}
/**
* @return list<string>
*/
@@ -211,13 +268,44 @@ class EmployeeYearlyHoursPrintProvider implements ProviderInterface
$currentRows = [];
$currentName = null;
// Crop the output window to [first data day, today] to avoid padding the
// export with empty rows (notably weekends before the first saisie or after today).
$firstDataDate = null;
foreach ($days as $date) {
$contract = $contractsByDate[$date] ?? null;
$isDriver = $driverByDate[$date] ?? false;
$wh = $workHoursByDate[$date] ?? null;
$hasData = null !== $wh || ($absenceData['hasDayAbsence'][$date] ?? false);
$hasRow = null !== ($workHoursByDate[$date] ?? null)
|| ($absenceData['hasDayAbsence'][$date] ?? false);
if ($hasRow) {
$firstDataDate = $date;
if (!$hasData) {
break;
}
}
if (null === $firstDataDate) {
return [];
}
$todayYmd = new DateTimeImmutable('today')->format('Y-m-d');
foreach ($days as $date) {
if ($date < $firstDataDate || $date > $todayYmd) {
continue;
}
$contract = $contractsByDate[$date] ?? null;
$isDriver = $driverByDate[$date] ?? false;
$wh = $workHoursByDate[$date] ?? null;
$hasData = null !== $wh || ($absenceData['hasDayAbsence'][$date] ?? false);
$isoDay = (int) new DateTimeImmutable($date)->format('N');
$isWeekend = $isoDay >= 6;
// Keep weekend rows even when empty so the reader can distinguish
// worked vs non-worked Saturdays/Sundays at a glance.
if (!$hasData && !$isWeekend) {
continue;
}
if (!$hasData && null === $contract) {
continue;
}
@@ -244,6 +332,7 @@ class EmployeeYearlyHoursPrintProvider implements ProviderInterface
$row = [
'date' => new DateTimeImmutable($date)->format('d/m/Y'),
'absenceLabel' => $absenceLabel,
'isWeekend' => $isWeekend,
];
if ('presence' === $mode) {

View File

@@ -18,12 +18,14 @@ use App\Repository\MileageAllowanceRepository;
use App\Repository\ObservationRepository;
use App\Repository\WorkHourRepository;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\PublicHolidayServiceInterface;
use DateInterval;
use DateTimeImmutable;
use Dompdf\Dompdf;
use Dompdf\Options;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
use Twig\Environment;
class SalaryRecapPrintProvider implements ProviderInterface
@@ -39,6 +41,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
private MileageAllowanceRepository $mileageAllowanceRepository,
private ObservationRepository $observationRepository,
private EmployeeContractResolver $contractResolver,
private PublicHolidayServiceInterface $publicHolidayService,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
@@ -71,6 +74,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
$days = $this->buildDays($from, $to);
$contractMap = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
$driverMap = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
$holidayMap = $this->buildHolidayMap($from, $to);
$workHourMap = $this->buildWorkHourMap($workHours);
$absenceMap = $this->buildAbsenceMap($absences);
@@ -79,7 +83,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
$mileageMap = $this->buildMileageMap($mileages);
$observationMap = $this->buildObservationMap($observations);
$siteGroups = $this->aggregateBySite($employees, $days, $contractMap, $driverMap, $workHourMap, $absenceMap, $rttPaymentMap, $bonusMap, $mileageMap, $observationMap);
$siteGroups = $this->aggregateBySite($employees, $days, $contractMap, $driverMap, $workHourMap, $absenceMap, $rttPaymentMap, $bonusMap, $mileageMap, $observationMap, $holidayMap);
$options = new Options();
$options->set('isRemoteEnabled', true);
@@ -208,6 +212,29 @@ class SalaryRecapPrintProvider implements ProviderInterface
return $map;
}
/**
* @return array<string, string> Y-m-d → label
*/
private function buildHolidayMap(DateTimeImmutable $from, DateTimeImmutable $to): array
{
$map = [];
$startYear = (int) $from->format('Y');
$endYear = (int) $to->format('Y');
try {
for ($year = $startYear; $year <= $endYear; ++$year) {
$holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
foreach ($holidays as $date => $label) {
$map[(string) $date] = (string) $label;
}
}
} catch (Throwable) {
return [];
}
return $map;
}
/**
* @return array<int, string>
*/
@@ -236,6 +263,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
array $bonusMap,
array $mileageMap,
array $observationMap,
array $holidayMap,
): array {
$siteGroups = [];
@@ -257,6 +285,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
$bonusMap[$employeeId] ?? 0.0,
$mileageMap[$employeeId] ?? 0.0,
$observationMap[$employeeId] ?? '',
$holidayMap,
);
if (!isset($siteGroups[$siteId])) {
@@ -285,18 +314,20 @@ class SalaryRecapPrintProvider implements ProviderInterface
float $bonusAmount,
float $mileageKm,
string $observation,
array $holidayMap,
): array {
$contractName = null;
$presenceDays = 0.0;
$nightMinutesTotal = 0;
$nightBasketCount = 0;
$sundayMinutesTotal = 0;
$isDriverAnyDay = false;
$driverBreakfast = 0;
$driverMeals = 0;
$driverOvernight = 0;
$driverSaturdays = 0;
$isForfait = false;
$contractName = null;
$presenceDays = 0.0;
$nightMinutesTotal = 0;
$nightBasketCount = 0;
$sundayMinutesTotal = 0;
$holidayMinutesTotal = 0;
$isDriverAnyDay = false;
$driverBreakfast = 0;
$driverMeals = 0;
$driverOvernight = 0;
$driverSaturdays = 0;
$isForfait = false;
foreach ($days as $date) {
$contract = $contractsByDate[$date] ?? null;
@@ -318,10 +349,13 @@ class SalaryRecapPrintProvider implements ProviderInterface
$dayOfWeek = (int) new DateTimeImmutable($date)->format('N');
$isHoliday = isset($holidayMap[$date]);
if ($isDriver) {
$nightMinutesTotal += $wh->getNightHoursMinutes() ?? 0;
$dayMin = $wh->getDayHoursMinutes() ?? 0;
$nightMin = $wh->getNightHoursMinutes() ?? 0;
$dayMin = $wh->getDayHoursMinutes() ?? 0;
$nightMin = $wh->getNightHoursMinutes() ?? 0;
$workshopMin = $wh->getWorkshopHoursMinutes() ?? 0;
if (($nightMin > $dayMin && $nightMin > 0) || $nightMin >= 240) {
++$nightBasketCount;
}
@@ -336,12 +370,16 @@ class SalaryRecapPrintProvider implements ProviderInterface
++$driverOvernight;
}
if (6 === $dayOfWeek && ($dayMin > 0 || $nightMin > 0 || ($wh->getWorkshopHoursMinutes() ?? 0) > 0)) {
if (6 === $dayOfWeek && ($dayMin > 0 || $nightMin > 0 || $workshopMin > 0)) {
++$driverSaturdays;
}
if (7 === $dayOfWeek) {
$sundayMinutesTotal += $dayMin + $nightMin + ($wh->getWorkshopHoursMinutes() ?? 0);
$sundayMinutesTotal += $dayMin + $nightMin + $workshopMin;
}
if ($isHoliday) {
$holidayMinutesTotal += $dayMin + $nightMin + $workshopMin;
}
} else {
$metrics = $this->computeNightMinutes($wh);
@@ -359,6 +397,10 @@ class SalaryRecapPrintProvider implements ProviderInterface
$sundayMinutesTotal += $this->computeOverflowAfterMidnight($wh);
}
if ($isHoliday) {
$holidayMinutesTotal += $metrics['dayMinutes'] + $metrics['nightMinutes'];
}
if ($isForfait) {
if ($wh->getIsPresentMorning()) {
$presenceDays += 0.5;
@@ -373,9 +415,10 @@ class SalaryRecapPrintProvider implements ProviderInterface
$conges = $this->countAbsencesByCode($absences, ['C']);
$maladie = $this->countAbsencesByCode($absences, ['M', 'AT']);
$nightHours = round($nightMinutesTotal / 60, 2);
$paidHours = round($rttPaidMinutes / 60, 2);
$sundayHours = round($sundayMinutesTotal / 60, 2);
$nightHours = round($nightMinutesTotal / 60, 2);
$paidHours = round($rttPaidMinutes / 60, 2);
$sundayHours = round($sundayMinutesTotal / 60, 2);
$holidayHours = round($holidayMinutesTotal / 60, 2);
return [
'lastName' => mb_strimwidth($employee->getLastName() ?? '', 0, 15, '...'),
@@ -387,6 +430,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
'nightBasketCount' => $nightBasketCount,
'paidHours' => $paidHours,
'sundayHours' => $sundayHours,
'holidayHours' => $holidayHours,
'bonusAmount' => $bonusAmount,
'congesCount' => $conges['count'],
'congesDates' => $conges['dates'],

View File

@@ -369,6 +369,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
weeklyDinnerCount: $weeklyDinnerCount,
weeklyOvernightCount: $weeklyOvernightCount,
hasContractForWeek: $hasContractForWeek,
contractNature: $weekAnchorContractNature->value,
);
}