fix : wip
This commit is contained in:
293
src/State/WorkHourWeeklySummaryProvider.php
Normal file
293
src/State/WorkHourWeeklySummaryProvider.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\WorkHourWeeklySummary;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkHour;
|
||||
use App\Repository\EmployeeRepository;
|
||||
use App\Repository\WorkHourRepository;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private RequestStack $requestStack,
|
||||
private EmployeeRepository $employeeRepository,
|
||||
private WorkHourRepository $workHourRepository,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
|
||||
{
|
||||
$user = $this->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<string>}
|
||||
*/
|
||||
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<Employee> $employees
|
||||
* @param list<WorkHour> $workHours
|
||||
* @param list<string> $days
|
||||
*
|
||||
* @return list<array{
|
||||
* employeeId:int,
|
||||
* firstName:string,
|
||||
* lastName:string,
|
||||
* siteName:?string,
|
||||
* contractName:?string,
|
||||
* trackingMode:?string,
|
||||
* daily:list<array{date:string, dayMinutes:int, nightMinutes:int, totalMinutes:int, present:?float}>,
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user