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

@@ -15,6 +15,7 @@ use App\Entity\UserPreference;
use App\Event\PrepareUserEvent;
use App\Form\AccessTokenForm;
use App\Form\Model\TotpActivation;
use App\Form\Model\UserContractModel;
use App\Form\UserApiPasswordType;
use App\Form\UserContractType;
use App\Form\UserEditType;
@@ -267,7 +268,7 @@ final class ProfileController extends AbstractController
#[IsGranted('contract', 'profile')]
public function contractAction(User $profile, Request $request, UserRepository $userRepository): Response
{
$form = $this->createForm(UserContractType::class, $profile, [
$form = $this->createForm(UserContractType::class, new UserContractModel($profile), [
'action' => $this->generateUrl('user_profile_contract', ['username' => $profile->getUserIdentifier()]),
'method' => 'POST',
]);

View File

@@ -12,6 +12,7 @@ namespace App\Entity;
use App\Export\Annotation as Exporter;
use App\Utils\StringHelper;
use App\Validator\Constraints as Constraints;
use App\WorkingTime\Mode\WorkingTimeModeNone;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@@ -1232,36 +1233,57 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
/**
* @deprecated since 2.22.0
*/
public function getWorkHoursMonday(): int
{
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_MONDAY, 0);
}
/**
* @deprecated since 2.22.0
*/
public function getWorkHoursTuesday(): int
{
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, 0);
}
/**
* @deprecated since 2.22.0
*/
public function getWorkHoursWednesday(): int
{
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, 0);
}
/**
* @deprecated since 2.22.0
*/
public function getWorkHoursThursday(): int
{
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, 0);
}
/**
* @deprecated since 2.22.0
*/
public function getWorkHoursFriday(): int
{
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, 0);
}
/**
* @deprecated since 2.22.0
*/
public function getWorkHoursSaturday(): int
{
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, 0);
}
/**
* @deprecated since 2.22.0
*/
public function getWorkHoursSunday(): int
{
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, 0);
@@ -1302,39 +1324,60 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return $this->getFormattedHoliday(is_numeric($holidays) ? $holidays : 0.0);
}
public function setWorkHoursMonday(int $seconds): void
/**
* @deprecated since 2.22.0
*/
public function setWorkHoursMonday(?int $seconds): void
{
$this->setPreferenceValue(UserPreference::WORK_HOURS_MONDAY, $seconds);
$this->setPreferenceValue(UserPreference::WORK_HOURS_MONDAY, $seconds ?? 0);
}
public function setWorkHoursTuesday(int $seconds): void
/**
* @deprecated since 2.22.0
*/
public function setWorkHoursTuesday(?int $seconds): void
{
$this->setPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, $seconds);
$this->setPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, $seconds ?? 0);
}
public function setWorkHoursWednesday(int $seconds): void
/**
* @deprecated since 2.22.0
*/
public function setWorkHoursWednesday(?int $seconds): void
{
$this->setPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, $seconds);
$this->setPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, $seconds ?? 0);
}
public function setWorkHoursThursday(int $seconds): void
/**
* @deprecated since 2.22.0
*/
public function setWorkHoursThursday(?int $seconds): void
{
$this->setPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, $seconds);
$this->setPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, $seconds ?? 0);
}
public function setWorkHoursFriday(int $seconds): void
/**
* @deprecated since 2.22.0
*/
public function setWorkHoursFriday(?int $seconds): void
{
$this->setPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, $seconds);
$this->setPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, $seconds ?? 0);
}
public function setWorkHoursSaturday(int $seconds): void
/**
* @deprecated since 2.22.0
*/
public function setWorkHoursSaturday(?int $seconds): void
{
$this->setPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, $seconds);
$this->setPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, $seconds ?? 0);
}
public function setWorkHoursSunday(int $seconds): void
/**
* @deprecated since 2.22.0
*/
public function setWorkHoursSunday(?int $seconds): void
{
$this->setPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, $seconds);
$this->setPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, $seconds ?? 0);
}
public function setPublicHolidayGroup(null|string $group = null): void
@@ -1361,6 +1404,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return (float) number_format((round($holidays * 2) / 2), 1);
}
/**
* @deprecated since 2.22.0
*/
public function hasContractSettings(): bool
{
return $this->hasWorkHourConfiguration() || $this->getHolidaysPerYear() !== 0.0;
@@ -1368,15 +1414,12 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
public function hasWorkHourConfiguration(): bool
{
return $this->getWorkHoursMonday() !== 0 ||
$this->getWorkHoursTuesday() !== 0 ||
$this->getWorkHoursWednesday() !== 0 ||
$this->getWorkHoursThursday() !== 0 ||
$this->getWorkHoursFriday() !== 0 ||
$this->getWorkHoursSaturday() !== 0 ||
$this->getWorkHoursSunday() !== 0;
return $this->getWorkContractMode() !== WorkingTimeModeNone::ID;
}
/**
* @deprecated since 2.22.0
*/
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
{
return match ($dateTime->format('N')) {
@@ -1391,6 +1434,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
};
}
/**
* @deprecated since 2.22.0
*/
public function isWorkDay(\DateTimeInterface $dateTime): bool
{
return $this->getWorkHoursForDay($dateTime) > 0;
@@ -1410,4 +1456,14 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
{
$this->supervisor = $supervisor;
}
public function getWorkContractMode(): string
{
return (string) $this->getPreferenceValue(UserPreference::WORK_CONTRACT_TYPE, WorkingTimeModeNone::ID);
}
public function setWorkContractMode(string $mode): void
{
$this->setPreferenceValue(UserPreference::WORK_CONTRACT_TYPE, $mode);
}
}

View File

@@ -10,6 +10,7 @@
namespace App\Entity;
use App\Form\Type\YesNoType;
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -32,16 +33,24 @@ class UserPreference
public const LOCALE = 'locale';
public const TIMEZONE = 'timezone';
public const FIRST_WEEKDAY = 'first_weekday';
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';
/** @deprecated since 2.22*/
public const WORK_HOURS_MONDAY = WorkingTimeCalculatorDay::WORK_HOURS_MONDAY;
/** @deprecated since 2.22*/
public const WORK_HOURS_TUESDAY = WorkingTimeCalculatorDay::WORK_HOURS_TUESDAY;
/** @deprecated since 2.22*/
public const WORK_HOURS_WEDNESDAY = WorkingTimeCalculatorDay::WORK_HOURS_WEDNESDAY;
/** @deprecated since 2.22*/
public const WORK_HOURS_THURSDAY = WorkingTimeCalculatorDay::WORK_HOURS_THURSDAY;
/** @deprecated since 2.22*/
public const WORK_HOURS_FRIDAY = WorkingTimeCalculatorDay::WORK_HOURS_FRIDAY;
/** @deprecated since 2.22*/
public const WORK_HOURS_SATURDAY = WorkingTimeCalculatorDay::WORK_HOURS_SATURDAY;
/** @deprecated since 2.22*/
public const WORK_HOURS_SUNDAY = WorkingTimeCalculatorDay::WORK_HOURS_SUNDAY;
public const WORK_STARTING_DAY = 'work_start_day';
public const PUBLIC_HOLIDAY_GROUP = 'public_holiday_group';
public const HOLIDAYS_PER_YEAR = 'holidays';
public const WORK_CONTRACT_TYPE = 'work_contract_type';
#[ORM\Id]
#[ORM\GeneratedValue]

View File

@@ -0,0 +1,52 @@
<?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\Form\Model;
use App\Entity\User;
final class UserContractModel
{
public function __construct(private readonly User $user)
{
}
public function __isset(string $name): bool
{
return true;
}
public function __set(string $name, mixed $value): void
{
$method = 'set' . ucfirst($name);
if (method_exists($this->user, $method)) {
$this->user->$method($value);
return;
}
if (!\is_scalar($value) && $value !== null) {
throw new \InvalidArgumentException('Invalid value passed');
}
$this->user->setPreferenceValue($name, $value);
}
public function __get(string $name): mixed
{
$method = 'get' . ucfirst($name);
if (method_exists($this->user, $method)) {
return $this->user->$method();
}
return $this->user->getPreferenceValue($name);
}
}

View File

@@ -9,42 +9,56 @@
namespace App\Form;
use App\Entity\User;
use App\Form\Type\DurationType;
use App\Form\Model\UserContractModel;
use App\WorkingTime\Mode\WorkingTimeMode;
use App\WorkingTime\Mode\WorkingTimeModeFactory;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
/**
* @extends AbstractType<User>
* @extends AbstractType<UserContractModel>
*/
final class UserContractType extends AbstractType
{
public function __construct(private readonly WorkingTimeModeFactory $contractModeService)
{
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['workContractModes'] = $this->contractModeService->getAll();
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$dayOptions = [
'translation_domain' => 'system-configuration',
'constraints' => [
new GreaterThanOrEqual(0)
],
];
$sorted = $this->contractModeService->getAll();
$builder
->add('workHoursMonday', DurationType::class, array_merge(['label' => 'Monday'], $dayOptions))
->add('workHoursTuesday', DurationType::class, array_merge(['label' => 'Tuesday'], $dayOptions))
->add('workHoursWednesday', DurationType::class, array_merge(['label' => 'Wednesday'], $dayOptions))
->add('workHoursThursday', DurationType::class, array_merge(['label' => 'Thursday'], $dayOptions))
->add('workHoursFriday', DurationType::class, array_merge(['label' => 'Friday'], $dayOptions))
->add('workHoursSaturday', DurationType::class, array_merge(['label' => 'Saturday'], $dayOptions))
->add('workHoursSunday', DurationType::class, array_merge(['label' => 'Sunday'], $dayOptions))
;
usort($sorted, function (WorkingTimeMode $a, WorkingTimeMode $b) {
return $a->getOrder() <=> $b->getOrder();
});
$modes = [];
foreach ($sorted as $mode) {
$modes[$mode->getName()] = $mode->getId();
}
if (\count($modes) > 1) {
$builder->add('workContractMode', ChoiceType::class, ['label' => 'work_hours_mode', 'choices' => $modes]);
}
foreach ($modes as $mode) {
$this->contractModeService->getMode($mode)->buildForm($builder, $options); // @phpstan-ignore-line
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
'data_class' => UserContractModel::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'edit_user_contract',

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)) {