*/ final class WorkHourRepository extends ServiceEntityRepository implements WorkHourReadRepositoryInterface { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, WorkHour::class); } /** * @param list $employees * * @return array */ public function findByDateAndEmployeesIndexedByEmployeeId(DateTimeImmutable $workDate, array $employees): array { if ([] === $employees) { return []; } $qb = $this->createQueryBuilder('w') ->leftJoin('w.employee', 'e') ->addSelect('e') ->andWhere('w.workDate = :workDate') ->andWhere('w.employee IN (:employees)') ->setParameter('workDate', $workDate) ->setParameter('employees', $employees) ; /** @var list $workHours */ $workHours = $qb->getQuery()->getResult(); $byEmployeeId = []; foreach ($workHours as $workHour) { $employeeId = $workHour->getEmployee()?->getId(); if ($employeeId) { $byEmployeeId[$employeeId] = $workHour; } } return $byEmployeeId; } /** * @param list $employees * * @return list */ public function findByDateRangeAndEmployees(DateTimeImmutable $from, DateTimeImmutable $to, array $employees): array { if ([] === $employees) { return []; } $qb = $this->createQueryBuilder('w') ->leftJoin('w.employee', 'e') ->addSelect('e') ->andWhere('w.workDate >= :from') ->andWhere('w.workDate <= :to') ->andWhere('w.employee IN (:employees)') ->setParameter('from', $from) ->setParameter('to', $to) ->setParameter('employees', $employees) ; // @var list $workHours return $qb->getQuery()->getResult(); } public function hasValidatedInRange(Employee $employee, DateTimeInterface $from, DateTimeInterface $to): bool { $fromDate = DateTimeImmutable::createFromInterface($from); $toDate = DateTimeImmutable::createFromInterface($to); $qb = $this->createQueryBuilder('w') ->select('COUNT(w.id)') ->andWhere('w.employee = :employee') ->andWhere('w.workDate >= :from') ->andWhere('w.workDate <= :to') ->andWhere('w.isValid = :isValid') ->setParameter('employee', $employee) ->setParameter('from', $fromDate) ->setParameter('to', $toDate) ->setParameter('isValid', true) ; return ((int) $qb->getQuery()->getSingleScalarResult()) > 0; } public function hasSiteValidatedInRange(Employee $employee, DateTimeInterface $from, DateTimeInterface $to): bool { $fromDate = DateTimeImmutable::createFromInterface($from); $toDate = DateTimeImmutable::createFromInterface($to); $qb = $this->createQueryBuilder('w') ->select('COUNT(w.id)') ->andWhere('w.employee = :employee') ->andWhere('w.workDate >= :from') ->andWhere('w.workDate <= :to') ->andWhere('w.isSiteValid = :isSiteValid') ->setParameter('employee', $employee) ->setParameter('from', $fromDate) ->setParameter('to', $toDate) ->setParameter('isSiteValid', true) ; return ((int) $qb->getQuery()->getSingleScalarResult()) > 0; } public function findOneByEmployeeAndDate(Employee $employee, DateTimeInterface $date): ?WorkHour { $workDate = DateTimeImmutable::createFromInterface($date); $qb = $this->createQueryBuilder('w') ->andWhere('w.employee = :employee') ->andWhere('w.workDate = :workDate') ->setParameter('employee', $employee) ->setParameter('workDate', $workDate) ->setMaxResults(1) ; // @var null|WorkHour $workHour return $qb->getQuery()->getOneOrNullResult(); } /** * Count weekend worked days by month. * >= 5h total = 1.0 day, < 5h = 0.5 day. * * @return array YYYY-MM => weekend worked day count */ public function countWeekendWorkedDaysByMonth(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array { $sql = <<<'SQL' SELECT TO_CHAR(work_date, 'YYYY-MM') AS month, SUM( CASE WHEN total_minutes >= 300 THEN 1.0 WHEN total_minutes > 0 THEN 0.5 ELSE 0 END ) AS cnt FROM ( SELECT work_date, COALESCE( EXTRACT(EPOCH FROM (morning_to::time - morning_from::time)) / 60, 0 ) + COALESCE( EXTRACT(EPOCH FROM (afternoon_to::time - afternoon_from::time)) / 60, 0 ) + COALESCE( EXTRACT(EPOCH FROM (evening_to::time - evening_from::time)) / 60, 0 ) AS total_minutes FROM work_hours WHERE employee_id = :employee AND work_date >= :from AND work_date <= :to AND EXTRACT(ISODOW FROM work_date) IN (6, 7) AND (morning_from IS NOT NULL OR afternoon_from IS NOT NULL OR evening_from IS NOT NULL) ) sub GROUP BY month SQL; $conn = $this->getEntityManager()->getConnection(); $rows = $conn->fetchAllAssociative($sql, [ 'employee' => $employee->getId(), 'from' => $from->format('Y-m-d'), 'to' => $to->format('Y-m-d'), ]); $result = []; foreach ($rows as $row) { $result[(string) $row['month']] = (float) $row['cnt']; } return $result; } /** * Count weekend and public holiday worked days for forfait bonus leave (PRESENCE mode only). * Morning + afternoon = 1.0 day, one only = 0.5 day. * * @param list $publicHolidayDates Y-m-d formatted weekday public holiday dates */ public function countWeekendAndHolidayWorkedDays(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to, array $publicHolidayDates = []): float { $targetDates = []; // Collect weekend dates in range for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) { if ((int) $cursor->format('N') >= 6) { $targetDates[] = $cursor; } } // Add weekday public holidays foreach ($publicHolidayDates as $date) { $targetDates[] = new DateTimeImmutable($date); } if ([] === $targetDates) { return 0.0; } $dateStrings = array_map(static fn (DateTimeImmutable $d): string => $d->format('Y-m-d'), $targetDates); /** @var list $rows */ $rows = $this->createQueryBuilder('w') ->andWhere('w.employee = :employee') ->andWhere('w.workDate IN (:dates)') ->andWhere('w.isPresentMorning = true OR w.isPresentAfternoon = true') ->setParameter('employee', $employee) ->setParameter('dates', $dateStrings) ->getQuery() ->getResult() ; $total = 0.0; foreach ($rows as $row) { if ($row->isPresentMorning() && $row->isPresentAfternoon()) { $total += 1.0; } else { $total += 0.5; } } return $total; } /** * Return the set of Y-m-d dates where the employee has worked hours on the given dates. * * @param list $dates Y-m-d formatted dates * * @return array Y-m-d => true */ public function findWorkedDatesAmong(Employee $employee, array $dates): array { if ([] === $dates) { return []; } $placeholders = []; $params = ['employee' => $employee->getId()]; foreach (array_values($dates) as $i => $date) { $key = "d{$i}"; $placeholders[] = ":{$key}"; $params[$key] = $date; } $sql = sprintf( 'SELECT work_date FROM work_hours WHERE employee_id = :employee AND work_date IN (%s) AND (morning_from IS NOT NULL OR afternoon_from IS NOT NULL OR evening_from IS NOT NULL)', implode(', ', $placeholders) ); $conn = $this->getEntityManager()->getConnection(); $rows = $conn->fetchAllAssociative($sql, $params); $result = []; foreach ($rows as $row) { $result[(string) $row['work_date']] = true; } return $result; } public function isWeekFullyValidated(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool { // Count weekdays (Mon-Fri) in range $expectedWeekdays = 0; for ($d = $from; $d <= $to; $d = $d->modify('+1 day')) { if ((int) $d->format('N') <= 5) { ++$expectedWeekdays; } } if (0 === $expectedWeekdays) { return false; } // Every weekday must have a work_hour row $totalCount = (int) $this->createQueryBuilder('w') ->select('COUNT(w.id)') ->andWhere('w.employee = :employee') ->andWhere('w.workDate >= :from') ->andWhere('w.workDate <= :to') ->setParameter('employee', $employee) ->setParameter('from', $from) ->setParameter('to', $to) ->getQuery() ->getSingleScalarResult() ; if ($totalCount < $expectedWeekdays) { return false; } // All rows must be validated $nonValidatedCount = (int) $this->createQueryBuilder('w') ->select('COUNT(w.id)') ->andWhere('w.employee = :employee') ->andWhere('w.workDate >= :from') ->andWhere('w.workDate <= :to') ->andWhere('w.isValid = :isValid') ->setParameter('employee', $employee) ->setParameter('from', $from) ->setParameter('to', $to) ->setParameter('isValid', false) ->getQuery() ->getSingleScalarResult() ; return 0 === $nonValidatedCount; } public function hasPendingSiteValidationForSiteAndDate(int $siteId, DateTimeInterface $date): bool { $workDate = DateTimeImmutable::createFromInterface($date); $qb = $this->createQueryBuilder('w') ->select('COUNT(w.id)') ->leftJoin('w.employee', 'e') ->leftJoin('e.site', 's') ->andWhere('s.id = :siteId') ->andWhere('w.workDate = :workDate') ->andWhere('w.isSiteValid = :isSiteValid') ->setParameter('siteId', $siteId) ->setParameter('workDate', $workDate) ->setParameter('isSiteValid', false) ; return ((int) $qb->getQuery()->getSingleScalarResult()) > 0; } }