Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)
This commit is contained in:
@@ -15,9 +15,9 @@ use App\Timesheet\CalculatorInterface;
|
||||
/**
|
||||
* Implementation to calculate the billable field for a timesheet record.
|
||||
*/
|
||||
class BillableCalculator implements CalculatorInterface
|
||||
final class BillableCalculator implements CalculatorInterface
|
||||
{
|
||||
public function calculate(Timesheet $record)
|
||||
public function calculate(Timesheet $record, array $changeset): void
|
||||
{
|
||||
switch ($record->getBillableMode()) {
|
||||
case Timesheet::BILLABLE_NO:
|
||||
|
||||
@@ -18,20 +18,11 @@ use App\Timesheet\RoundingService;
|
||||
*/
|
||||
final class DurationCalculator implements CalculatorInterface
|
||||
{
|
||||
/**
|
||||
* @var RoundingService
|
||||
*/
|
||||
private $roundings;
|
||||
|
||||
public function __construct(RoundingService $roundings)
|
||||
public function __construct(private RoundingService $roundings)
|
||||
{
|
||||
$this->roundings = $roundings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
*/
|
||||
public function calculate(Timesheet $record)
|
||||
public function calculate(Timesheet $record, array $changeset): void
|
||||
{
|
||||
if (null === $record->getEnd()) {
|
||||
return;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Timesheet\Calculator;
|
||||
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
use App\Timesheet\RateService;
|
||||
@@ -17,19 +16,13 @@ use App\Timesheet\RateService;
|
||||
/**
|
||||
* Implementation to calculate the rate for a timesheet record.
|
||||
*/
|
||||
class RateCalculator implements CalculatorInterface
|
||||
final class RateCalculator implements CalculatorInterface
|
||||
{
|
||||
/**
|
||||
* @var RateService
|
||||
*/
|
||||
private $service;
|
||||
|
||||
public function __construct(RateService $service)
|
||||
public function __construct(private RateService $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function calculate(Timesheet $record)
|
||||
public function calculate(Timesheet $record, array $changeset): void
|
||||
{
|
||||
$rate = $this->service->calculate($record);
|
||||
|
||||
|
||||
52
src/Timesheet/Calculator/RateResetCalculator.php
Normal file
52
src/Timesheet/Calculator/RateResetCalculator.php
Normal 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\Timesheet\Calculator;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
|
||||
final class RateResetCalculator implements CalculatorInterface
|
||||
{
|
||||
public function calculate(Timesheet $record, array $changeset): void
|
||||
{
|
||||
// check if the rate was changed manually
|
||||
$changedRate = false;
|
||||
foreach (['hourlyRate', 'fixedRate', 'internalRate', 'rate'] as $field) {
|
||||
if (\array_key_exists($field, $changeset)) {
|
||||
$changedRate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if no manual rate changed was applied:
|
||||
// check if a field changed, that is relevant for the rate calculation: if one was changed =>
|
||||
// reset all rates, because most users do not even see their rates and would not be able
|
||||
// to fix or empty the rate, even if they knew that the changed project has another base rate
|
||||
if (!$changedRate) {
|
||||
foreach (['project', 'activity', 'user'] as $field) {
|
||||
if (\array_key_exists($field, $changeset)) {
|
||||
// this has room for minor improvements: entries with a manual rate might be changed
|
||||
$record->setRate(0.00);
|
||||
$record->setInternalRate(null);
|
||||
$record->setHourlyRate(null);
|
||||
$record->setFixedRate(null);
|
||||
$record->setBillableMode(Timesheet::BILLABLE_AUTOMATIC);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
// needs to run before all other
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
@@ -19,21 +19,18 @@ interface CalculatorInterface
|
||||
{
|
||||
/**
|
||||
* All necessary changes need to be applied on the given $record.
|
||||
* The methods return value will not be evaluated.
|
||||
*
|
||||
* @param Timesheet $record
|
||||
* @ param array<string, array<mixed, mixed>> $changeset
|
||||
* @param array<string, array<mixed, mixed>> $changeset
|
||||
* @return void
|
||||
*/
|
||||
public function calculate(Timesheet $record/*, array $changeset*/);
|
||||
public function calculate(Timesheet $record, array $changeset): void;
|
||||
|
||||
/*
|
||||
* FIXME use with Kimai 2.0
|
||||
*
|
||||
* Default priority is 1000 (after all system Calculator were executed).
|
||||
* The higher the priority the later it will be executed.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
//public function getPriority(): int;
|
||||
public function getPriority(): int;
|
||||
}
|
||||
|
||||
@@ -11,18 +11,13 @@ namespace App\Timesheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
|
||||
class DateTimeFactory
|
||||
final class DateTimeFactory
|
||||
{
|
||||
/**
|
||||
* @var DateTimeZone
|
||||
*/
|
||||
private $timezone;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $startOnSunday;
|
||||
private DateTimeZone $timezone;
|
||||
private bool $startOnSunday = false;
|
||||
|
||||
public static function createByUser(User $user): self
|
||||
{
|
||||
@@ -34,13 +29,8 @@ class DateTimeFactory
|
||||
if (null === $timezone) {
|
||||
$timezone = new \DateTimeZone(date_default_timezone_get());
|
||||
}
|
||||
$this->setTimezone($timezone);
|
||||
$this->startOnSunday = $startOnSunday;
|
||||
}
|
||||
|
||||
protected function setTimezone(DateTimeZone $timezone)
|
||||
{
|
||||
$this->timezone = $timezone;
|
||||
$this->startOnSunday = $startOnSunday;
|
||||
}
|
||||
|
||||
public function getTimezone(): DateTimeZone
|
||||
@@ -48,13 +38,9 @@ class DateTimeFactory
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
public function getStartOfMonth(?DateTime $date = null): DateTime
|
||||
public function getStartOfMonth(DateTimeInterface|string|null $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime();
|
||||
} else {
|
||||
$date = clone $date;
|
||||
}
|
||||
$date = $this->getDate($date);
|
||||
|
||||
$date->modify('first day of this month');
|
||||
$date->setTime(0, 0, 0);
|
||||
@@ -62,14 +48,22 @@ class DateTimeFactory
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function getStartOfWeek(?DateTime $date = null): DateTime
|
||||
private function getDate(DateTimeInterface|string|null $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime('now');
|
||||
} else {
|
||||
$date = clone $date;
|
||||
if ($date === null) {
|
||||
$date = 'now';
|
||||
}
|
||||
|
||||
if (\is_string($date)) {
|
||||
return $this->createDateTime($date);
|
||||
}
|
||||
|
||||
return DateTime::createFromInterface($date);
|
||||
}
|
||||
|
||||
public function getStartOfWeek(DateTimeInterface|string|null $date = null): DateTime
|
||||
{
|
||||
$date = $this->getDate($date);
|
||||
$firstDay = 1;
|
||||
|
||||
if ($this->startOnSunday) {
|
||||
@@ -84,14 +78,9 @@ class DateTimeFactory
|
||||
return $this->createWeekDateTime($date->format('o'), $date->format('W'), $firstDay, 0, 0, 0);
|
||||
}
|
||||
|
||||
public function getEndOfWeek(?DateTime $date = null): DateTime
|
||||
public function getEndOfWeek(DateTimeInterface|string|null $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime();
|
||||
} else {
|
||||
$date = clone $date;
|
||||
}
|
||||
|
||||
$date = $this->getDate($date);
|
||||
$lastDay = 7;
|
||||
|
||||
if ($this->startOnSunday) {
|
||||
@@ -106,13 +95,10 @@ class DateTimeFactory
|
||||
return $this->createWeekDateTime($date->format('o'), $date->format('W'), $lastDay, 23, 59, 59);
|
||||
}
|
||||
|
||||
public function getEndOfMonth(?DateTime $date = null): DateTime
|
||||
public function getEndOfMonth(DateTimeInterface|string|null $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime();
|
||||
}
|
||||
$date = $this->getDate($date);
|
||||
|
||||
$date = clone $date;
|
||||
$date = $date->modify('last day of this month');
|
||||
$date->setTime(23, 59, 59);
|
||||
|
||||
@@ -138,31 +124,23 @@ class DateTimeFactory
|
||||
* @param null|string $datetime
|
||||
* @return bool|DateTime
|
||||
*/
|
||||
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now')
|
||||
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now'): bool|DateTime
|
||||
{
|
||||
return DateTime::createFromFormat($format, $datetime, $this->getTimezone());
|
||||
return DateTime::createFromFormat($format, $datetime ?? 'now', $this->getTimezone());
|
||||
}
|
||||
|
||||
public function createStartOfYear(?DateTime $date = null): DateTime
|
||||
public function createStartOfYear(DateTimeInterface|string|null $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime();
|
||||
} else {
|
||||
$date = clone $date;
|
||||
}
|
||||
$date = $this->getDate($date);
|
||||
|
||||
$date->modify('first day of january 00:00:00');
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function createEndOfYear(?DateTime $date = null): DateTime
|
||||
public function createEndOfYear(DateTimeInterface|string|null $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime();
|
||||
} else {
|
||||
$date = clone $date;
|
||||
}
|
||||
$date = $this->getDate($date);
|
||||
|
||||
$date->modify('last day of december 23:59:59');
|
||||
|
||||
@@ -189,9 +167,9 @@ class DateTimeFactory
|
||||
return $financialYear;
|
||||
}
|
||||
|
||||
public function createEndOfFinancialYear(DateTime $financialYear): DateTime
|
||||
public function createEndOfFinancialYear(DateTimeInterface $financialYear): DateTime
|
||||
{
|
||||
$yearEnd = clone $financialYear;
|
||||
$yearEnd = DateTime::createFromInterface($financialYear);
|
||||
$yearEnd->modify('+1 year')->modify('-1 day')->setTime(23, 59, 59);
|
||||
|
||||
return $yearEnd;
|
||||
|
||||
114
src/Timesheet/FavoriteRecordService.php
Normal file
114
src/Timesheet/FavoriteRecordService.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?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\Timesheet;
|
||||
|
||||
use App\Entity\Bookmark;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\FavoriteTimesheet;
|
||||
use App\Repository\BookmarkRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class FavoriteRecordService
|
||||
{
|
||||
public function __construct(private TimesheetRepository $repository, private BookmarkRepository $bookmarkRepository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param int $limit
|
||||
* @return array<FavoriteTimesheet>
|
||||
*/
|
||||
public function favoriteEntries(User $user, int $limit = 5): array
|
||||
{
|
||||
$favIds = $this->getBookmark($user)->getContent();
|
||||
$recentIds = [];
|
||||
if (\count($favIds) < 5) {
|
||||
$recentIds = $this->repository->getRecentActivityIds($user, null, $limit);
|
||||
}
|
||||
$ids = \array_slice(array_unique(array_merge($favIds, $recentIds)), 0, $limit);
|
||||
|
||||
$favorites = [];
|
||||
foreach ($ids as $id) {
|
||||
$favorites[$id] = \in_array($id, $favIds);
|
||||
}
|
||||
|
||||
if (\count($ids) > 0) {
|
||||
$timesheets = $this->repository->findTimesheetsById($ids, false, false);
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$favorites[$timesheet->getId()] = new FavoriteTimesheet($timesheet, $favorites[$timesheet->getId()]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($favorites);
|
||||
}
|
||||
|
||||
private function getBookmark(User $user): Bookmark
|
||||
{
|
||||
$bookmark = $this->bookmarkRepository->findBookmark($user, 'favorite', 'recent');
|
||||
|
||||
if ($bookmark === null) {
|
||||
$bookmark = new Bookmark();
|
||||
$bookmark->setUser($user);
|
||||
$bookmark->setType('favorite');
|
||||
$bookmark->setName('timesheet');
|
||||
}
|
||||
|
||||
return $bookmark;
|
||||
}
|
||||
|
||||
public function addFavorite(Timesheet $timesheet): void
|
||||
{
|
||||
if ($timesheet->getUser() === null) {
|
||||
throw new \InvalidArgumentException('Cannot favorite timesheet without user');
|
||||
}
|
||||
|
||||
$bookmark = $this->getBookmark($timesheet->getUser());
|
||||
$ids = $bookmark->getContent();
|
||||
if (\in_array($timesheet->getId(), $ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (\count($ids) >= 5) {
|
||||
array_pop($ids); // remove the last element and make space for a new id
|
||||
}
|
||||
array_unshift($ids, $timesheet->getId());
|
||||
$bookmark->setContent($ids);
|
||||
|
||||
$this->bookmarkRepository->saveBookmark($bookmark);
|
||||
}
|
||||
|
||||
public function removeFavorite(Timesheet $timesheet): void
|
||||
{
|
||||
if ($timesheet->getUser() === null) {
|
||||
throw new \InvalidArgumentException('Cannot favorite timesheet without user');
|
||||
}
|
||||
|
||||
$bookmark = $this->getBookmark($timesheet->getUser());
|
||||
$ids = $bookmark->getContent();
|
||||
|
||||
if (!\in_array($timesheet->getId(), $ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newIds = [];
|
||||
foreach ($ids as $id) {
|
||||
if ($id !== $timesheet->getId()) {
|
||||
$newIds[] = $id;
|
||||
}
|
||||
}
|
||||
$bookmark->setContent($newIds);
|
||||
$this->bookmarkRepository->saveBookmark($bookmark);
|
||||
}
|
||||
}
|
||||
@@ -11,36 +11,171 @@ namespace App\Timesheet;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
|
||||
final class LockdownService
|
||||
{
|
||||
private $configuration;
|
||||
private $isActive;
|
||||
private ?bool $isActive = null;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
public function __construct(private SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function isLockdownActive(): bool
|
||||
{
|
||||
if ($this->isActive === null) {
|
||||
$this->isActive = $this->configuration->isTimesheetLockdownActive();
|
||||
$this->isActive = $this->getLockdownPeriodStart() !== null && $this->getLockdownPeriodEnd() !== null;
|
||||
}
|
||||
|
||||
return $this->isActive;
|
||||
}
|
||||
|
||||
public function getLockdownTimezone(): ?string
|
||||
{
|
||||
$timezone = $this->configuration->find('timesheet.rules.lockdown_period_timezone');
|
||||
|
||||
if ($timezone === null || $timezone === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $timezone;
|
||||
}
|
||||
|
||||
private function getTimezone(User $user): \DateTimeZone
|
||||
{
|
||||
$timezone = $this->getLockdownTimezone();
|
||||
|
||||
if ($timezone === null) {
|
||||
$timezone = $user->getTimezone();
|
||||
}
|
||||
|
||||
return new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
public function getLockdownStart(User $user): ?\DateTimeInterface
|
||||
{
|
||||
$start = $this->getLockdownPeriodStart();
|
||||
if ($start === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$start = new \DateTimeImmutable($start, $this->getTimezone($user));
|
||||
|
||||
return $start->setTimezone(new \DateTimeZone($user->getTimezone()));
|
||||
}
|
||||
|
||||
private function getLockdownPeriodStart(): ?string
|
||||
{
|
||||
$start = $this->configuration->find('timesheet.rules.lockdown_period_start');
|
||||
|
||||
if (!\is_string($start) || trim($start) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$start = explode(',', $start);
|
||||
if (\count($start) === 1) {
|
||||
return $start[0];
|
||||
}
|
||||
|
||||
$min = null;
|
||||
$date = null;
|
||||
foreach ($start as $dateString) {
|
||||
$tmp = new \DateTimeImmutable($dateString);
|
||||
if ($min === null) {
|
||||
$min = $dateString;
|
||||
$date = $tmp;
|
||||
continue;
|
||||
}
|
||||
if ($tmp > $date) {
|
||||
$min = $dateString;
|
||||
$date = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return $min;
|
||||
}
|
||||
|
||||
public function getLockdownEnd(User $user): ?\DateTimeInterface
|
||||
{
|
||||
$end = $this->getLockdownPeriodEnd();
|
||||
if ($end === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$end = new \DateTimeImmutable($end, $this->getTimezone($user));
|
||||
|
||||
return $end->setTimezone(new \DateTimeZone($user->getTimezone()));
|
||||
}
|
||||
|
||||
private function getLockdownPeriodEnd(): ?string
|
||||
{
|
||||
$end = $this->configuration->find('timesheet.rules.lockdown_period_end');
|
||||
|
||||
if (!\is_string($end) || trim($end) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$end = explode(',', $end);
|
||||
if (\count($end) === 1) {
|
||||
return $end[0];
|
||||
}
|
||||
|
||||
$min = null;
|
||||
$date = null;
|
||||
foreach ($end as $dateString) {
|
||||
$tmp = new \DateTimeImmutable($dateString);
|
||||
if ($min === null) {
|
||||
$min = $dateString;
|
||||
$date = $tmp;
|
||||
continue;
|
||||
}
|
||||
if ($tmp > $date) {
|
||||
$min = $dateString;
|
||||
$date = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return $min;
|
||||
}
|
||||
|
||||
public function getLockdownGrace(User $user): ?\DateTimeInterface
|
||||
{
|
||||
$gracePeriod = $this->getLockdownGracePeriod();
|
||||
if ($gracePeriod === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$end = $this->getLockdownEnd($user);
|
||||
if ($end === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$grace = \DateTimeImmutable::createFromInterface($end);
|
||||
|
||||
return $grace->modify($gracePeriod);
|
||||
}
|
||||
|
||||
private function getLockdownGracePeriod(): ?string
|
||||
{
|
||||
$grace = $this->configuration->find('timesheet.rules.lockdown_grace_period');
|
||||
|
||||
if ($grace === null || $grace === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $grace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not check if the current user is allowed to edit timesheets in lockdown situations.
|
||||
* This needs to be performed earlier by yourself (see TimesheetVoter or LockdownValidator).
|
||||
*
|
||||
* @param Timesheet $timesheet
|
||||
* @param \DateTime $now
|
||||
* @param \DateTimeInterface $now
|
||||
* @param bool $allowEditInGracePeriod
|
||||
* @return bool
|
||||
*/
|
||||
public function isEditable(Timesheet $timesheet, \DateTime $now, bool $allowEditInGracePeriod = false)
|
||||
public function isEditable(Timesheet $timesheet, \DateTimeInterface $now, bool $allowEditInGracePeriod = false): bool
|
||||
{
|
||||
if (!$this->isLockdownActive()) {
|
||||
return true;
|
||||
@@ -52,10 +187,15 @@ final class LockdownService
|
||||
return true;
|
||||
}
|
||||
|
||||
$lockedStart = $this->configuration->getTimesheetLockdownPeriodStart();
|
||||
$lockedEnd = $this->configuration->getTimesheetLockdownPeriodEnd();
|
||||
$gracePeriod = $this->configuration->getTimesheetLockdownGracePeriod();
|
||||
$timezone = $this->configuration->getTimesheetLockdownTimeZone();
|
||||
$lockedStart = $this->getLockdownPeriodStart();
|
||||
$lockedEnd = $this->getLockdownPeriodEnd();
|
||||
|
||||
if ($lockedStart === null || $lockedEnd === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$gracePeriod = $this->getLockdownGracePeriod();
|
||||
$timezone = $this->getLockdownTimezone();
|
||||
|
||||
if ($timezone === null) {
|
||||
$timezone = $timesheetStart->getTimezone();
|
||||
@@ -64,11 +204,11 @@ final class LockdownService
|
||||
}
|
||||
|
||||
try {
|
||||
$lockdownStart = new \DateTime($lockedStart, $timezone);
|
||||
$lockdownEnd = new \DateTime($lockedEnd, $timezone);
|
||||
$lockdownStart = new \DateTimeImmutable($lockedStart, $timezone);
|
||||
$lockdownEnd = new \DateTimeImmutable($lockedEnd, $timezone);
|
||||
$lockdownGrace = clone $lockdownEnd;
|
||||
if (!empty($gracePeriod)) {
|
||||
$lockdownGrace->modify($gracePeriod);
|
||||
$lockdownGrace = $lockdownGrace->modify($gracePeriod);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
// should not happen, but ... if parsing of datetimes fails: skip validation
|
||||
@@ -86,7 +226,7 @@ final class LockdownService
|
||||
}
|
||||
|
||||
// further validate entries inside of the most recent lockdown
|
||||
if ($timesheetStart >= $lockdownStart && $timesheetStart <= $lockdownEnd) {
|
||||
if ($timesheetStart >= $lockdownStart) {
|
||||
// if grace period is still in effect, validation succeeds
|
||||
if ($now <= $lockdownGrace) {
|
||||
return true;
|
||||
|
||||
@@ -11,22 +11,10 @@ namespace App\Timesheet;
|
||||
|
||||
final class Rate
|
||||
{
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $internalRate = 0.00;
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
/**
|
||||
* @var float|null
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
private float $rate;
|
||||
private float $internalRate;
|
||||
private ?float $hourlyRate;
|
||||
private ?float $fixedRate;
|
||||
|
||||
public function __construct(float $rate, float $internalRate, ?float $hourlyRate = null, ?float $fixedRate = null)
|
||||
{
|
||||
|
||||
@@ -19,19 +19,8 @@ use App\Repository\TimesheetRepository;
|
||||
*/
|
||||
final class RateService implements RateServiceInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $rates;
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(array $rates, TimesheetRepository $repository)
|
||||
public function __construct(private array $rates, private TimesheetRepository $repository)
|
||||
{
|
||||
$this->rates = $rates;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function calculate(Timesheet $record): Rate
|
||||
@@ -49,13 +38,13 @@ final class RateService implements RateServiceInterface
|
||||
|
||||
if (null !== $rate) {
|
||||
if ($rate->isFixed()) {
|
||||
$fixedRate = $fixedRate ?? $rate->getRate();
|
||||
$fixedRate ??= $rate->getRate();
|
||||
$fixedInternalRate = $rate->getRate();
|
||||
if (null !== $rate->getInternalRate()) {
|
||||
$fixedInternalRate = $rate->getInternalRate();
|
||||
}
|
||||
} else {
|
||||
$hourlyRate = $hourlyRate ?? $rate->getRate();
|
||||
$hourlyRate ??= $rate->getRate();
|
||||
$internalRate = $rate->getRate();
|
||||
if (null !== $rate->getInternalRate()) {
|
||||
$internalRate = $rate->getInternalRate();
|
||||
@@ -86,8 +75,8 @@ final class RateService implements RateServiceInterface
|
||||
$factor = $this->getRateFactor($record);
|
||||
}
|
||||
|
||||
$factoredHourlyRate = (float) ($hourlyRate * $factor);
|
||||
$factoredInternalRate = (float) ($internalRate * $factor);
|
||||
$factoredHourlyRate = $hourlyRate * $factor;
|
||||
$factoredInternalRate = $internalRate * $factor;
|
||||
$totalRate = 0;
|
||||
$totalInternalRate = 0;
|
||||
|
||||
@@ -130,7 +119,7 @@ final class RateService implements RateServiceInterface
|
||||
$weekday = $record->getEnd()->format('l');
|
||||
$days = array_map('strtolower', $rateFactor['days']);
|
||||
if (\in_array(strtolower($weekday), $days)) {
|
||||
$factor += $rateFactor['factor'];
|
||||
$factor += (float) $rateFactor['factor'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +127,6 @@ final class RateService implements RateServiceInterface
|
||||
$factor = 1.00;
|
||||
}
|
||||
|
||||
return (float) $factor;
|
||||
return $factor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,7 @@ final class CeilRounding implements RoundingInterface
|
||||
return 'ceil';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundBegin(Timesheet $record, $minutes)
|
||||
public function roundBegin(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
@@ -41,11 +37,7 @@ final class CeilRounding implements RoundingInterface
|
||||
$record->setBegin($newBegin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundEnd(Timesheet $record, $minutes)
|
||||
public function roundEnd(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
@@ -64,17 +56,13 @@ final class CeilRounding implements RoundingInterface
|
||||
$record->setEnd($newEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundDuration(Timesheet $record, $minutes)
|
||||
public function roundDuration(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = $record->getDuration();
|
||||
$timestamp = $record->getDuration() ?? 0;
|
||||
$seconds = $minutes * 60;
|
||||
$diff = $timestamp % $seconds;
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@ final class ClosestRounding implements RoundingInterface
|
||||
return 'closest';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundBegin(Timesheet $record, $minutes)
|
||||
public function roundBegin(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
@@ -45,11 +41,7 @@ final class ClosestRounding implements RoundingInterface
|
||||
$record->setBegin($newBegin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundEnd(Timesheet $record, $minutes)
|
||||
public function roundEnd(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
@@ -72,17 +64,13 @@ final class ClosestRounding implements RoundingInterface
|
||||
$record->setEnd($newEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundDuration(Timesheet $record, $minutes)
|
||||
public function roundDuration(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = $record->getDuration();
|
||||
$timestamp = $record->getDuration() ?? 0;
|
||||
$seconds = $minutes * 60;
|
||||
$diff = $timestamp % $seconds;
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@ final class DefaultRounding implements RoundingInterface
|
||||
return 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundBegin(Timesheet $record, $minutes)
|
||||
public function roundBegin(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
@@ -41,11 +37,7 @@ final class DefaultRounding implements RoundingInterface
|
||||
$record->setBegin($newBegin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundEnd(Timesheet $record, $minutes)
|
||||
public function roundEnd(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
@@ -64,17 +56,13 @@ final class DefaultRounding implements RoundingInterface
|
||||
$record->setEnd($newEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundDuration(Timesheet $record, $minutes)
|
||||
public function roundDuration(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = $record->getDuration();
|
||||
$timestamp = $record->getDuration() ?? 0;
|
||||
$seconds = $minutes * 60;
|
||||
$diff = $timestamp % $seconds;
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@ final class FloorRounding implements RoundingInterface
|
||||
return 'floor';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundBegin(Timesheet $record, $minutes)
|
||||
public function roundBegin(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
@@ -41,11 +37,7 @@ final class FloorRounding implements RoundingInterface
|
||||
$record->setBegin($newBegin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundEnd(Timesheet $record, $minutes)
|
||||
public function roundEnd(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
@@ -64,17 +56,13 @@ final class FloorRounding implements RoundingInterface
|
||||
$record->setEnd($newEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundDuration(Timesheet $record, $minutes)
|
||||
public function roundDuration(Timesheet $record, int $minutes): void
|
||||
{
|
||||
if ($minutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = $record->getDuration();
|
||||
$timestamp = $record->getDuration() ?? 0;
|
||||
$seconds = $minutes * 60;
|
||||
$diff = $timestamp % $seconds;
|
||||
|
||||
|
||||
@@ -16,26 +16,11 @@ use App\Entity\Timesheet;
|
||||
*/
|
||||
interface RoundingInterface
|
||||
{
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundBegin(Timesheet $record, $minutes);
|
||||
public function roundBegin(Timesheet $record, int $minutes): void;
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundEnd(Timesheet $record, $minutes);
|
||||
public function roundEnd(Timesheet $record, int $minutes): void;
|
||||
|
||||
/**
|
||||
* @param Timesheet $record
|
||||
* @param int $minutes
|
||||
*/
|
||||
public function roundDuration(Timesheet $record, $minutes);
|
||||
public function roundDuration(Timesheet $record, int $minutes): void;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string;
|
||||
}
|
||||
|
||||
@@ -15,33 +15,18 @@ use App\Timesheet\Rounding\RoundingInterface;
|
||||
|
||||
final class RoundingService
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $rules;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $rulesCache;
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var RoundingInterface[]
|
||||
*/
|
||||
private $roundingModes;
|
||||
|
||||
/**
|
||||
* @param SystemConfiguration $configuration
|
||||
* @param RoundingInterface[] $roundingModes
|
||||
* @param array $rules
|
||||
*/
|
||||
public function __construct(SystemConfiguration $configuration, iterable $roundingModes, array $rules)
|
||||
public function __construct(private SystemConfiguration $configuration, private iterable $roundingModes, private array $rules)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->roundingModes = $roundingModes;
|
||||
$this->rules = $rules;
|
||||
}
|
||||
|
||||
private function getRoundingRules(): array
|
||||
@@ -71,6 +56,9 @@ final class RoundingService
|
||||
public function roundBegin(Timesheet $record): void
|
||||
{
|
||||
foreach ($this->getRoundingRules() as $rounding) {
|
||||
if ($record->getBegin() === null) {
|
||||
continue;
|
||||
}
|
||||
$weekday = $record->getBegin()->format('l');
|
||||
|
||||
if (\in_array(strtolower($weekday), $rounding['days'])) {
|
||||
@@ -83,6 +71,9 @@ final class RoundingService
|
||||
public function roundEnd(Timesheet $record): void
|
||||
{
|
||||
foreach ($this->getRoundingRules() as $rounding) {
|
||||
if ($record->getEnd() === null) {
|
||||
continue;
|
||||
}
|
||||
$weekday = $record->getEnd()->format('l');
|
||||
|
||||
if (\in_array(strtolower($weekday), $rounding['days'])) {
|
||||
@@ -95,6 +86,9 @@ final class RoundingService
|
||||
public function roundDuration(Timesheet $record): void
|
||||
{
|
||||
foreach ($this->getRoundingRules() as $rounding) {
|
||||
if ($record->getEnd() === null) {
|
||||
continue;
|
||||
}
|
||||
$weekday = $record->getEnd()->format('l');
|
||||
|
||||
if (\in_array(strtolower($weekday), $rounding['days'])) {
|
||||
@@ -111,6 +105,9 @@ final class RoundingService
|
||||
}
|
||||
|
||||
foreach ($this->getRoundingRules() as $rounding) {
|
||||
if ($record->getEnd() === null) {
|
||||
continue;
|
||||
}
|
||||
$weekday = $record->getEnd()->format('l');
|
||||
|
||||
if (\in_array(strtolower($weekday), $rounding['days'])) {
|
||||
@@ -118,10 +115,12 @@ final class RoundingService
|
||||
$rounder->roundBegin($record, $rounding['begin']);
|
||||
$rounder->roundEnd($record, $rounding['end']);
|
||||
|
||||
$duration = $record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp();
|
||||
$record->setDuration($duration);
|
||||
if ($record->getBegin() !== null) {
|
||||
$duration = $record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp();
|
||||
$record->setDuration($duration);
|
||||
|
||||
$rounder->roundDuration($record, $rounding['duration']);
|
||||
$rounder->roundDuration($record, $rounding['duration']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,45 +38,14 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final class TimesheetService
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var TrackingModeService
|
||||
*/
|
||||
private $trackingModeService;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
private $auth;
|
||||
/**
|
||||
* @var ValidatorInterface
|
||||
*/
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
SystemConfiguration $configuration,
|
||||
TimesheetRepository $repository,
|
||||
TrackingModeService $service,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
AuthorizationCheckerInterface $security,
|
||||
ValidatorInterface $validator
|
||||
private SystemConfiguration $configuration,
|
||||
private TimesheetRepository $repository,
|
||||
private TrackingModeService $trackingModeService,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private AuthorizationCheckerInterface $auth,
|
||||
private ValidatorInterface $validator
|
||||
) {
|
||||
$this->configuration = $configuration;
|
||||
$this->repository = $repository;
|
||||
$this->trackingModeService = $service;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->auth = $security;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,6 +132,7 @@ final class TimesheetService
|
||||
$this->stopActiveEntries($timesheet);
|
||||
} catch (ValidationFailedException $vex) {
|
||||
// could happen for timesheets that were started in the future (end before begin)
|
||||
// or if you try to create a new timesheet while an old one is running for too long
|
||||
throw new ValidationFailedException($vex->getViolations(), 'Cannot stop running timesheet');
|
||||
}
|
||||
$this->repository->commit();
|
||||
@@ -183,16 +153,6 @@ final class TimesheetService
|
||||
*/
|
||||
public function updateTimesheet(Timesheet $timesheet): Timesheet
|
||||
{
|
||||
// FIXME stop active entries upon update
|
||||
// there is at least one edge case which leads to a problem:
|
||||
// if you do not allow overlapping entries, you cannot restart a timesheet by removing the
|
||||
// end date if another timesheet is running, because the check for existing timesheets will always trigger
|
||||
/*
|
||||
if ($timesheet->getEnd() === null) {
|
||||
$this->stopActiveEntries($timesheet);
|
||||
}
|
||||
*/
|
||||
|
||||
$this->fixTimezone($timesheet);
|
||||
|
||||
$this->dispatcher->dispatch(new TimesheetUpdatePreEvent($timesheet));
|
||||
|
||||
@@ -9,29 +9,16 @@
|
||||
|
||||
namespace App\Timesheet;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Model\MonthlyStatistic;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
|
||||
final class TimesheetStatisticService
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $repository;
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(TimesheetRepository $repository, EntityManagerInterface $entityManager)
|
||||
public function __construct(private TimesheetRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,9 +97,10 @@ final class TimesheetStatisticService
|
||||
$usersById = [];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$usersById[$user->getId()] = $user;
|
||||
if (!isset($stats[$user->getId()])) {
|
||||
$stats[$user->getId()] = [];
|
||||
$uid = (string) $user->getId();
|
||||
$usersById[$uid] = $user;
|
||||
if (!isset($stats[$uid])) {
|
||||
$stats[$uid] = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +130,9 @@ final class TimesheetStatisticService
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
foreach ($results as $row) {
|
||||
$uid = $row['user'];
|
||||
$pid = $row['project'];
|
||||
$aid = $row['activity'];
|
||||
$uid = (string) $row['user'];
|
||||
$pid = (string) $row['project'];
|
||||
$aid = (string) $row['activity'];
|
||||
if (!isset($stats[$uid][$pid])) {
|
||||
$stats[$uid][$pid] = ['project' => $pid, 'activities' => []];
|
||||
}
|
||||
@@ -186,9 +174,10 @@ final class TimesheetStatisticService
|
||||
$usersById = [];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$usersById[$user->getId()] = $user;
|
||||
if (!isset($stats[$user->getId()])) {
|
||||
$stats[$user->getId()] = [];
|
||||
$uid = (string) $user->getId();
|
||||
$usersById[$uid] = $user;
|
||||
if (!isset($stats[$uid])) {
|
||||
$stats[$uid] = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,9 +209,9 @@ final class TimesheetStatisticService
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
foreach ($results as $row) {
|
||||
$uid = $row['user'];
|
||||
$pid = $row['project'];
|
||||
$aid = $row['activity'];
|
||||
$uid = (string) $row['user'];
|
||||
$pid = (string) $row['project'];
|
||||
$aid = (string) $row['activity'];
|
||||
if (!isset($stats[$uid][$pid])) {
|
||||
$stats[$uid][$pid] = ['project' => $pid, 'activities' => []];
|
||||
}
|
||||
@@ -328,145 +317,4 @@ final class TimesheetStatisticService
|
||||
|
||||
return array_values($stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @param User[] $users
|
||||
* @return array
|
||||
*/
|
||||
public function getGroupedByCustomerProjectActivityUser(DateTime $begin, DateTime $end, array $users): array
|
||||
{
|
||||
$stats = [];
|
||||
|
||||
$qb = $this->repository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
|
||||
->addSelect('IDENTITY(t.user) as user')
|
||||
->addSelect('IDENTITY(t.activity) as activity')
|
||||
->addSelect('IDENTITY(t.project) as project')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('user', $users)
|
||||
->groupBy('project')
|
||||
->addGroupBy('activity')
|
||||
->addGroupBy('user')
|
||||
;
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$projectIds = [];
|
||||
$activityIds = [];
|
||||
$userIds = [];
|
||||
|
||||
foreach ($results as $row) {
|
||||
$projectId = $row['project'];
|
||||
$activityId = $row['activity'];
|
||||
$userId = $row['user'];
|
||||
|
||||
$projectIds[$projectId] = $projectId;
|
||||
$activityIds[$activityId] = $activityId;
|
||||
$userIds[$userId] = $userId;
|
||||
|
||||
if (!isset($stats[$projectId])) {
|
||||
$stats[$projectId] = [
|
||||
'id' => $projectId,
|
||||
'customer' => '',
|
||||
'customer_id' => null,
|
||||
'name' => null,
|
||||
'activities' => [],
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'internalRate' => 0,
|
||||
'max_users' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$stats[$projectId]['duration'] += (int) $row['duration'];
|
||||
$stats[$projectId]['rate'] += (int) $row['rate'];
|
||||
$stats[$projectId]['internalRate'] += (int) $row['internalRate'];
|
||||
|
||||
if (!isset($stats[$projectId]['activities'][$activityId])) {
|
||||
$stats[$projectId]['activities'][$activityId] = [
|
||||
'id' => $activityId,
|
||||
'name' => null,
|
||||
'users' => [],
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'internalRate' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$stats[$projectId]['activities'][$activityId]['duration'] += (int) $row['duration'];
|
||||
$stats[$projectId]['activities'][$activityId]['rate'] += (int) $row['rate'];
|
||||
$stats[$projectId]['activities'][$activityId]['internalRate'] += (int) $row['internalRate'];
|
||||
|
||||
if (!isset($stats[$projectId]['activities'][$activityId]['users'][$userId])) {
|
||||
$stats[$projectId]['activities'][$activityId]['users'][$userId] = [
|
||||
'id' => $userId,
|
||||
'name' => null,
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'internalRate' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$stats[$projectId]['activities'][$activityId]['users'][$userId]['duration'] += (int) $row['duration'];
|
||||
$stats[$projectId]['activities'][$activityId]['users'][$userId]['rate'] += (int) $row['rate'];
|
||||
$stats[$projectId]['activities'][$activityId]['users'][$userId]['internalRate'] += (int) $row['internalRate'];
|
||||
}
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb
|
||||
->select('a.id, a.name')
|
||||
->from(Activity::class, 'a', 'a.id')
|
||||
->where($qb->expr()->in('a.id', ':id'))
|
||||
->setParameter('id', array_values($activityIds))
|
||||
;
|
||||
$activities = $qb->getQuery()->getResult();
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb
|
||||
->select('p.id, p.name, c.id as customer_id, c.name as customer, c.currency')
|
||||
->from(Project::class, 'p', 'p.id')
|
||||
->leftJoin(Customer::class, 'c', Join::WITH, 'c.id = p.customer')
|
||||
->where($qb->expr()->in('p.id', ':id'))
|
||||
->setParameter('id', array_values($projectIds))
|
||||
;
|
||||
$projects = $qb->getQuery()->getResult();
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb
|
||||
->select('u')
|
||||
->from(User::class, 'u', 'u.id')
|
||||
->where($qb->expr()->in('u.id', ':id'))
|
||||
->setParameter('id', array_values($userIds))
|
||||
;
|
||||
$users = $qb->getQuery()->getResult();
|
||||
|
||||
foreach (array_keys($stats) as $pid) {
|
||||
$stats[$pid]['name'] = $projects[$pid]['name'];
|
||||
$stats[$pid]['customer'] = $projects[$pid]['customer'];
|
||||
$stats[$pid]['customer_id'] = $projects[$pid]['customer_id'];
|
||||
foreach (array_keys($stats[$pid]['activities']) as $aid) {
|
||||
$stats[$pid]['activities'][$aid]['name'] = $activities[$aid]['name'];
|
||||
foreach (array_keys($stats[$pid]['activities'][$aid]['users']) as $uid) {
|
||||
$stats[$pid]['activities'][$aid]['users'][$uid]['name'] = $users[$uid]->getDisplayName();
|
||||
}
|
||||
$stats[$pid]['max_users'] = max($stats[$pid]['max_users'], \count($stats[$pid]['activities'][$aid]['users']));
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'stats' => $stats,
|
||||
'projects' => $projects,
|
||||
'activities' => $activities,
|
||||
'users' => $users,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,8 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class DefaultMode extends AbstractTrackingMode
|
||||
{
|
||||
/**
|
||||
* @var RoundingService
|
||||
*/
|
||||
private $rounding;
|
||||
|
||||
public function __construct(RoundingService $rounding)
|
||||
public function __construct(private RoundingService $rounding)
|
||||
{
|
||||
$this->rounding = $rounding;
|
||||
}
|
||||
|
||||
public function canEditBegin(): bool
|
||||
@@ -56,6 +50,11 @@ final class DefaultMode extends AbstractTrackingMode
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getEditTemplate(): string
|
||||
{
|
||||
return 'timesheet/edit-default.html.twig';
|
||||
}
|
||||
|
||||
public function create(Timesheet $timesheet, ?Request $request = null): void
|
||||
{
|
||||
parent::create($timesheet, $request);
|
||||
|
||||
@@ -18,14 +18,8 @@ final class DurationFixedBeginMode implements TrackingModeInterface
|
||||
{
|
||||
use TrackingModeTrait;
|
||||
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
public function __construct(private SystemConfiguration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function canEditBegin(): bool
|
||||
@@ -72,4 +66,9 @@ final class DurationFixedBeginMode implements TrackingModeInterface
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getEditTemplate(): string
|
||||
{
|
||||
return 'timesheet/edit-default.html.twig';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,21 +9,21 @@
|
||||
|
||||
namespace App\Timesheet\TrackingMode;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\RoundingService;
|
||||
use DateTime;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* This is a copy of the DefaultMode from 2.0
|
||||
* FIXME 2.1 remove me with the next release
|
||||
* @deprecated since 2.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
final class DurationOnlyMode extends AbstractTrackingMode
|
||||
{
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
public function __construct(SystemConfiguration $configuration)
|
||||
public function __construct(private RoundingService $rounding)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function canEditBegin(): bool
|
||||
@@ -33,7 +33,7 @@ final class DurationOnlyMode extends AbstractTrackingMode
|
||||
|
||||
public function canEditEnd(): bool
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canEditDuration(): bool
|
||||
@@ -53,24 +53,30 @@ final class DurationOnlyMode extends AbstractTrackingMode
|
||||
|
||||
public function canSeeBeginAndEndTimes(): bool
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getEditTemplate(): string
|
||||
{
|
||||
return 'timesheet/edit-default.html.twig';
|
||||
}
|
||||
|
||||
public function create(Timesheet $timesheet, ?Request $request = null): void
|
||||
{
|
||||
parent::create($timesheet, $request);
|
||||
|
||||
if (null === $timesheet->getBegin()) {
|
||||
$timesheet->setBegin(new DateTime('now', $this->getTimezone($timesheet)));
|
||||
}
|
||||
|
||||
$newBegin = clone $timesheet->getBegin();
|
||||
$this->rounding->roundBegin($timesheet);
|
||||
|
||||
// this prevents the problem that "now" is being ignored in modify()
|
||||
$beginTime = $this->configuration->getTimesheetDefaultBeginTime();
|
||||
$beginTime = (new DateTime($this->configuration->getTimesheetDefaultBeginTime(), $newBegin->getTimezone()))->format('H:i:s');
|
||||
$newBegin->modify($beginTime);
|
||||
if (null !== $timesheet->getEnd()) {
|
||||
$this->rounding->roundEnd($timesheet);
|
||||
|
||||
$timesheet->setBegin($newBegin);
|
||||
|
||||
parent::create($timesheet, $request);
|
||||
if (null !== $timesheet->getDuration()) {
|
||||
$this->rounding->roundDuration($timesheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,4 +53,9 @@ final class PunchInOutMode implements TrackingModeInterface
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getEditTemplate(): string
|
||||
{
|
||||
return 'timesheet/edit-default.html.twig';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,13 @@ interface TrackingModeInterface
|
||||
*/
|
||||
public function canUpdateTimesWithAPI(): bool;
|
||||
|
||||
/**
|
||||
* Returns the edit template path for this tracking mode for regular user mode.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEditTemplate(): string;
|
||||
|
||||
/**
|
||||
* Whether the real begin and end times are shown in the user timesheet.
|
||||
*
|
||||
|
||||
@@ -15,23 +15,12 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
|
||||
final class TrackingModeService
|
||||
{
|
||||
/**
|
||||
* @var TrackingModeInterface[]
|
||||
*/
|
||||
private $modes = [];
|
||||
/**
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* @param SystemConfiguration $configuration
|
||||
* @param TrackingModeInterface[] $modes
|
||||
*/
|
||||
public function __construct(SystemConfiguration $configuration, iterable $modes)
|
||||
public function __construct(private SystemConfiguration $configuration, private iterable $modes)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->modes = $modes;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<?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\Timesheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\CurrentUser;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
* @deprecated will be removed with 2.0
|
||||
*/
|
||||
class UserDateTimeFactory extends DateTimeFactory
|
||||
{
|
||||
/**
|
||||
* @var CurrentUser
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $initializedFromUser = false;
|
||||
|
||||
public function __construct(CurrentUser $user)
|
||||
{
|
||||
parent::__construct(null);
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getTimezone(): DateTimeZone
|
||||
{
|
||||
if ($this->initializedFromUser === false) {
|
||||
@trigger_error('UserDateTimeFactory is deprecated and will be removed with 2.0, use DateTimeFactory instead', E_USER_DEPRECATED);
|
||||
$timezone = date_default_timezone_get();
|
||||
|
||||
$user = $this->user->getUser();
|
||||
if ($user instanceof User) {
|
||||
$timezone = $user->getTimezone();
|
||||
}
|
||||
|
||||
$timezone = new DateTimeZone($timezone);
|
||||
|
||||
parent::setTimezone($timezone);
|
||||
$this->initializedFromUser = true;
|
||||
}
|
||||
|
||||
return parent::getTimezone();
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,12 @@ namespace App\Timesheet;
|
||||
/**
|
||||
* A static helper class for re-usable functionality.
|
||||
*/
|
||||
class Util
|
||||
final class Util
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the rate for a hourly rate and a given duration in seconds.
|
||||
*
|
||||
@@ -23,9 +27,8 @@ class Util
|
||||
*/
|
||||
public static function calculateRate(float $hourlyRate, int $seconds): float
|
||||
{
|
||||
$rate = (float) ($hourlyRate * ($seconds / 3600));
|
||||
$rate = round($rate, 4);
|
||||
$rate = $hourlyRate * ($seconds / 3600);
|
||||
|
||||
return $rate;
|
||||
return round($rate, 4);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user