Support for changeable work contract types (#5069)

This commit is contained in:
Kevin Papst
2024-09-22 16:18:21 +02:00
committed by GitHub
parent 8de54e1fa7
commit 4076e1c3d3
23 changed files with 830 additions and 85 deletions

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\WorkingTime\Calculator;
interface WorkingTimeCalculator
{
/**
* @return int seconds
*/
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int;
public function isWorkDay(\DateTimeInterface $dateTime): bool;
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\WorkingTime\Calculator;
use App\Entity\User;
final class WorkingTimeCalculatorDay implements WorkingTimeCalculator
{
public const WORK_HOURS_MONDAY = 'work_monday';
public const WORK_HOURS_TUESDAY = 'work_tuesday';
public const WORK_HOURS_WEDNESDAY = 'work_wednesday';
public const WORK_HOURS_THURSDAY = 'work_thursday';
public const WORK_HOURS_FRIDAY = 'work_friday';
public const WORK_HOURS_SATURDAY = 'work_saturday';
public const WORK_HOURS_SUNDAY = 'work_sunday';
public function __construct(private readonly User $user)
{
}
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
{
return (int) match ($dateTime->format('N')) {
'1' => $this->user->getPreferenceValue(self::WORK_HOURS_MONDAY, 0),
'2' => $this->user->getPreferenceValue(self::WORK_HOURS_TUESDAY, 0),
'3' => $this->user->getPreferenceValue(self::WORK_HOURS_WEDNESDAY, 0),
'4' => $this->user->getPreferenceValue(self::WORK_HOURS_THURSDAY, 0),
'5' => $this->user->getPreferenceValue(self::WORK_HOURS_FRIDAY, 0),
'6' => $this->user->getPreferenceValue(self::WORK_HOURS_SATURDAY, 0),
'7' => $this->user->getPreferenceValue(self::WORK_HOURS_SUNDAY, 0),
default => throw new \Exception('Unknown day: ' . $dateTime->format('Y-m-d'))
};
}
public function isWorkDay(\DateTimeInterface $dateTime): bool
{
return $this->getWorkHoursForDay($dateTime) > 0;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\WorkingTime\Calculator;
final class WorkingTimeCalculatorNone implements WorkingTimeCalculator
{
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
{
return 0;
}
public function isWorkDay(\DateTimeInterface $dateTime): bool
{
// we don't know it, so we must assume every day is a a working day
return true;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\WorkingTime\Mode;
use App\Entity\User;
use App\Form\Model\UserContractModel;
use App\WorkingTime\Calculator\WorkingTimeCalculator;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\Form\FormBuilderInterface;
#[AutoconfigureTag]
interface WorkingTimeMode
{
/**
* Short and unique identifier for this mode.
*/
public function getId(): string;
/**
* @return int<0, 100>
*/
public function getOrder(): int;
/**
* Translation key for the name of this mode.
*/
public function getName(): string;
/**
* @param FormBuilderInterface<UserContractModel> $builder
* @param array<mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void;
public function getCalculator(User $user): WorkingTimeCalculator;
/**
* @return array<int, string>
*/
public function getFormFields(): array;
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\WorkingTime\Mode;
use App\Entity\User;
use App\Form\Type\DurationType;
use App\WorkingTime\Calculator\WorkingTimeCalculator;
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\LessThanOrEqual;
class WorkingTimeModeDay implements WorkingTimeMode
{
final public const ID = 'day';
/**
* @var array<string, string>
*/
private array $fields = [
'workHoursMonday' => 'Monday',
'workHoursTuesday' => 'Tuesday',
'workHoursWednesday' => 'Wednesday',
'workHoursThursday' => 'Thursday',
'workHoursFriday' => 'Friday',
'workHoursSaturday' => 'Saturday',
'workHoursSunday' => 'Sunday',
];
public function getId(): string
{
return self::ID;
}
public function getOrder(): int
{
return 10;
}
public function getName(): string
{
return 'hours_per_day';
}
public function getFormFields(): array
{
return array_keys($this->fields);
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$durationOptions = [
'required' => false,
'translation_domain' => 'system-configuration',
'constraints' => [
new GreaterThanOrEqual(0),
new LessThanOrEqual(86400),
],
];
foreach ($this->fields as $field => $label) {
$builder->add($field, DurationType::class, array_merge(['label' => $label], $durationOptions));
}
}
public function getCalculator(User $user): WorkingTimeCalculator
{
return new WorkingTimeCalculatorDay($user);
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\WorkingTime\Mode;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
final class WorkingTimeModeFactory
{
/**
* @param iterable<WorkingTimeMode> $modes
*/
public function __construct(
#[TaggedIterator(WorkingTimeMode::class)]
private readonly iterable $modes
)
{
}
/**
* @return WorkingTimeMode[]
*/
public function getAll(): array
{
$modes = [];
foreach ($this->modes as $mode) {
$modes[] = $mode;
}
return $modes;
}
public function getModeForUser(User $user): WorkingTimeMode
{
return $this->getMode($user->getWorkContractMode());
}
public function getMode(string $contractMode): WorkingTimeMode
{
foreach ($this->modes as $mode) {
if ($mode->getId() === $contractMode) {
return $mode;
}
}
throw new \InvalidArgumentException('Unknown working contract mode: ' . $contractMode);
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\WorkingTime\Mode;
use App\Entity\User;
use App\WorkingTime\Calculator\WorkingTimeCalculator;
use App\WorkingTime\Calculator\WorkingTimeCalculatorNone;
use Symfony\Component\Form\FormBuilderInterface;
class WorkingTimeModeNone implements WorkingTimeMode
{
final public const ID = 'none';
public function getId(): string
{
return self::ID;
}
public function getOrder(): int
{
return 0;
}
public function getFormFields(): array
{
return [];
}
public function getName(): string
{
return '';
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// nothing to do here
}
public function getCalculator(User $user): WorkingTimeCalculator
{
return new WorkingTimeCalculatorNone();
}
}

View File

@@ -17,13 +17,15 @@ use App\Event\WorkingTimeYearSummaryEvent;
use App\Repository\TimesheetRepository;
use App\Repository\WorkingTimeRepository;
use App\Timesheet\DateTimeFactory;
use App\WorkingTime\Mode\WorkingTimeMode;
use App\WorkingTime\Mode\WorkingTimeModeFactory;
use App\WorkingTime\Model\Month;
use App\WorkingTime\Model\Year;
use App\WorkingTime\Model\YearPerUserSummary;
use Psr\EventDispatcher\EventDispatcherInterface;
/**
* @internal this API and the entire namespace is instable: you should expect changes!
* @internal this API and the entire namespace is experimental: expect changes!
*/
final class WorkingTimeService
{
@@ -33,11 +35,17 @@ final class WorkingTimeService
public function __construct(
private readonly TimesheetRepository $timesheetRepository,
private readonly WorkingTimeRepository $workingTimeRepository,
private readonly EventDispatcherInterface $eventDispatcher
private readonly EventDispatcherInterface $eventDispatcher,
private readonly WorkingTimeModeFactory $contractModeService
)
{
}
public function getContractMode(User $user): WorkingTimeMode
{
return $this->contractModeService->getModeForUser($user);
}
public function getYearSummary(Year $year, \DateTimeInterface $until): YearPerUserSummary
{
$yearPerUserSummary = new YearPerUserSummary($year);
@@ -94,6 +102,7 @@ final class WorkingTimeService
$stats = null;
$firstDay = $user->getWorkStartingDay();
$calculator = $this->getContractMode($user)->getCalculator($user);
foreach ($year->getMonths() as $month) {
foreach ($month->getDays() as $day) {
@@ -111,7 +120,7 @@ final class WorkingTimeService
$result = new WorkingTime($user, $dayDate);
if ($firstDay === null || $firstDay <= $dayDate) {
$result->setExpectedTime($user->getWorkHoursForDay($dayDate));
$result->setExpectedTime($calculator->getWorkHoursForDay($dayDate));
}
if (\array_key_exists($key, $stats)) {