diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8eaa2eb..148322c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,7 +22,8 @@ "Bash(which python3:*)", "Bash(sudo apt-get:*)", "Bash(npx xlsx-cli:*)", - "Bash(cat /home/m-tristan/.claude/projects/-home-m-tristan-workspace-SIRH/4b53d9d7-d8ae-451f-a5cc-5d4fd55f2eef/tool-results/toolu_019hng9Cu2m9wiNACuC2Wm3F.json | python3 -c \"import json,sys; data=json.load\\(sys.stdin\\); print\\(data[0]['text']\\)\" 2>/dev/null | head -2000)" + "Bash(cat /home/m-tristan/.claude/projects/-home-m-tristan-workspace-SIRH/4b53d9d7-d8ae-451f-a5cc-5d4fd55f2eef/tool-results/toolu_019hng9Cu2m9wiNACuC2Wm3F.json | python3 -c \"import json,sys; data=json.load\\(sys.stdin\\); print\\(data[0]['text']\\)\" 2>/dev/null | head -2000)", + "Bash(pip3 install:*)" ] } } diff --git a/doc/functional-rules.md b/doc/functional-rules.md index b41f978..aded520 100644 --- a/doc/functional-rules.md +++ b/doc/functional-rules.md @@ -270,7 +270,34 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer. - affichage: - le compteur global RTT est affiché en **heures** (format `Xh00`) -## 10) Notifications +## 10) Récapitulatif Salaire (PDF mensuel) + +- Accessible depuis la page Employés via le bouton "Récap. Salaire" (réservé `ROLE_ADMIN`) +- Sélecteur de mois (défaut = mois courant), génère un PDF A3 paysage +- Endpoint: `GET /api/salary-recap/print?month=YYYY-MM` +- Données groupées par site, un en-tête par site + +### Colonnes du tableau + +| Colonne | Source | Logique | +|---------|--------|---------| +| Nom | Employee | firstName + lastName | +| Base | Contract.name | Via EmployeeContractResolver pour le mois | +| Jour de présence Cadre | WorkHour | Uniquement FORFAIT (PRESENCE). Somme isPresentMorning (0.5) + isPresentAfternoon (0.5) | +| Heures de nuit | WorkHour | Non-chauffeurs: calcul intervalles nuit (00:00-06:00, 21:00-24:00). Chauffeurs: somme nightHoursMinutes | +| Panier de nuit | WorkHour | Nombre de jours où nightMinutes > dayMinutes | +| Heures payés | EmployeeRttPayment | Somme base25Minutes + base50Minutes du mois, convertie en heures | +| Congés - Nombre | Absence code 'C' | Jours (demi-journées = 0.5) | +| Congés - Date | Absence code 'C' | Dates formatées dd/mm | +| Maladie - Nombre | Absence code 'M' ou 'AT' | Jours (demi-journées = 0.5) | +| Maladie - Date | Absence code 'M' ou 'AT' | Dates formatées dd/mm | +| CHAUFFEUR - PDJ | WorkHour.hasBreakfast | Comptage mois (chauffeurs uniquement) | +| CHAUFFEUR - REPAS | WorkHour.hasLunch + hasDinner | Comptage mois (chauffeurs uniquement) | +| CHAUFFEUR - NUITEE | WorkHour.hasOvernight | Comptage mois (chauffeurs uniquement) | +| CHAUFFEUR - samedi | WorkHour (samedi) | Samedis travaillés (chauffeurs uniquement) | +| Observations | — | Colonne vide pour saisie manuelle | + +## 11) Notifications - Icône cloche en topbar: - badge = nombre de notifications non lues diff --git a/frontend/components/SalaryRecapDrawer.vue b/frontend/components/SalaryRecapDrawer.vue new file mode 100644 index 0000000..0f01b68 --- /dev/null +++ b/frontend/components/SalaryRecapDrawer.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/pages/employees/index.vue b/frontend/pages/employees/index.vue index d97426e..0b03075 100644 --- a/frontend/pages/employees/index.vue +++ b/frontend/pages/employees/index.vue @@ -3,13 +3,22 @@

Employés

- +
+ + +
@@ -200,6 +209,11 @@
+ +
@@ -211,7 +225,9 @@ import {listContracts} from '~/services/contracts' import {createEmployee, deleteEmployee, listEmployees, updateEmployee} from '~/services/employees' import {listSites} from '~/services/sites' import SiteFilterSelector from '~/components/SiteFilterSelector.vue' +import SalaryRecapDrawer from '~/components/SalaryRecapDrawer.vue' import {contractNatureLabel, isContractNature, requiresContractEndDate, showsContractEndDate} from '~/utils/contract' +import {usePdfPrinter} from '~/composables/usePdfPrinter' useHead({ title: 'Employés' @@ -220,6 +236,8 @@ useHead({ const isDrawerOpen = ref(false) const isSubmitting = ref(false) const isLoading = ref(false) +const isSalaryRecapOpen = ref(false) +const { printPdf } = usePdfPrinter() const sitesInitialized = ref(false) const editingEmployee = ref(null) const drawerTitle = computed(() => @@ -503,6 +521,11 @@ const openCreate = () => { isDrawerOpen.value = true } +const handleSalaryRecapPrint = async (month: string) => { + await printPdf(`/salary-recap/print?month=${month}`) + isSalaryRecapOpen.value = false +} + const confirmDelete = async (employee: Employee) => { const ok = window.confirm(`Supprimer ${employee.firstName} ${employee.lastName} ?`) if (!ok) return diff --git a/src/ApiResource/SalaryRecapPrint.php b/src/ApiResource/SalaryRecapPrint.php new file mode 100644 index 0000000..be6cc38 --- /dev/null +++ b/src/ApiResource/SalaryRecapPrint.php @@ -0,0 +1,24 @@ +createQueryBuilder('b') + ->andWhere('b.month >= :from') + ->andWhere('b.month <= :to') + ->setParameter('from', $from) + ->setParameter('to', $to) + ->innerJoin('b.employee', 'e') + ->addSelect('e') + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Repository/EmployeeRttPaymentRepository.php b/src/Repository/EmployeeRttPaymentRepository.php index bac4e2e..e5ed8c2 100644 --- a/src/Repository/EmployeeRttPaymentRepository.php +++ b/src/Repository/EmployeeRttPaymentRepository.php @@ -43,4 +43,21 @@ final class EmployeeRttPaymentRepository extends ServiceEntityRepository ->getResult() ; } + + /** + * @return EmployeeRttPayment[] + */ + public function findByYearAndMonth(int $year, int $month): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.year = :year') + ->andWhere('p.month = :month') + ->setParameter('year', $year) + ->setParameter('month', $month) + ->innerJoin('p.employee', 'e') + ->addSelect('e') + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Repository/MileageAllowanceRepository.php b/src/Repository/MileageAllowanceRepository.php index bb951b3..55e52a9 100644 --- a/src/Repository/MileageAllowanceRepository.php +++ b/src/Repository/MileageAllowanceRepository.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Repository; use App\Entity\MileageAllowance; +use DateTimeImmutable; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -17,4 +18,21 @@ final class MileageAllowanceRepository extends ServiceEntityRepository { parent::__construct($registry, MileageAllowance::class); } + + /** + * @return MileageAllowance[] + */ + public function findByMonth(DateTimeImmutable $from, DateTimeImmutable $to): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.month >= :from') + ->andWhere('m.month <= :to') + ->setParameter('from', $from) + ->setParameter('to', $to) + ->innerJoin('m.employee', 'e') + ->addSelect('e') + ->getQuery() + ->getResult() + ; + } } diff --git a/src/State/SalaryRecapPrintProvider.php b/src/State/SalaryRecapPrintProvider.php new file mode 100644 index 0000000..226113d --- /dev/null +++ b/src/State/SalaryRecapPrintProvider.php @@ -0,0 +1,588 @@ +requestStack->getCurrentRequest(); + if (!$request) { + return new Response('Missing request.', Response::HTTP_BAD_REQUEST); + } + + $month = $request->query->get('month'); + if (!$month || !preg_match('/^\d{4}-\d{2}$/', $month)) { + return new Response('Missing or invalid month query param (expected YYYY-MM).', Response::HTTP_BAD_REQUEST); + } + + $from = DateTimeImmutable::createFromFormat('Y-m-d', $month.'-01'); + $to = $from->modify('last day of this month'); + + $employees = $this->employeeRepository->findForPrintBySiteIds([]); + $workHours = $this->workHourRepository->findByDateRangeAndEmployees($from, $to, $employees); + $absences = $this->absenceRepository->findForPrint($from, $to, $employees); + + $year = (int) $from->format('Y'); + $monthNumber = (int) $from->format('n'); + $rttPayments = $this->rttPaymentRepository->findByYearAndMonth($year, $monthNumber); + + $bonuses = $this->bonusRepository->findByMonth($from, $to); + $mileages = $this->mileageAllowanceRepository->findByMonth($from, $to); + + $days = $this->buildDays($from, $to); + $contractMap = $this->contractResolver->resolveForEmployeesAndDays($employees, $days); + $driverMap = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days); + + $workHourMap = $this->buildWorkHourMap($workHours); + $absenceMap = $this->buildAbsenceMap($absences); + $rttPaymentMap = $this->buildRttPaymentMap($rttPayments); + $bonusMap = $this->buildBonusMap($bonuses); + $mileageMap = $this->buildMileageMap($mileages); + + $siteGroups = $this->aggregateBySite($employees, $days, $contractMap, $driverMap, $workHourMap, $absenceMap, $rttPaymentMap, $bonusMap, $mileageMap); + + $options = new Options(); + $options->set('isRemoteEnabled', true); + + $dompdf = new Dompdf($options); + $html = $this->twig->render('salary-recap/print.html.twig', [ + 'from' => $from, + 'to' => $to, + 'siteGroups' => $siteGroups, + ]); + + $dompdf->loadHtml($html); + $dompdf->setPaper('A4', 'landscape'); + $dompdf->render(); + + $filename = sprintf( + 'recap_salaire_%s.pdf', + $from->format('Y-m') + ); + + return new Response($dompdf->output(), Response::HTTP_OK, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'inline; filename="'.$filename.'"', + ]); + } + + /** + * @return list + */ + private function buildDays(DateTimeImmutable $from, DateTimeImmutable $to): array + { + $days = []; + $current = $from; + + while ($current <= $to) { + $days[] = $current->format('Y-m-d'); + $current = $current->add(new DateInterval('P1D')); + } + + return $days; + } + + /** + * @return array> + */ + private function buildWorkHourMap(array $workHours): array + { + $map = []; + foreach ($workHours as $wh) { + $employeeId = $wh->getEmployee()?->getId(); + if (!$employeeId) { + continue; + } + $date = $wh->getWorkDate()->format('Y-m-d'); + $map[$employeeId][$date] = $wh; + } + + return $map; + } + + /** + * @return array> + */ + private function buildAbsenceMap(array $absences): array + { + $map = []; + foreach ($absences as $absence) { + $employeeId = $absence->getEmployee()?->getId(); + if (!$employeeId) { + continue; + } + $map[$employeeId][] = $absence; + } + + return $map; + } + + /** + * @return array + */ + private function buildRttPaymentMap(array $rttPayments): array + { + $map = []; + foreach ($rttPayments as $payment) { + $employeeId = $payment->getEmployee()?->getId(); + if (!$employeeId) { + continue; + } + $map[$employeeId] = ($map[$employeeId] ?? 0) + $payment->getBase25Minutes() + $payment->getBase50Minutes(); + } + + return $map; + } + + /** + * @return array + */ + private function buildBonusMap(array $bonuses): array + { + $map = []; + foreach ($bonuses as $bonus) { + $employeeId = $bonus->getEmployee()?->getId(); + if (!$employeeId) { + continue; + } + $map[$employeeId] = ($map[$employeeId] ?? 0.0) + $bonus->getAmount(); + } + + return $map; + } + + /** + * @return array + */ + private function buildMileageMap(array $mileages): array + { + $map = []; + foreach ($mileages as $mileage) { + $employeeId = $mileage->getEmployee()?->getId(); + if (!$employeeId) { + continue; + } + $map[$employeeId] = ($map[$employeeId] ?? 0.0) + $mileage->getKilometers(); + } + + return $map; + } + + private function aggregateBySite( + array $employees, + array $days, + array $contractMap, + array $driverMap, + array $workHourMap, + array $absenceMap, + array $rttPaymentMap, + array $bonusMap, + array $mileageMap, + ): array { + $siteGroups = []; + + foreach ($employees as $employee) { + $employeeId = $employee->getId(); + $site = $employee->getSite(); + $siteName = $site ? $site->getName() : 'Sans site'; + $siteId = $site ? $site->getId() : 0; + + $row = $this->buildEmployeeRow( + $employee, + $employeeId, + $days, + $contractMap[$employeeId] ?? [], + $driverMap[$employeeId] ?? [], + $workHourMap[$employeeId] ?? [], + $absenceMap[$employeeId] ?? [], + $rttPaymentMap[$employeeId] ?? 0, + $bonusMap[$employeeId] ?? 0.0, + $mileageMap[$employeeId] ?? 0.0, + ); + + if (!isset($siteGroups[$siteId])) { + $siteGroups[$siteId] = [ + 'name' => $siteName, + 'color' => $site?->getColor() ?? '#ffd7d7', + 'employees' => [], + ]; + } + + $siteGroups[$siteId]['employees'][] = $row; + } + + return $siteGroups; + } + + private function buildEmployeeRow( + Employee $employee, + int $employeeId, + array $days, + array $contractsByDate, + array $driverByDate, + array $workHoursByDate, + array $absences, + int $rttPaidMinutes, + float $bonusAmount, + float $mileageKm, + ): array { + $contractName = null; + $presenceDays = 0.0; + $nightMinutesTotal = 0; + $nightBasketCount = 0; + $sundayMinutesTotal = 0; + $isDriverAnyDay = false; + $driverBreakfast = 0; + $driverMeals = 0; + $driverOvernight = 0; + $driverSaturdays = 0; + $isForfait = false; + + foreach ($days as $date) { + $contract = $contractsByDate[$date] ?? null; + $isDriver = $driverByDate[$date] ?? false; + $wh = $workHoursByDate[$date] ?? null; + + if ($contract && null === $contractName) { + $contractName = $contract->getName(); + $isForfait = TrackingMode::PRESENCE === $contract->getTrackingModeEnum(); + } + + if ($isDriver) { + $isDriverAnyDay = true; + } + + if (!$wh) { + continue; + } + + $dayOfWeek = (int) new DateTimeImmutable($date)->format('N'); + + if ($isDriver) { + $nightMinutesTotal += $wh->getNightHoursMinutes() ?? 0; + $dayMin = $wh->getDayHoursMinutes() ?? 0; + $nightMin = $wh->getNightHoursMinutes() ?? 0; + if ($nightMin > $dayMin && $nightMin > 0) { + ++$nightBasketCount; + } + + if ($wh->getHasBreakfast()) { + ++$driverBreakfast; + } + if ($wh->getHasLunch() || $wh->getHasDinner()) { + ++$driverMeals; + } + if ($wh->getHasOvernight()) { + ++$driverOvernight; + } + + if (6 === $dayOfWeek && ($dayMin > 0 || $nightMin > 0 || ($wh->getWorkshopHoursMinutes() ?? 0) > 0)) { + ++$driverSaturdays; + } + + if (7 === $dayOfWeek) { + $sundayMinutesTotal += $dayMin + $nightMin + ($wh->getWorkshopHoursMinutes() ?? 0); + } + } else { + $metrics = $this->computeNightMinutes($wh); + $nightMinutesTotal += $metrics['nightMinutes']; + if ($metrics['nightMinutes'] > $metrics['dayMinutes'] && $metrics['nightMinutes'] > 0) { + ++$nightBasketCount; + } + + if (7 === $dayOfWeek) { + $sundayMinutesTotal += $metrics['dayMinutes'] + $metrics['nightMinutes']; + } + + // Samedi : les minutes après minuit débordent sur le dimanche + if (6 === $dayOfWeek) { + $sundayMinutesTotal += $this->computeOverflowAfterMidnight($wh); + } + + if ($isForfait) { + if ($wh->getIsPresentMorning()) { + $presenceDays += 0.5; + } + if ($wh->getIsPresentAfternoon()) { + $presenceDays += 0.5; + } + } + } + } + + $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); + + return [ + 'lastName' => mb_strimwidth($employee->getLastName() ?? '', 0, 15, '...'), + 'firstName' => mb_strimwidth($employee->getFirstName() ?? '', 0, 15, '...'), + 'contractName' => $contractName, + 'presenceDays' => $presenceDays, + 'mileageKm' => $mileageKm, + 'nightHours' => $nightHours, + 'nightBasketCount' => $nightBasketCount, + 'paidHours' => $paidHours, + 'sundayHours' => $sundayHours, + 'bonusAmount' => $bonusAmount, + 'congesCount' => $conges['count'], + 'congesDates' => $conges['dates'], + 'maladieCount' => $maladie['count'], + 'maladieDates' => $maladie['dates'], + 'isDriver' => $isDriverAnyDay, + 'driverBreakfast' => $driverBreakfast, + 'driverMeals' => $driverMeals, + 'driverOvernight' => $driverOvernight, + 'driverSaturdays' => $driverSaturdays, + ]; + } + + /** + * @return array{nightMinutes: int, dayMinutes: int} + */ + private function computeNightMinutes(WorkHour $workHour): array + { + $ranges = [ + [$workHour->getMorningFrom(), $workHour->getMorningTo()], + [$workHour->getAfternoonFrom(), $workHour->getAfternoonTo()], + [$workHour->getEveningFrom(), $workHour->getEveningTo()], + ]; + + $totalMinutes = 0; + $nightMinutes = 0; + + foreach ($ranges as [$from, $to]) { + $totalMinutes += $this->intervalMinutes($from, $to); + $nightMinutes += $this->nightIntervalMinutes($from, $to); + } + + $dayMinutes = max(0, $totalMinutes - $nightMinutes); + + return [ + 'nightMinutes' => $nightMinutes, + 'dayMinutes' => $dayMinutes, + ]; + } + + /** + * @return null|array{int, int} + */ + private function resolveInterval(?string $from, ?string $to): ?array + { + $fromMinutes = $this->toMinutes($from); + $toMinutes = $this->toMinutes($to); + if (null === $fromMinutes || null === $toMinutes) { + return null; + } + + $end = $toMinutes <= $fromMinutes ? $toMinutes + 1440 : $toMinutes; + + return [$fromMinutes, $end]; + } + + private function toMinutes(?string $time): ?int + { + if (null === $time || '' === $time) { + return null; + } + + [$hours, $minutes] = array_map('intval', explode(':', $time)); + + return ($hours * 60) + $minutes; + } + + private function intervalMinutes(?string $from, ?string $to): int + { + $interval = $this->resolveInterval($from, $to); + if (null === $interval) { + return 0; + } + + [$start, $end] = $interval; + + return max(0, $end - $start); + } + + private function nightIntervalMinutes(?string $from, ?string $to): int + { + $interval = $this->resolveInterval($from, $to); + if (null === $interval) { + return 0; + } + + [$start, $end] = $interval; + $windows = [[0, 360], [1260, 1440]]; + $total = 0; + + for ($dayOffset = 0; $dayOffset <= 1; ++$dayOffset) { + $shift = $dayOffset * 1440; + foreach ($windows as [$windowStart, $windowEnd]) { + $total += $this->overlap($start, $end, $windowStart + $shift, $windowEnd + $shift); + } + } + + return $total; + } + + /** + * Calcule les minutes qui débordent après minuit (> 1440) pour les créneaux d'un WorkHour. + * Ex: créneau soir 21:00-05:00 → interval [1260, 1740] → overflow = 1740-1440 = 300 min (5h). + */ + private function computeOverflowAfterMidnight(WorkHour $workHour): int + { + $ranges = [ + [$workHour->getMorningFrom(), $workHour->getMorningTo()], + [$workHour->getAfternoonFrom(), $workHour->getAfternoonTo()], + [$workHour->getEveningFrom(), $workHour->getEveningTo()], + ]; + + $overflow = 0; + + foreach ($ranges as [$from, $to]) { + $interval = $this->resolveInterval($from, $to); + if (null === $interval) { + continue; + } + + [$start, $end] = $interval; + + // Si le créneau dépasse minuit (1440), la partie au-delà est sur le jour suivant + if ($end > 1440) { + $overflow += $end - max($start, 1440); + } + } + + return $overflow; + } + + private function overlap(int $startA, int $endA, int $startB, int $endB): int + { + $start = max($startA, $startB); + $end = min($endA, $endB); + + return max(0, $end - $start); + } + + /** + * @param list $absences + * @param list $codes + * + * @return array{count: float, dates: string} + */ + private function countAbsencesByCode(array $absences, array $codes): array + { + $count = 0.0; + $dayKeys = []; + + foreach ($absences as $absence) { + $type = $absence->getType(); + if (!$type || !in_array($type->getCode(), $codes, true)) { + continue; + } + + $startHalf = $absence->getStartHalf(); + $endHalf = $absence->getEndHalf(); + + if ($startHalf === $endHalf) { + $count += 0.5; + } else { + $count += 1.0; + } + + $dayKeys[] = $absence->getStartDate()->format('Y-m-d'); + } + + sort($dayKeys); + $dayKeys = array_unique($dayKeys); + + $periods = $this->mergeDaysIntoPeriods($dayKeys); + + return [ + 'count' => $count, + 'dates' => implode(', ', $periods), + ]; + } + + /** + * @param list $sortedDates Y-m-d sorted + * + * @return list + */ + private function mergeDaysIntoPeriods(array $sortedDates): array + { + if ([] === $sortedDates) { + return []; + } + + $periods = []; + $rangeStart = $sortedDates[0]; + $rangeEnd = $sortedDates[0]; + + for ($i = 1, $len = count($sortedDates); $i < $len; ++$i) { + $prev = new DateTimeImmutable($rangeEnd); + $current = new DateTimeImmutable($sortedDates[$i]); + + if (1 === $current->diff($prev)->days) { + $rangeEnd = $sortedDates[$i]; + } else { + $periods[] = $this->formatPeriod($rangeStart, $rangeEnd); + $rangeStart = $sortedDates[$i]; + $rangeEnd = $sortedDates[$i]; + } + } + + $periods[] = $this->formatPeriod($rangeStart, $rangeEnd); + + return $periods; + } + + private function formatPeriod(string $start, string $end): string + { + $s = new DateTimeImmutable($start)->format('d/m'); + + if ($start === $end) { + return $s; + } + + return 'Du '.$s.' au '.new DateTimeImmutable($end)->format('d/m'); + } +} diff --git a/templates/salary-recap/print.html.twig b/templates/salary-recap/print.html.twig new file mode 100644 index 0000000..66d4c00 --- /dev/null +++ b/templates/salary-recap/print.html.twig @@ -0,0 +1,163 @@ + + + + + Récapitulatif Salaire + + + + + +{% set months = { + 1:'Janvier', 2:'Février', 3:'Mars', 4:'Avril', 5:'Mai', 6:'Juin', + 7:'Juillet', 8:'Août', 9:'Septembre', 10:'Octobre', 11:'Novembre', 12:'Décembre' +} %} + +
+

RECAPITULATIF SALAIRE DU {{ from|date('d/m/Y') }} au {{ to|date('d/m/Y') }}

+
{{ months[from|date('n')|number_format] }} {{ from|date('Y') }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% for siteId, group in siteGroups %} + {% set siteColor = group.color ?? '#B3E5FC' %} + + + + {% for row in group.employees %} + + + + + + + + + + + + + + + + + + + + + {% else %} + + + + {% endfor %} + {% endfor %} + +
NomBaseJour de
présence
Cadre
Frais
Kms
Heures
de
nuit
Panier
de
nuit
Heures
payés
Heures
dim.
PrimeCongésMaladieCHAUFFEURObservations
NbreDateNbreDatePDJREPASNUITEEsamedi
{{ row.lastName }}
{{ row.firstName }}
{{ row.contractName ?? '' }}{{ row.presenceDays > 0 ? row.presenceDays : '' }}{{ row.mileageKm > 0 ? row.mileageKm : '' }}{{ row.nightHours > 0 ? row.nightHours : '' }}{{ row.nightBasketCount > 0 ? row.nightBasketCount : '' }}{{ row.paidHours > 0 ? row.paidHours : '' }}{{ row.sundayHours > 0 ? row.sundayHours : '' }}{{ row.bonusAmount > 0 ? row.bonusAmount : '' }}{{ row.congesCount > 0 ? row.congesCount : '' }}{{ row.congesDates }}{{ row.maladieCount > 0 ? row.maladieCount : '' }}{{ row.maladieDates }}{{ row.isDriver and row.driverBreakfast > 0 ? row.driverBreakfast : '' }}{{ row.isDriver and row.driverMeals > 0 ? row.driverMeals : '' }}{{ row.isDriver and row.driverOvernight > 0 ? row.driverOvernight : '' }}{{ row.isDriver and row.driverSaturdays > 0 ? row.driverSaturdays : '' }}
Aucun employé.
+ + +