feat: import work hours from SaaS and exchange signed time sheet
This commit is contained in:
1 parent
7cf39850e6
commit
1a7c272414
73 files changed
+10488
-72
No files matched your search
@@ -0,0 +1,154 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user