added API for handling work contract times (#4016)
This commit is contained in:
93
src/Controller/ContractController.php
Normal file
93
src/Controller/ContractController.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\WorkContractDetailControllerEvent;
|
||||
use App\Form\ContractByUserForm;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
use App\Utils\PageSetup;
|
||||
use App\WorkingTime\Model\BoxConfiguration;
|
||||
use App\WorkingTime\WorkingTimeService;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Users can control their working time statistics
|
||||
*/
|
||||
final class ContractController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/contract', name: 'user_contract', methods: ['GET', 'POST'])]
|
||||
public function __invoke(Request $request, WorkingTimeService $workingTimeService, EventDispatcherInterface $eventDispatcher): Response
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$canChangeUser = $this->isGranted('contract_other_profile');
|
||||
$defaultDate = $dateTimeFactory->createStartOfYear();
|
||||
|
||||
$values = new YearByUser();
|
||||
$values->setUser($currentUser);
|
||||
$values->setDate($defaultDate);
|
||||
|
||||
$form = $this->createFormForGetRequest(ContractByUserForm::class, $values, [
|
||||
'include_user' => $canChangeUser,
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
]);
|
||||
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($values->getUser() === null) {
|
||||
$values->setUser($currentUser);
|
||||
}
|
||||
|
||||
/** @var User $profile */
|
||||
$profile = $values->getUser();
|
||||
if ($this->getUser() !== $profile && !$canChangeUser) {
|
||||
throw $this->createAccessDeniedException('Cannot access user contract settings');
|
||||
}
|
||||
|
||||
if ($values->getDate() === null) {
|
||||
$values->setDate(clone $defaultDate);
|
||||
}
|
||||
|
||||
/** @var \DateTime $yearDate */
|
||||
$yearDate = $values->getDate();
|
||||
$year = $workingTimeService->getYear($profile, $yearDate);
|
||||
|
||||
$page = new PageSetup('status');
|
||||
$page->setHelp('contract.html');
|
||||
|
||||
// additional boxes by plugins
|
||||
$controllerEvent = new WorkContractDetailControllerEvent($year);
|
||||
$eventDispatcher->dispatch($controllerEvent);
|
||||
|
||||
$now = $dateTimeFactory->createDateTime();
|
||||
$summary = $workingTimeService->getYearSummary($year, $now);
|
||||
|
||||
$boxConfiguration = new BoxConfiguration();
|
||||
$boxConfiguration->setDecimal(false);
|
||||
$boxConfiguration->setCollapsed($profile->hasWorkHourConfiguration() && $summary->count() > 0);
|
||||
|
||||
return $this->render('contract/status.html.twig', [
|
||||
'box_configuration' => $boxConfiguration,
|
||||
'page_setup' => $page,
|
||||
'decimal' => $boxConfiguration->isDecimal(),
|
||||
'summaries' => $summary,
|
||||
'now' => $now,
|
||||
'boxes' => $controllerEvent->getController(),
|
||||
'year' => $year,
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Form\Model\TotpActivation;
|
||||
use App\Form\UserApiTokenType;
|
||||
use App\Form\UserContractType;
|
||||
use App\Form\UserEditType;
|
||||
use App\Form\UserPasswordType;
|
||||
use App\Form\UserPreferencesForm;
|
||||
@@ -178,6 +179,32 @@ final class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/contract', name: 'user_profile_contract', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
#[IsGranted('contract', 'profile')]
|
||||
public function contractAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$form = $this->createForm(UserContractType::class, $profile, [
|
||||
'action' => $this->generateUrl('user_profile_contract', ['username' => $profile->getUserIdentifier()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$userRepository->saveUser($profile);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_contract', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/contract.html.twig', [
|
||||
'tab' => 'contract',
|
||||
'user' => $profile,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/teams', name: 'user_profile_teams', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('teams', 'profile')]
|
||||
public function teamsAction(User $profile, Request $request, UserRepository $userRepository, TeamRepository $teamRepository): Response
|
||||
|
||||
@@ -1133,4 +1133,114 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
{
|
||||
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||
}
|
||||
|
||||
public function getWorkHoursMonday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_MONDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursTuesday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursWednesday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursThursday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursFriday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursSaturday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, 0);
|
||||
}
|
||||
|
||||
public function getWorkHoursSunday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, 0);
|
||||
}
|
||||
|
||||
public function getHolidaysPerYear(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::HOLIDAYS_PER_YEAR, 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursMonday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_MONDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursTuesday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursWednesday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursThursday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursFriday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursSaturday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setWorkHoursSunday(int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, $seconds);
|
||||
}
|
||||
|
||||
public function setHolidaysPerYear(int $holidays): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::HOLIDAYS_PER_YEAR, $holidays);
|
||||
}
|
||||
|
||||
public function hasContractSettings(): bool
|
||||
{
|
||||
return $this->hasWorkHourConfiguration() || $this->getHolidaysPerYear() !== 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
|
||||
{
|
||||
return match ($dateTime->format('N')) {
|
||||
'1' => $this->getWorkHoursMonday(),
|
||||
'2' => $this->getWorkHoursTuesday(),
|
||||
'3' => $this->getWorkHoursWednesday(),
|
||||
'4' => $this->getWorkHoursThursday(),
|
||||
'5' => $this->getWorkHoursFriday(),
|
||||
'6' => $this->getWorkHoursSaturday(),
|
||||
'7' => $this->getWorkHoursSunday(),
|
||||
default => throw new \Exception('Unknown day: ' . $dateTime->format('Y-m-d'))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ class UserPreference
|
||||
public const LOCALE = 'language';
|
||||
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';
|
||||
public const HOLIDAYS_PER_YEAR = 'holidays';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
|
||||
113
src/Entity/WorkingTime.php
Normal file
113
src/Entity/WorkingTime.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?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\Entity;
|
||||
|
||||
use App\Repository\WorkingTimeRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Table(name: 'kimai2_working_times')]
|
||||
#[ORM\UniqueConstraint(columns: ['user_id', 'date'])]
|
||||
#[ORM\Entity(repositoryClass: WorkingTimeRepository::class)]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class WorkingTime
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'date', type: 'date', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private \DateTimeInterface $date;
|
||||
#[ORM\Column(name: 'expected', type: 'integer', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private int $expectedTime = 0;
|
||||
#[ORM\Column(name: 'actual', type: 'integer', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private int $actualTime = 0;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?User $approvedBy = null;
|
||||
#[ORM\Column(name: 'approved_at', type: 'datetime', nullable: true)]
|
||||
#[Assert\NotNull]
|
||||
private ?\DateTimeInterface $approvedAt = null;
|
||||
|
||||
public function __construct(User $user, \DateTimeInterface $date)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getDate(): \DateTimeInterface
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function getExpectedTime(): int
|
||||
{
|
||||
return $this->expectedTime;
|
||||
}
|
||||
|
||||
public function setExpectedTime(int $expectedTime): void
|
||||
{
|
||||
$this->expectedTime = $expectedTime;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
return $this->actualTime;
|
||||
}
|
||||
|
||||
public function setActualTime(int $actualTime): void
|
||||
{
|
||||
$this->actualTime = $actualTime;
|
||||
}
|
||||
|
||||
public function getApprovedBy(): ?User
|
||||
{
|
||||
return $this->approvedBy;
|
||||
}
|
||||
|
||||
public function setApprovedBy(?User $approvedBy): void
|
||||
{
|
||||
$this->approvedBy = $approvedBy;
|
||||
}
|
||||
|
||||
public function getApprovedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->approvedAt;
|
||||
}
|
||||
|
||||
public function setApprovedAt(?\DateTimeInterface $approvedAt): void
|
||||
{
|
||||
$this->approvedAt = $approvedAt;
|
||||
}
|
||||
|
||||
public function isApproved(): bool
|
||||
{
|
||||
return $this->approvedAt !== null;
|
||||
}
|
||||
}
|
||||
48
src/Event/WorkContractDetailControllerEvent.php
Normal file
48
src/Event/WorkContractDetailControllerEvent.php
Normal 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\Event;
|
||||
|
||||
use App\WorkingTime\Model\Year;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Triggered for work-contract status pages, to add additional content boxes.
|
||||
*
|
||||
* @see https://symfony.com/doc/current/templates.html#embedding-controllers
|
||||
*/
|
||||
final class WorkContractDetailControllerEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $controller = [];
|
||||
|
||||
public function __construct(private Year $year)
|
||||
{
|
||||
}
|
||||
|
||||
public function getYear(): Year
|
||||
{
|
||||
return $this->year;
|
||||
}
|
||||
|
||||
public function addController(string $controller): void
|
||||
{
|
||||
$this->controller[] = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getController(): array
|
||||
{
|
||||
return $this->controller;
|
||||
}
|
||||
}
|
||||
37
src/Event/WorkingTimeYearSummaryEvent.php
Normal file
37
src/Event/WorkingTimeYearSummaryEvent.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\WorkingTime\Model\Year;
|
||||
use App\WorkingTime\Model\YearPerUserSummary;
|
||||
use App\WorkingTime\Model\YearSummary;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class WorkingTimeYearSummaryEvent extends Event
|
||||
{
|
||||
public function __construct(private YearPerUserSummary $yearPerUserSummary, private \DateTimeInterface $until)
|
||||
{
|
||||
}
|
||||
|
||||
public function getYear(): Year
|
||||
{
|
||||
return $this->yearPerUserSummary->getYear();
|
||||
}
|
||||
|
||||
public function getUntil(): \DateTimeInterface
|
||||
{
|
||||
return $this->until;
|
||||
}
|
||||
|
||||
public function addSummary(YearSummary $yearSummary): void
|
||||
{
|
||||
$this->yearPerUserSummary->addSummary($yearSummary);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ final class UserFormsSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $user)) {
|
||||
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'edit', 'translation_domain' => 'actions']);
|
||||
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'profile-stats', 'translation_domain' => 'actions']);
|
||||
}
|
||||
if ($this->isGranted('password', $user)) {
|
||||
$event->addAction('password', ['url' => $this->path('user_profile_password', ['username' => $user->getUserIdentifier()]), 'title' => 'profile.password']);
|
||||
@@ -48,5 +48,8 @@ final class UserFormsSubscriber extends AbstractActionsSubscriber
|
||||
if ($this->isGranted('roles', $user)) {
|
||||
$event->addAction('roles', ['url' => $this->path('user_profile_roles', ['username' => $user->getUserIdentifier()]), 'title' => 'profile.roles']);
|
||||
}
|
||||
if ($this->isGranted('contract', $user)) {
|
||||
$event->addAction('contract', ['url' => $this->path('user_profile_contract', ['username' => $user->getUserIdentifier()]), 'title' => 'work_contract']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\ConfigureMainMenuEvent;
|
||||
use App\Utils\MenuItemModel;
|
||||
use KevinPapst\TablerBundle\Helper\ContextHelper;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
final class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private AuthorizationCheckerInterface $security, private ContextHelper $helper)
|
||||
public function __construct(private Security $security, private ContextHelper $helper)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -45,6 +46,8 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
// main menu
|
||||
$menu = $event->getMenu();
|
||||
/** @var User $user */
|
||||
$user = $auth->getUser();
|
||||
|
||||
$menu->addChild(new MenuItemModel('dashboard', 'dashboard.title', 'dashboard', [], 'dashboard'));
|
||||
$menu->addChild(new MenuItemModel('favorites', 'favorite_routes', null, [], 'bookmarked'));
|
||||
@@ -87,6 +90,15 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
$menu->addChild($times);
|
||||
}
|
||||
|
||||
$contract = new MenuItemModel('contract', 'work_contract', null, [], 'contract');
|
||||
if ($user->hasContractSettings() || $auth->isGranted('contract_other_profile')) {
|
||||
$contract->addChild(new MenuItemModel('contract_status', 'work_times', 'user_contract', [], 'work_times'));
|
||||
}
|
||||
|
||||
if ($contract->hasChildren()) {
|
||||
$menu->addChild($contract);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_reporting')) {
|
||||
$reporting = new MenuItemModel('reporting', 'menu.reporting', 'reporting', [], 'reporting');
|
||||
$reporting->setChildRoutes(['report_user_week', 'report_user_month', 'report_weekly_users', 'report_monthly_users', 'report_project_view']);
|
||||
|
||||
48
src/Form/ContractByUserForm.php
Normal file
48
src/Form/ContractByUserForm.php
Normal 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\Form;
|
||||
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YearPickerType;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ContractByUserForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', YearPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
]);
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class, ['width' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => YearByUser::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'include_user' => false,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
63
src/Form/UserContractType.php
Normal file
63
src/Form/UserContractType.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\DurationType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<User>
|
||||
*/
|
||||
final class UserContractType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$dayOptions = [
|
||||
'translation_domain' => 'system-configuration',
|
||||
'constraints' => [
|
||||
new GreaterThanOrEqual(0)
|
||||
],
|
||||
];
|
||||
|
||||
$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))
|
||||
/*
|
||||
->add('holidaysPerYear', IntegerType::class, [
|
||||
'label' => false,
|
||||
'constraints' => [
|
||||
new Range(['min' => 0, 'max' => 365])
|
||||
],
|
||||
])
|
||||
*/
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'edit_user_contract',
|
||||
]);
|
||||
}
|
||||
}
|
||||
24
src/Model/Day.php
Normal file
24
src/Model/Day.php
Normal 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\Model;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
class Day
|
||||
{
|
||||
public function __construct(private DateTimeInterface $day)
|
||||
{
|
||||
}
|
||||
|
||||
public function getDay(): DateTimeInterface
|
||||
{
|
||||
return $this->day;
|
||||
}
|
||||
}
|
||||
54
src/Model/Month.php
Normal file
54
src/Model/Month.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?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\Model;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
class Month
|
||||
{
|
||||
/**
|
||||
* @var Day[]
|
||||
*/
|
||||
private array $days = [];
|
||||
|
||||
public function __construct(private \DateTimeInterface $month)
|
||||
{
|
||||
$date = new \DateTimeImmutable($this->month->format('Y-m-01 00:00:00'));
|
||||
$start = $date->format('m');
|
||||
while ($start === $date->format('m')) {
|
||||
$day = $this->createDay($date);
|
||||
$this->setDay($day);
|
||||
$date = $date->add(new \DateInterval('P1D'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function createDay(\DateTimeInterface $day): Day
|
||||
{
|
||||
return new Day($day);
|
||||
}
|
||||
|
||||
public function getMonth(): DateTimeInterface
|
||||
{
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
protected function setDay(Day $day): void
|
||||
{
|
||||
$this->days['_' . $day->getDay()->format('d')] = $day;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Day[]
|
||||
*/
|
||||
public function getDays(): array
|
||||
{
|
||||
return array_values($this->days);
|
||||
}
|
||||
}
|
||||
60
src/Model/Year.php
Normal file
60
src/Model/Year.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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\Model;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
class Year
|
||||
{
|
||||
/**
|
||||
* @var Month[]
|
||||
*/
|
||||
private array $months = [];
|
||||
|
||||
public function __construct(private DateTimeInterface $month)
|
||||
{
|
||||
$monthDate = new \DateTimeImmutable();
|
||||
$monthDate = $monthDate->setDate((int) $this->month->format('Y'), 1, 1);
|
||||
$monthDate = $monthDate->setTime(1, 0);
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
$month = $this->createMonth($monthDate);
|
||||
$this->setMonth($month);
|
||||
$monthDate = $monthDate->add(new \DateInterval('P1M'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function createMonth(\DateTimeInterface $month): Month
|
||||
{
|
||||
return new Month($month);
|
||||
}
|
||||
|
||||
public function getYear(): DateTimeInterface
|
||||
{
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
protected function setMonth(Month $month): void
|
||||
{
|
||||
$this->months['_' . $month->getMonth()->format('m')] = $month;
|
||||
}
|
||||
|
||||
public function getMonth(\DateTimeInterface $month): Month
|
||||
{
|
||||
return $this->months['_' . $month->format('m')];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Month[]
|
||||
*/
|
||||
public function getMonths(): array
|
||||
{
|
||||
return array_values($this->months);
|
||||
}
|
||||
}
|
||||
89
src/Repository/WorkingTimeRepository.php
Normal file
89
src/Repository/WorkingTimeRepository.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
/**
|
||||
* @extends EntityRepository<WorkingTime>
|
||||
* @internal use WorkingTimeService instead!
|
||||
*/
|
||||
class WorkingTimeRepository extends EntityRepository
|
||||
{
|
||||
public function deleteWorkingTime(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($workingTime);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function saveWorkingTime(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($workingTime);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function scheduleWorkingTimeUpdate(WorkingTime $workingTime): void
|
||||
{
|
||||
$this->getEntityManager()->persist($workingTime);
|
||||
}
|
||||
|
||||
public function persistScheduledWorkingTimes(): void
|
||||
{
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<WorkingTime>
|
||||
*/
|
||||
public function findForYear(User $user, \DateTimeInterface $year): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('w');
|
||||
$qb->select('w')
|
||||
->where($qb->expr()->eq('w.user', ':user'))
|
||||
->setParameter('user', $user->getId())
|
||||
->andWhere($qb->expr()->eq('YEAR(w.date)', ':date'))
|
||||
->setParameter('date', $year->format('Y'))
|
||||
->indexBy('w', 'w.date')
|
||||
->orderBy('w.date')
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function getLatestApproval(User $user): ?WorkingTime
|
||||
{
|
||||
$qb = $this->createQueryBuilder('w');
|
||||
$qb->select('MAX(DATE(w.date))')
|
||||
->where($qb->expr()->eq('w.user', ':user'))
|
||||
->setParameter('user', $user->getId())
|
||||
->andWhere($qb->expr()->isNotNull('w.approvedAt'))
|
||||
;
|
||||
|
||||
$date = $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
if ($date === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$qb = $this->createQueryBuilder('w');
|
||||
$qb->select('w')
|
||||
->where($qb->expr()->eq('w.user', ':user'))
|
||||
->setParameter('user', $user->getId())
|
||||
->andWhere($qb->expr()->eq('DATE(w.date)', 'DATE(:date)'))
|
||||
->setParameter('date', $date)
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult(); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ final class UserVoter extends Voter
|
||||
'api-token',
|
||||
'hourly-rate',
|
||||
'view_team_member',
|
||||
'contract',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
@@ -60,6 +61,10 @@ final class UserVoter extends Voter
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($attribute === 'contract') {
|
||||
return $this->permissionManager->hasRolePermission($user, 'contract_other_profile');
|
||||
}
|
||||
|
||||
if ($attribute === 'view_team_member') {
|
||||
if ($subject->getId() !== $user->getId()) {
|
||||
return false;
|
||||
|
||||
36
src/WorkingTime/Model/BoxConfiguration.php
Normal file
36
src/WorkingTime/Model/BoxConfiguration.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Model;
|
||||
|
||||
final class BoxConfiguration
|
||||
{
|
||||
private bool $decimal = false;
|
||||
private bool $collapsed = false;
|
||||
|
||||
public function setDecimal(bool $decimal): void
|
||||
{
|
||||
$this->decimal = $decimal;
|
||||
}
|
||||
|
||||
public function setCollapsed(bool $collapsed): void
|
||||
{
|
||||
$this->collapsed = $collapsed;
|
||||
}
|
||||
|
||||
public function isDecimal(): bool
|
||||
{
|
||||
return $this->decimal;
|
||||
}
|
||||
|
||||
public function isCollapsed(): bool
|
||||
{
|
||||
return $this->collapsed;
|
||||
}
|
||||
}
|
||||
28
src/WorkingTime/Model/Day.php
Normal file
28
src/WorkingTime/Model/Day.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Entity\WorkingTime;
|
||||
use App\Model\Day as BaseDay;
|
||||
|
||||
final class Day extends BaseDay
|
||||
{
|
||||
private ?WorkingTime $workingTime = null;
|
||||
|
||||
public function getWorkingTime(): ?WorkingTime
|
||||
{
|
||||
return $this->workingTime;
|
||||
}
|
||||
|
||||
public function setWorkingTime(?WorkingTime $workingTime): void
|
||||
{
|
||||
$this->workingTime = $workingTime;
|
||||
}
|
||||
}
|
||||
97
src/WorkingTime/Model/Month.php
Normal file
97
src/WorkingTime/Model/Month.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\Month as BaseMonth;
|
||||
|
||||
/**
|
||||
* @method array<Day> getDays()
|
||||
*/
|
||||
final class Month extends BaseMonth
|
||||
{
|
||||
private ?bool $locked = null;
|
||||
|
||||
/**
|
||||
* A month is only locked IF every day is approved.
|
||||
* If there is even one day left open, the entire month is not locked.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocked(): bool
|
||||
{
|
||||
if ($this->locked === null) {
|
||||
$this->locked = true;
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($day->getWorkingTime() !== null && !$day->getWorkingTime()->isApproved()) {
|
||||
$this->locked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->locked;
|
||||
}
|
||||
|
||||
public function getLockDate(): ?\DateTimeInterface
|
||||
{
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($day->getWorkingTime() !== null && $day->getWorkingTime()->isApproved()) {
|
||||
return $day->getWorkingTime()->getApprovedAt();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getLockedBy(): ?User
|
||||
{
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($day->getWorkingTime() !== null && $day->getWorkingTime()->isApproved()) {
|
||||
return $day->getWorkingTime()->getApprovedBy();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function createDay(\DateTimeInterface $day): Day
|
||||
{
|
||||
return new Day($day);
|
||||
}
|
||||
|
||||
public function getExpectedTime(\DateTimeInterface $until): int
|
||||
{
|
||||
$time = 0;
|
||||
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($until < $day->getDay()) {
|
||||
break;
|
||||
}
|
||||
if ($day->getWorkingTime() !== null) {
|
||||
$time += $day->getWorkingTime()->getExpectedTime();
|
||||
}
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
$time = 0;
|
||||
|
||||
foreach ($this->getDays() as $day) {
|
||||
if ($day->getWorkingTime() !== null) {
|
||||
$time += $day->getWorkingTime()->getActualTime();
|
||||
}
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
}
|
||||
38
src/WorkingTime/Model/MonthSummary.php
Normal file
38
src/WorkingTime/Model/MonthSummary.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Model\Month as BaseMonth;
|
||||
|
||||
final class MonthSummary extends BaseMonth
|
||||
{
|
||||
private int $expectedTime = 0;
|
||||
private int $actualTime = 0;
|
||||
|
||||
public function getExpectedTime(): int
|
||||
{
|
||||
return $this->expectedTime;
|
||||
}
|
||||
|
||||
public function setExpectedTime(int $expectedTime): void
|
||||
{
|
||||
$this->expectedTime = $expectedTime;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
return $this->actualTime;
|
||||
}
|
||||
|
||||
public function setActualTime(int $actualTime): void
|
||||
{
|
||||
$this->actualTime = $actualTime;
|
||||
}
|
||||
}
|
||||
57
src/WorkingTime/Model/Year.php
Normal file
57
src/WorkingTime/Model/Year.php
Normal 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 App\WorkingTime\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\Year as BaseYear;
|
||||
|
||||
/**
|
||||
* @method array<Month> getMonths()
|
||||
* @method Month getMonth(\DateTimeInterface $month)
|
||||
*/
|
||||
final class Year extends BaseYear
|
||||
{
|
||||
public function __construct(\DateTimeInterface $month, private User $user)
|
||||
{
|
||||
parent::__construct($month);
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
protected function createMonth(\DateTimeInterface $month): Month
|
||||
{
|
||||
return new Month($month);
|
||||
}
|
||||
|
||||
public function getExpectedTime(\DateTimeInterface $until): int
|
||||
{
|
||||
$time = 0;
|
||||
|
||||
foreach ($this->getMonths() as $month) {
|
||||
$time += $month->getExpectedTime($until);
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
$time = 0;
|
||||
|
||||
foreach ($this->getMonths() as $month) {
|
||||
$time += $month->getActualTime();
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
}
|
||||
78
src/WorkingTime/Model/YearPerUserSummary.php
Normal file
78
src/WorkingTime/Model/YearPerUserSummary.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* @implements \IteratorAggregate<int, YearSummary>
|
||||
*/
|
||||
final class YearPerUserSummary implements \Countable, \IteratorAggregate
|
||||
{
|
||||
/** @var array<YearSummary> */
|
||||
private array $summaries = [];
|
||||
|
||||
public function __construct(private Year $year)
|
||||
{
|
||||
}
|
||||
|
||||
public function getYear(): Year
|
||||
{
|
||||
return $this->year;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->year->getUser();
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return \count($this->summaries);
|
||||
}
|
||||
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
return new \ArrayIterator($this->summaries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return YearSummary[]
|
||||
*/
|
||||
public function getSummaries(): array
|
||||
{
|
||||
return $this->summaries;
|
||||
}
|
||||
|
||||
public function addSummary(YearSummary $summary): void
|
||||
{
|
||||
$this->summaries[] = $summary;
|
||||
}
|
||||
|
||||
public function getExpectedTime(): int
|
||||
{
|
||||
$all = 0;
|
||||
foreach ($this->getSummaries() as $month) {
|
||||
$all += $month->getExpectedTime();
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
$all = 0;
|
||||
foreach ($this->getSummaries() as $month) {
|
||||
$all += $month->getActualTime();
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
54
src/WorkingTime/Model/YearSummary.php
Normal file
54
src/WorkingTime/Model/YearSummary.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Model\Year as BaseYear;
|
||||
|
||||
/**
|
||||
* @method array<MonthSummary> getMonths()
|
||||
* @method MonthSummary getMonth(\DateTimeInterface $month)
|
||||
*/
|
||||
final class YearSummary extends BaseYear
|
||||
{
|
||||
public function __construct(\DateTimeInterface $month, private string $title)
|
||||
{
|
||||
parent::__construct($month);
|
||||
}
|
||||
|
||||
protected function createMonth(\DateTimeInterface $month): MonthSummary
|
||||
{
|
||||
return new MonthSummary($month);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getExpectedTime(): int
|
||||
{
|
||||
$all = 0;
|
||||
foreach ($this->getMonths() as $month) {
|
||||
$all += $month->getExpectedTime();
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
public function getActualTime(): int
|
||||
{
|
||||
$all = 0;
|
||||
foreach ($this->getMonths() as $month) {
|
||||
$all += $month->getActualTime();
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
156
src/WorkingTime/WorkingTimeService.php
Normal file
156
src/WorkingTime/WorkingTimeService.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?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;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use App\Event\WorkingTimeYearSummaryEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\WorkingTimeRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use App\WorkingTime\Model\Year;
|
||||
use App\WorkingTime\Model\YearPerUserSummary;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal this API and the entire namespace is instable: you should expect changes!
|
||||
*/
|
||||
final class WorkingTimeService
|
||||
{
|
||||
public function __construct(private TimesheetRepository $timesheetRepository, private WorkingTimeRepository $workingTimeRepository, private EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
public function getYearSummary(Year $year, \DateTimeInterface $until): YearPerUserSummary
|
||||
{
|
||||
$yearPerUserSummary = new YearPerUserSummary($year);
|
||||
|
||||
$summaryEvent = new WorkingTimeYearSummaryEvent($yearPerUserSummary, $until);
|
||||
$this->eventDispatcher->dispatch($summaryEvent);
|
||||
|
||||
return $yearPerUserSummary;
|
||||
}
|
||||
|
||||
public function getLatestApproval(User $user): ?WorkingTime
|
||||
{
|
||||
return $this->workingTimeRepository->getLatestApproval($user);
|
||||
}
|
||||
|
||||
public function getYear(User $user, \DateTimeInterface $yearDate): Year
|
||||
{
|
||||
$yearTimes = $this->workingTimeRepository->findForYear($user, $yearDate);
|
||||
$existing = [];
|
||||
foreach ($yearTimes as $workingTime) {
|
||||
$existing[$workingTime->getDate()->format('Y-m-d')] = $workingTime;
|
||||
}
|
||||
|
||||
$year = new Year(\DateTimeImmutable::createFromInterface($yearDate), $user);
|
||||
|
||||
$stats = null;
|
||||
|
||||
foreach ($year->getMonths() as $month) {
|
||||
foreach ($month->getDays() as $day) {
|
||||
$key = $day->getDay()->format('Y-m-d');
|
||||
if (\array_key_exists($key, $existing)) {
|
||||
$day->setWorkingTime($existing[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($stats === null) {
|
||||
$stats = $this->getYearStatistics($yearDate, $user);
|
||||
}
|
||||
|
||||
$result = new WorkingTime($user, $day->getDay());
|
||||
$result->setExpectedTime($user->getWorkHoursForDay($day->getDay()));
|
||||
|
||||
if (\array_key_exists($key, $stats)) {
|
||||
$result->setActualTime($stats[$key]);
|
||||
}
|
||||
|
||||
$day->setWorkingTime($result);
|
||||
}
|
||||
}
|
||||
|
||||
return $year;
|
||||
}
|
||||
|
||||
public function getMonth(User $user, \DateTimeInterface $monthDate): Month
|
||||
{
|
||||
// TODO improve me, do not calculate the entire year for that
|
||||
$year = $this->getYear($user, $monthDate);
|
||||
|
||||
return $year->getMonth($monthDate);
|
||||
}
|
||||
|
||||
public function approveMonth(Month $month, \DateTimeInterface $approvalDate, User $approver): void
|
||||
{
|
||||
$update = false;
|
||||
|
||||
foreach ($month->getDays() as $day) {
|
||||
$workingTime = $day->getWorkingTime();
|
||||
if ($workingTime === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($workingTime->getId() !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($month->isLocked() || $workingTime->isApproved()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$workingTime->setApprovedBy($approver);
|
||||
$workingTime->setApprovedAt($approvalDate);
|
||||
$this->workingTimeRepository->scheduleWorkingTimeUpdate($workingTime);
|
||||
$update = true;
|
||||
}
|
||||
|
||||
if ($update) {
|
||||
$this->workingTimeRepository->persistScheduledWorkingTimes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeInterface $year
|
||||
* @param User $user
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function getYearStatistics(\DateTimeInterface $year, User $user): array
|
||||
{
|
||||
$dateTimeFactory = DateTimeFactory::createByUser($user);
|
||||
$begin = $dateTimeFactory->createStartOfYear($year);
|
||||
$end = $dateTimeFactory->createEndOfYear($year);
|
||||
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
|
||||
$qb
|
||||
->select('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('DATE(t.date) as day')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->eq('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('user', $user->getId())
|
||||
->addGroupBy('day')
|
||||
;
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$durations = [];
|
||||
foreach ($results as $row) {
|
||||
$durations[$row['day']] = (int) $row['duration'];
|
||||
}
|
||||
|
||||
return $durations; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user