155 lines
4.6 KiB
PHP
155 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Model;
|
|
|
|
use App\Saas\Model\TimeAccount;
|
|
use App\Saas\Model\TimeAccountDay;
|
|
|
|
/**
|
|
* The days of one work month, and the only place that knows how they are stored.
|
|
*
|
|
* TeamerWorkMonth keeps its days in a JSON column rather than in rows: every read is
|
|
* "one teamer, one month", which one row answers directly. The price is that the shape is
|
|
* invisible to the schema, so it is kept small and passes through here alone - a change to
|
|
* it is a change to this file and nothing else.
|
|
*
|
|
* Shape, with everything optional but "minutes" left out when it has nothing to say:
|
|
* {"2026-03-01": {"minutes": 285, "break": 60, "start": "09:00", "end": "14:45"},
|
|
* "2026-03-05": {"minutes": 0, "absence": "Ruhetag 24/25"}}
|
|
* Days the SaaS did not report are simply absent; forCalendarMonth() fills the gaps.
|
|
*/
|
|
class WorkMonthDays
|
|
{
|
|
/**
|
|
* @param array<string, array{minutes: int, break?: int, start?: string, end?: string, absence?: string}> $days keyed by Y-m-d
|
|
*/
|
|
private function __construct(private readonly array $days)
|
|
{
|
|
}
|
|
|
|
public static function fromArray(array $days): self
|
|
{
|
|
$clean = [];
|
|
|
|
foreach ($days as $key => $value) {
|
|
if (!is_string($key) || !is_array($value) || 1 !== preg_match('/^\d{4}-\d{2}-\d{2}$/', $key)) {
|
|
continue;
|
|
}
|
|
|
|
$entry = ['minutes' => (int) ($value['minutes'] ?? 0)];
|
|
|
|
if (0 < $break = (int) ($value['break'] ?? 0)) {
|
|
$entry['break'] = $break;
|
|
}
|
|
|
|
$start = self::cleanTime($value['start'] ?? null);
|
|
$end = self::cleanTime($value['end'] ?? null);
|
|
|
|
// A span needs both ends; half of one says nothing.
|
|
if (null !== $start && null !== $end) {
|
|
$entry['start'] = $start;
|
|
$entry['end'] = $end;
|
|
}
|
|
|
|
$absence = $value['absence'] ?? null;
|
|
|
|
if (is_string($absence) && '' !== $absence) {
|
|
$entry['absence'] = $absence;
|
|
}
|
|
|
|
$clean[$key] = $entry;
|
|
}
|
|
|
|
ksort($clean);
|
|
|
|
return new self($clean);
|
|
}
|
|
|
|
public static function fromTimeAccount(TimeAccount $account): self
|
|
{
|
|
$days = [];
|
|
|
|
foreach ($account->getDays() as $key => $day) {
|
|
/* @var TimeAccountDay $day */
|
|
$days[$key] = ['minutes' => $day->getMinutes()];
|
|
|
|
if (0 < $day->getBreakMinutes()) {
|
|
$days[$key]['break'] = $day->getBreakMinutes();
|
|
}
|
|
|
|
if (null !== $day->getStartTime() && null !== $day->getEndTime()) {
|
|
$days[$key]['start'] = $day->getStartTime();
|
|
$days[$key]['end'] = $day->getEndTime();
|
|
}
|
|
|
|
if (null !== $day->getHolidayType()) {
|
|
$days[$key]['absence'] = $day->getHolidayType();
|
|
}
|
|
}
|
|
|
|
return new self($days);
|
|
}
|
|
|
|
private static function cleanTime(mixed $raw): ?string
|
|
{
|
|
return is_string($raw) && 1 === preg_match('/^\d{2}:\d{2}$/', $raw) ? $raw : null;
|
|
}
|
|
|
|
public function toArray(): array
|
|
{
|
|
return $this->days;
|
|
}
|
|
|
|
public function getTotalMinutes(): int
|
|
{
|
|
return array_sum(array_column($this->days, 'minutes'));
|
|
}
|
|
|
|
public function countWorkedDays(): int
|
|
{
|
|
return count(array_filter($this->days, static fn (array $day) => ($day['minutes'] ?? 0) > 0));
|
|
}
|
|
|
|
public function countAbsenceDays(): int
|
|
{
|
|
return count(array_filter($this->days, static fn (array $day) => isset($day['absence'])));
|
|
}
|
|
|
|
public function getTotalBreakMinutes(): int
|
|
{
|
|
return array_sum(array_map(static fn (array $day) => $day['break'] ?? 0, $this->days));
|
|
}
|
|
|
|
/**
|
|
* Every day of the month in order, reported or not, for tables and the PDF.
|
|
*
|
|
* @return list<WorkMonthDay>
|
|
*/
|
|
public function forCalendarMonth(int $year, int $month): array
|
|
{
|
|
$first = \DateTimeImmutable::createFromFormat('!Y-n-j', sprintf('%04d-%d-1', $year, $month));
|
|
|
|
if (false === $first) {
|
|
return [];
|
|
}
|
|
|
|
$days = [];
|
|
|
|
for ($day = 1, $last = (int) $first->format('t'); $day <= $last; ++$day) {
|
|
$date = $first->setDate($year, $month, $day);
|
|
$entry = $this->days[$date->format('Y-m-d')] ?? null;
|
|
|
|
$days[] = new WorkMonthDay(
|
|
$date,
|
|
null !== $entry ? (int) $entry['minutes'] : null,
|
|
$entry['absence'] ?? null,
|
|
$entry['start'] ?? null,
|
|
$entry['end'] ?? null,
|
|
(int) ($entry['break'] ?? 0),
|
|
);
|
|
}
|
|
|
|
return $days;
|
|
}
|
|
}
|