@@ -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
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
src/WorkingTime/Model/DayAddon.php
Normal file
43
src/WorkingTime/Model/DayAddon.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user