security->getUser(); if (!$user instanceof User) { throw new AccessDeniedHttpException('Authentication required.'); } $anchorDate = $this->resolveAnchorDate(); [$weekStart, $weekEnd, $days] = $this->resolveWeek($anchorDate); $employees = $this->employeeRepository->findScoped($user); $workHours = $this->workHourRepository->findByDateRangeAndEmployees($weekStart, $weekEnd, $employees); $summary = new WorkHourWeeklySummary(); $summary->weekStart = $weekStart->format('Y-m-d'); $summary->weekEnd = $weekEnd->format('Y-m-d'); $summary->days = $days; $summary->rows = $this->buildRows($employees, $workHours, $days); return $summary; } private function resolveAnchorDate(): DateTimeImmutable { $query = $this->requestStack->getCurrentRequest()?->query; $raw = (string) ($query?->get('weekStart') ?? ''); if ('' === $raw) { return new DateTimeImmutable('today'); } $date = DateTimeImmutable::createFromFormat('Y-m-d', $raw); if (!$date || $date->format('Y-m-d') !== $raw) { throw new UnprocessableEntityHttpException('weekStart must use Y-m-d format.'); } return $date; } /** * @return array{DateTimeImmutable, DateTimeImmutable, list} */ private function resolveWeek(DateTimeImmutable $anchorDate): array { $dayOfWeek = (int) $anchorDate->format('N'); $weekStart = $anchorDate->modify(sprintf('-%d days', $dayOfWeek - 1)); $weekEnd = $weekStart->modify('+6 days'); $days = []; for ($i = 0; $i < 7; ++$i) { $days[] = $weekStart->modify(sprintf('+%d days', $i))->format('Y-m-d'); } return [$weekStart, $weekEnd, $days]; } /** * @param list $employees * @param list $workHours * @param list $days * * @return list, * weeklyDayMinutes:int, * weeklyNightMinutes:int, * weeklyTotalMinutes:int, * weeklyPresenceCount:float, * weeklyOvertime25Minutes:int, * weeklyOvertime50Minutes:int * }> */ private function buildRows(array $employees, array $workHours, array $days): array { $metricsByEmployeeDate = []; foreach ($workHours as $workHour) { $employeeId = $workHour->getEmployee()?->getId(); if (!$employeeId) { continue; } $dateKey = $workHour->getWorkDate()->format('Y-m-d'); $metricsByEmployeeDate[$employeeId][$dateKey] = [ 'metrics' => $this->computeMetrics($workHour), 'isPresentMorning' => $workHour->getIsPresentMorning(), 'isPresentAfternoon' => $workHour->getIsPresentAfternoon(), ]; } $rows = []; foreach ($employees as $employee) { $employeeId = $employee->getId(); if (!$employeeId) { continue; } $weeklyDayMinutes = 0; $weeklyNightMinutes = 0; $weeklyTotalMinutes = 0; $weeklyPresenceCount = 0.0; $daily = []; $isPresenceTracking = 'PRESENCE' === $employee->getContract()?->getTrackingMode(); foreach ($days as $date) { $entry = $metricsByEmployeeDate[$employeeId][$date] ?? null; $metrics = $entry['metrics'] ?? [ 'dayMinutes' => 0, 'nightMinutes' => 0, 'totalMinutes' => 0, ]; $present = null; if ($isPresenceTracking) { $morning = ($entry['isPresentMorning'] ?? false) ? 0.5 : 0.0; $afternoon = ($entry['isPresentAfternoon'] ?? false) ? 0.5 : 0.0; $present = $morning + $afternoon; } $weeklyDayMinutes += $metrics['dayMinutes']; $weeklyNightMinutes += $metrics['nightMinutes']; $weeklyTotalMinutes += $metrics['totalMinutes']; if (null !== $present) { $weeklyPresenceCount += $present; } $daily[] = [ 'date' => $date, 'dayMinutes' => $metrics['dayMinutes'], 'nightMinutes' => $metrics['nightMinutes'], 'totalMinutes' => $metrics['totalMinutes'], 'present' => $present, ]; } $rows[] = [ 'employeeId' => $employeeId, 'firstName' => $employee->getFirstName(), 'lastName' => $employee->getLastName(), 'siteName' => $employee->getSite()?->getName(), 'contractName' => $employee->getContract()?->getName(), 'trackingMode' => $employee->getContract()?->getTrackingMode(), 'daily' => $daily, 'weeklyDayMinutes' => $weeklyDayMinutes, 'weeklyNightMinutes' => $weeklyNightMinutes, 'weeklyTotalMinutes' => $weeklyTotalMinutes, 'weeklyPresenceCount' => $weeklyPresenceCount, 'weeklyOvertime25Minutes' => $isPresenceTracking ? 0 : $this->computeOvertime25Minutes($weeklyTotalMinutes), 'weeklyOvertime50Minutes' => $isPresenceTracking ? 0 : $this->computeOvertime50Minutes($weeklyTotalMinutes), ]; } return $rows; } /** * @return array{dayMinutes:int, nightMinutes:int, totalMinutes:int} */ private function computeMetrics(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 [ 'dayMinutes' => $dayMinutes, 'nightMinutes' => $nightMinutes, 'totalMinutes' => $totalMinutes, ]; } /** * @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; } 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); } private function computeOvertime25Minutes(int $weeklyTotalMinutes): int { return max(0, min($weeklyTotalMinutes, 43 * 60) - (35 * 60)); } private function computeOvertime50Minutes(int $weeklyTotalMinutes): int { return max(0, $weeklyTotalMinutes - (43 * 60)); } }