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

@@ -23,13 +23,14 @@ services:
- '../src/API/Model/'
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Event/'
- '../src/Form/Model/'
- '../src/Model/'
- '../src/Repository/Loader/'
- '../src/Repository/Paginator/'
- '../src/Repository/Query/'
- '../src/Repository/Result/'
- '../src/Event/'
- '../src/Model/'
- '../src/WorkingTime/Calculator/'
- '../src/Kernel.php'
- '../src/Constants.php'

View File

@@ -0,0 +1,57 @@
<?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 DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* @version 2.22
*/
final class Version20240920105524 extends AbstractMigration
{
public function getDescription(): string
{
return 'Updates the user preferences for configurable work contract type';
}
public function up(Schema $schema): void
{
$ids = $this->connection->fetchFirstColumn("
SELECT DISTINCT user_id
FROM kimai2_user_preferences AS kp
WHERE kp.value > 0
AND kp.name IN ('work_monday', 'work_tuesday', 'work_wednesday', 'work_thursday', 'work_friday', 'work_saturday', 'work_sunday')
AND NOT EXISTS (
SELECT 1
FROM kimai2_user_preferences AS kp2
WHERE kp2.user_id = kp.user_id
AND kp2.name = 'work_contract_type'
);");
foreach ($ids as $id) {
$this->addSql('INSERT INTO kimai2_user_preferences (`user_id`, `name`, `value`) VALUES (:id, :name, :value)', [
'id' => $id,
'name' => 'work_contract_type',
'value' => 'day',
]);
}
}
public function down(Schema $schema): void
{
$this->addSql("DELETE FROM kimai2_user_preferences WHERE `name` = 'work_contract_type'");
}
public function isTransactional(): bool
{
return true;
}
}

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

View File

@@ -2,20 +2,39 @@
{% block form_content %}
<script>
function toggleWorkingContractModeForm(value)
{
document.querySelectorAll('.work_contract_mode').forEach(function (element) {
element.classList.add('d-none');
});
// Show the element with the class 'work_contract_mode_' + value
var elementToShow = document.querySelector('.work_contract_mode_' + value);
if (elementToShow) {
elementToShow.classList.remove('d-none');
}
}
</script>
{% form_theme form 'form/horizontal.html.twig' %}
{{ form_row(form._token) }}
<fieldset class="form-fieldset form-fieldset-light">
<legend>{{ 'work_times_should'|trans }}</legend>
{{ form_row(form.workHoursMonday) }}
{{ form_row(form.workHoursTuesday) }}
{{ form_row(form.workHoursWednesday) }}
{{ form_row(form.workHoursThursday) }}
{{ form_row(form.workHoursFriday) }}
{{ form_row(form.workHoursSaturday) }}
{{ form_row(form.workHoursSunday) }}
</fieldset>
{% if form.workContractMode is defined %}
<fieldset class="form-fieldset form-fieldset-light border-0 pb-0">
<legend>{{ 'work_times_should'|trans }}</legend>
{{ form_row(form.workContractMode, {attr:{'onchange': 'toggleWorkingContractModeForm(this.value)'}}) }}
</fieldset>
{% endif %}
{% for mode in form.vars.workContractModes %}
<fieldset class="form-fieldset form-fieldset-light border-0 work_contract_mode work_contract_mode_{{ mode.getId() }} {% if user.getWorkContractMode() != mode.getId() %}d-none{% endif %}">
{% for field in mode.getFormFields() %}
{{ form_row(form.children[field]) }}
{% endfor %}
</fieldset>
{% endfor %}
<fieldset class="form-fieldset form-fieldset-light">
{{- form_rest(form) -}}

View File

@@ -14,6 +14,7 @@ use App\Entity\User;
use App\Entity\UserPreference;
use App\Tests\DataFixtures\TeamFixtures;
use App\Tests\DataFixtures\TimesheetFixtures;
use App\WorkingTime\Mode\WorkingTimeModeDay;
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
use Symfony\Component\HttpKernel\HttpKernelBrowser;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
@@ -552,14 +553,15 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$user = $this->getUserByRole(User::ROLE_USER);
$calculator = (new WorkingTimeModeDay())->getCalculator($user);
$this->assertEquals(0, $user->getWorkHoursMonday());
$this->assertEquals(0, $user->getWorkHoursTuesday());
$this->assertEquals(0, $user->getWorkHoursWednesday());
$this->assertEquals(0, $user->getWorkHoursThursday());
$this->assertEquals(0, $user->getWorkHoursFriday());
$this->assertEquals(0, $user->getWorkHoursSaturday());
$this->assertEquals(0, $user->getWorkHoursSunday());
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('monday this week')));
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('tuesday this week')));
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('wednesday this week')));
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('thursday this week')));
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('friday this week')));
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('saturday this week')));
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('sunday this week')));
$form = $client->getCrawler()->filter('form[name=user_contract]')->form();
@@ -580,13 +582,14 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertHasFlashSuccess($client);
$user = $this->getUserByRole(User::ROLE_USER);
$calculator = (new WorkingTimeModeDay())->getCalculator($user);
$this->assertEquals(3600, $user->getWorkHoursMonday());
$this->assertEquals(7200, $user->getWorkHoursTuesday());
$this->assertEquals(10800, $user->getWorkHoursWednesday());
$this->assertEquals(16200, $user->getWorkHoursThursday());
$this->assertEquals(18720, $user->getWorkHoursFriday());
$this->assertEquals(25140, $user->getWorkHoursSaturday());
$this->assertEquals(60, $user->getWorkHoursSunday());
$this->assertEquals(3600, $calculator->getWorkHoursForDay(new \DateTime('monday this week')));
$this->assertEquals(7200, $calculator->getWorkHoursForDay(new \DateTime('tuesday this week')));
$this->assertEquals(10800, $calculator->getWorkHoursForDay(new \DateTime('wednesday this week')));
$this->assertEquals(16200, $calculator->getWorkHoursForDay(new \DateTime('thursday this week')));
$this->assertEquals(18720, $calculator->getWorkHoursForDay(new \DateTime('friday this week')));
$this->assertEquals(25140, $calculator->getWorkHoursForDay(new \DateTime('saturday this week')));
$this->assertEquals(60, $calculator->getWorkHoursForDay(new \DateTime('sunday this week')));
}
}

View File

@@ -17,6 +17,7 @@ use App\Entity\UserPreference;
use App\Export\Spreadsheet\ColumnDefinition;
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
use App\Tests\Security\TestUserEntity;
use App\WorkingTime\Mode\WorkingTimeModeDay;
use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\User\EquatableInterface;
@@ -74,13 +75,6 @@ class UserTest extends TestCase
$user->setAccountNumber('A-058375');
self::assertEquals('A-058375', $user->getAccountNumber());
self::assertEquals(0, $user->getWorkHoursMonday());
self::assertEquals(0, $user->getWorkHoursTuesday());
self::assertEquals(0, $user->getWorkHoursWednesday());
self::assertEquals(0, $user->getWorkHoursThursday());
self::assertEquals(0, $user->getWorkHoursFriday());
self::assertEquals(0, $user->getWorkHoursSaturday());
self::assertEquals(0, $user->getWorkHoursSunday());
self::assertEquals(0, $user->getHolidaysPerYear());
self::assertFalse($user->hasWorkHourConfiguration());
self::assertNull($user->getPublicHolidayGroup());
@@ -88,10 +82,23 @@ class UserTest extends TestCase
self::assertNull($user->getSupervisor());
}
/**
* @deprecated
* @group legacy
*/
public function testWorkContract(): void
{
$user = new User();
self::assertEquals(0, $user->getWorkHoursMonday());
self::assertEquals(0, $user->getWorkHoursTuesday());
self::assertEquals(0, $user->getWorkHoursWednesday());
self::assertEquals(0, $user->getWorkHoursThursday());
self::assertEquals(0, $user->getWorkHoursFriday());
self::assertEquals(0, $user->getWorkHoursSaturday());
self::assertEquals(0, $user->getWorkHoursSunday());
self::assertFalse($user->hasWorkHourConfiguration());
$monday = new \DateTime('2023-05-08 12:00:00', new \DateTimeZone('Europe/Berlin'));
$tuesday = new \DateTime('2023-05-09 12:00:00', new \DateTimeZone('Europe/Berlin'));
$wednesday = new \DateTime('2023-05-10 12:00:00', new \DateTimeZone('Europe/Berlin'));
@@ -108,6 +115,8 @@ class UserTest extends TestCase
self::assertFalse($user->isWorkDay($saturday));
self::assertFalse($user->isWorkDay($sunday));
$user->setWorkContractMode(WorkingTimeModeDay::ID);
$user->setWorkHoursMonday(7200);
self::assertTrue($user->hasWorkHourConfiguration());
$user->setWorkHoursTuesday(7300);

View File

@@ -0,0 +1,58 @@
<?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\Tests\WorkingTime\Calculator;
use App\Entity\User;
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\WorkingTime\Calculator\WorkingTimeCalculatorDay
*/
class WorkingTimeCalculatorDayTest extends TestCase
{
public function testDefaults(): void
{
$monday = new \DateTimeImmutable('monday last week 12:00:00');
$tuesday = $monday->modify('+1 day');
$wednesday = $tuesday->modify('+1 day');
$thursday = $wednesday->modify('+1 day');
$friday = $thursday->modify('+1 day');
$saturday = $friday->modify('+1 day');
$sunday = $saturday->modify('+1 day');
$user = new User();
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_MONDAY, 0);
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_TUESDAY, 3600);
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_WEDNESDAY, 0);
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_THURSDAY, 7200);
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_FRIDAY, 0);
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_SATURDAY, 1800);
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_SUNDAY, 9000);
$sut = new WorkingTimeCalculatorDay($user);
self::assertFalse($sut->isWorkDay($monday));
self::assertTrue($sut->isWorkDay($tuesday));
self::assertFalse($sut->isWorkDay($wednesday));
self::assertTrue($sut->isWorkDay($thursday));
self::assertFalse($sut->isWorkDay($friday));
self::assertTrue($sut->isWorkDay($saturday));
self::assertTrue($sut->isWorkDay($sunday));
self::assertEquals(0, $sut->getWorkHoursForDay($monday));
self::assertEquals(3600, $sut->getWorkHoursForDay($tuesday));
self::assertEquals(0, $sut->getWorkHoursForDay($wednesday));
self::assertEquals(7200, $sut->getWorkHoursForDay($thursday));
self::assertEquals(0, $sut->getWorkHoursForDay($friday));
self::assertEquals(1800, $sut->getWorkHoursForDay($saturday));
self::assertEquals(9000, $sut->getWorkHoursForDay($sunday));
}
}

View File

@@ -0,0 +1,27 @@
<?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\Tests\WorkingTime\Calculator;
use App\WorkingTime\Calculator\WorkingTimeCalculatorNone;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\WorkingTime\Calculator\WorkingTimeCalculatorNone
*/
class WorkingTimeCalculatorNoneTest extends TestCase
{
public function testDefaults(): void
{
$date = new \DateTime();
$sut = new WorkingTimeCalculatorNone();
self::assertTrue($sut->isWorkDay($date));
self::assertEquals(0, $sut->getWorkHoursForDay($date));
}
}

View File

@@ -0,0 +1,34 @@
<?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\Tests\WorkingTime\Mode;
use App\Entity\User;
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
use App\WorkingTime\Mode\WorkingTimeModeDay;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\WorkingTime\Mode\WorkingTimeModeDay
*/
class WorkingTimeModeDayTest extends TestCase
{
public function testDefaults(): void
{
$user = new User();
$user->setWorkContractMode('day');
$sut = new WorkingTimeModeDay();
$this->assertEquals('day', $sut->getId());
$this->assertEquals(10, $sut->getOrder());
$this->assertEquals('hours_per_day', $sut->getName());
$this->assertInstanceOf(WorkingTimeCalculatorDay::class, $sut->getCalculator($user));
$fields = $sut->getFormFields();
$this->assertCount(7, $fields);
}
}

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\Tests\WorkingTime\Mode;
use App\Entity\User;
use App\WorkingTime\Mode\WorkingTimeModeDay;
use App\WorkingTime\Mode\WorkingTimeModeFactory;
use App\WorkingTime\Mode\WorkingTimeModeNone;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\WorkingTime\Mode\WorkingTimeModeFactory
*/
class WorkingTimeModeFactoryTest extends TestCase
{
public function testDefaults(): void
{
$none = new WorkingTimeModeNone();
$day = new WorkingTimeModeDay();
$modes = [$none, $day];
$sut = new WorkingTimeModeFactory($modes);
$this->assertEquals($modes, $sut->getAll());
$this->assertSame($none, $sut->getMode('none'));
$this->assertSame($day, $sut->getMode('day'));
$user = new User();
$user->setWorkContractMode('day');
$this->assertSame($day, $sut->getModeForUser($user));
}
public function testException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Unknown working contract mode: foo');
$sut = new WorkingTimeModeFactory([]);
$sut->getMode('foo');
}
}

View File

@@ -0,0 +1,30 @@
<?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\Tests\WorkingTime\Mode;
use App\Entity\User;
use App\WorkingTime\Calculator\WorkingTimeCalculatorNone;
use App\WorkingTime\Mode\WorkingTimeModeNone;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\WorkingTime\Mode\WorkingTimeModeNone
*/
class WorkingTimeModeNoneTest extends TestCase
{
public function testDefaults(): void
{
$sut = new WorkingTimeModeNone();
$this->assertEquals('none', $sut->getId());
$this->assertEquals(0, $sut->getOrder());
$this->assertEquals('', $sut->getName());
$this->assertInstanceOf(WorkingTimeCalculatorNone::class, $sut->getCalculator(new User()));
}
}