added API for handling work contract times (#4016)
This commit is contained in:
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