Release 2.0.28 (#4172)

See https://github.com/kimai/kimai/pull/4172
This commit is contained in:
Kevin Papst
2023-07-09 15:27:27 +02:00
committed by GitHub
parent 651f812da8
commit 385753fbc3
27 changed files with 182 additions and 53 deletions

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.0.27';
public const VERSION = '2.0.28';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20027;
public const VERSION_ID = 20028;
/**
* The software name
*/

View File

@@ -33,6 +33,7 @@ final class ContractController extends AbstractController
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
$canChangeUser = $this->isGranted('contract_other_profile');
$defaultDate = $dateTimeFactory->createStartOfYear();
$now = $dateTimeFactory->createDateTime();
$values = new YearByUser();
$values->setUser($currentUser);
@@ -62,7 +63,7 @@ final class ContractController extends AbstractController
/** @var \DateTime $yearDate */
$yearDate = $values->getDate();
$year = $workingTimeService->getYear($profile, $yearDate);
$year = $workingTimeService->getYear($profile, $yearDate, $now);
$page = new PageSetup('work_times');
$page->setHelp('contract.html');
@@ -73,7 +74,6 @@ final class ContractController extends AbstractController
$controllerEvent = new WorkContractDetailControllerEvent($year);
$eventDispatcher->dispatch($controllerEvent);
$now = $dateTimeFactory->createDateTime();
$summary = $workingTimeService->getYearSummary($year, $now);
$boxConfiguration = new BoxConfiguration();

View File

@@ -12,6 +12,7 @@ namespace App\Controller;
use App\Configuration\LocaleService;
use App\Entity\User;
use App\Event\ConfigureMainMenuEvent;
use App\Repository\UserRepository;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -28,7 +29,7 @@ final class HomepageController extends AbstractController
public const DEFAULT_ROUTE = 'timesheet';
#[Route(path: '', defaults: [], name: 'homepage', methods: ['GET'])]
public function indexAction(Request $request, LocaleService $service, EventDispatcherInterface $eventDispatcher): Response
public function homepage(Request $request, LocaleService $service, EventDispatcherInterface $eventDispatcher, UserRepository $userRepository): Response
{
$user = $this->getUser();
$userLanguage = $user->getLanguage();
@@ -72,8 +73,13 @@ final class HomepageController extends AbstractController
try {
return $this->redirectToRoute($route, ['_locale' => $language]);
} catch (\Exception $ex) {
$this->logException($ex);
// something is wrong with the url parameters ...
if ($route === $userRoute) {
// fix invalid routes from old plugins / versions
$user->setPreferenceValue('login_initial_view', 'dashboard');
$userRepository->saveUser($user);
} else {
$this->logException($ex);
}
}
}

View File

@@ -60,9 +60,12 @@ final class ProfileController extends AbstractController
{
$dateFactory = $this->getDateTimeFactory();
$userStats = $repository->getUserStatistics($profile);
$firstEntry = $statisticService->findFirstRecordDate($profile);
$workStartingDay = $profile->getWorkStartingDay();
if ($workStartingDay === null) {
$workStartingDay = $statisticService->findFirstRecordDate($profile);
}
$begin = $firstEntry ?? $dateFactory->getStartOfMonth();
$begin = $workStartingDay ?? $dateFactory->getStartOfMonth();
$end = $dateFactory->getEndOfMonth();
// statistic service does not fill up the complete year by default!
@@ -73,7 +76,7 @@ final class ProfileController extends AbstractController
'tab' => 'charts',
'user' => $profile,
'stats' => $userStats,
'firstTimesheet' => $firstEntry,
'workingSince' => $workStartingDay,
'workMonths' => $statisticService->getMonthlyStats($begin, $end, [$profile])[0]
];

View File

@@ -1174,6 +1174,27 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, 0);
}
public function getWorkStartingDay(): ?\DateTimeInterface
{
$date = $this->getPreferenceValue(UserPreference::WORK_STARTING_DAY);
if ($date === null) {
return null;
}
try {
$date = \DateTimeImmutable::createFromFormat('Y-m-d h:i:s', $date . ' 00:00:00', new \DateTimeZone($this->getTimezone()));
} catch (Exception $e) {
}
return ($date instanceof \DateTimeInterface) ? $date : null;
}
public function setWorkStartingDay(?\DateTimeInterface $date): void
{
$this->setPreferenceValue(UserPreference::WORK_STARTING_DAY, $date?->format('Y-m-d'));
}
public function getPublicHolidayGroup(): null|string
{
$group = $this->getPreferenceValue(UserPreference::PUBLIC_HOLIDAY_GROUP);

View File

@@ -38,6 +38,7 @@ class UserPreference
public const WORK_HOURS_FRIDAY = 'work_friday';
public const WORK_HOURS_SATURDAY = 'work_saturday';
public const WORK_HOURS_SUNDAY = 'work_sunday';
public const WORK_STARTING_DAY = 'work_start_day';
public const PUBLIC_HOLIDAY_GROUP = 'public_holiday_group';
public const HOLIDAYS_PER_YEAR = 'holidays';

View File

@@ -21,7 +21,6 @@ final class ConfigureMainMenuEvent extends Event
private MenuItemModel $apps;
private MenuItemModel $admin;
private MenuItemModel $system;
private ?MenuItemModel $root = null;
public function __construct()
{
@@ -33,15 +32,23 @@ final class ConfigureMainMenuEvent extends Event
public function findById(string $identifier): ?MenuItemModel
{
if ($this->root === null) {
$this->root = new MenuItemModel('root', 'root');
$this->root->addChild($this->menu);
$this->root->addChild($this->apps);
$this->root->addChild($this->admin);
$this->root->addChild($this->system);
if (($tmp = $this->menu->findChild($identifier)) !== null) {
return $tmp;
}
return $this->root->findChild($identifier);
if (($tmp = $this->apps->findChild($identifier)) !== null) {
return $tmp;
}
if (($tmp = $this->admin->findChild($identifier)) !== null) {
return $tmp;
}
if (($tmp = $this->system->findChild($identifier)) !== null) {
return $tmp;
}
return null;
}
public function getMenu(): MenuItemModel

View File

@@ -12,12 +12,21 @@ namespace App\Event;
use App\WorkingTime\Model\Year;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Working time for every day of the given year.
* Will be reflected in the working-time summary row.
*/
final class WorkingTimeYearEvent extends Event
{
public function __construct(private Year $year)
public function __construct(private Year $year, private \DateTimeInterface $until)
{
}
public function getUntil(): \DateTimeInterface
{
return $this->until;
}
public function getYear(): Year
{
return $this->year;

View File

@@ -15,8 +15,8 @@ use App\Model\Day as BaseDay;
final class Day extends BaseDay
{
private ?WorkingTime $workingTime = null;
/** @var array<string, int> */
private array $descriptions = [];
/** @var array<DayAddon> */
private array $addons = [];
public function isLocked(): bool
{
@@ -38,18 +38,27 @@ final class Day extends BaseDay
}
/**
* @return array<string, int>
* @return array<DayAddon>
*/
public function getDescriptions(): array
public function getAddons(): array
{
return $this->descriptions;
return $this->addons;
}
public function hasAddons(): bool
{
return \count($this->addons) > 0;
}
/**
* Descriptions show up in the approval PDF and maybe in other places as well.
*/
public function addDescription(string $description, int $duration): void
public function addAddon(DayAddon $addon): void
{
$this->descriptions[$description] = $duration;
$this->addons[] = $addon;
if (!$this->isLocked() && $this->workingTime !== null) {
$this->workingTime->setActualTime($this->workingTime->getActualTime() + $addon->getDuration());
}
}
}

View File

@@ -0,0 +1,43 @@
<?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 DayAddon
{
private string $title;
private int $duration;
private bool $billable = true;
public function __construct(string $title, int $duration)
{
$this->title = $title;
$this->duration = $duration;
}
public function getTitle(): string
{
return $this->title;
}
public function getDuration(): int
{
return $this->duration;
}
public function isBillable(): bool
{
return $this->billable;
}
public function setBillable(bool $billable): void
{
$this->billable = $billable;
}
}

View File

@@ -18,6 +18,16 @@ use App\Model\Month as BaseMonth;
*/
final class Month extends BaseMonth
{
public function __construct(\DateTimeInterface $month, private User $user)
{
parent::__construct($month);
}
public function getUser(): User
{
return $this->user;
}
/**
* A month is only locked IF every day is approved.
* If there is even one day left open, the entire month is not locked.

View File

@@ -30,7 +30,7 @@ final class Year extends BaseYear
protected function createMonth(\DateTimeInterface $month): Month
{
return new Month($month);
return new Month($month, $this->user);
}
public function getExpectedTime(\DateTimeInterface $until): int

View File

@@ -46,7 +46,7 @@ final class WorkingTimeService
return $this->workingTimeRepository->getLatestApproval($user);
}
public function getYear(User $user, \DateTimeInterface $yearDate): Year
public function getYear(User $user, \DateTimeInterface $yearDate, \DateTimeInterface $until): Year
{
$yearTimes = $this->workingTimeRepository->findForYear($user, $yearDate);
$existing = [];
@@ -57,6 +57,7 @@ final class WorkingTimeService
$year = new Year(\DateTimeImmutable::createFromInterface($yearDate), $user);
$stats = null;
$firstDay = $user->getWorkStartingDay();
foreach ($year->getMonths() as $month) {
foreach ($month->getDays() as $day) {
@@ -70,8 +71,12 @@ final class WorkingTimeService
$stats = $this->getYearStatistics($yearDate, $user);
}
$result = new WorkingTime($user, $day->getDay());
$result->setExpectedTime($user->getWorkHoursForDay($day->getDay()));
$dayDate = $day->getDay();
$result = new WorkingTime($user, $dayDate);
if ($firstDay === null || $firstDay <= $dayDate) {
$result->setExpectedTime($user->getWorkHoursForDay($dayDate));
}
if (\array_key_exists($key, $stats)) {
$result->setActualTime($stats[$key]);
@@ -81,16 +86,16 @@ final class WorkingTimeService
}
}
$event = new WorkingTimeYearEvent($year);
$event = new WorkingTimeYearEvent($year, $until);
$this->eventDispatcher->dispatch($event);
return $year;
}
public function getMonth(User $user, \DateTimeInterface $monthDate): Month
public function getMonth(User $user, \DateTimeInterface $monthDate, \DateTimeInterface $until): Month
{
// uses the year, because that triggers the required events to collect all different working times
$year = $this->getYear($user, $monthDate);
$year = $this->getYear($user, $monthDate, $until);
return $year->getMonth($monthDate);
}