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

@@ -606,7 +606,7 @@ export default class KimaiCalendar {
<li>` + this.options['translations']['activity'] + `: ` + escaper.escapeForHtml(eventObj.activity) + `</li>
</ul>` +
(eventObj.description !== null || eventObj.tags.length > 0 ? '<hr>' : '') +
(eventObj.description ? '<p>' + escaper.escapeForHtml(eventObj.description) + '</p>' : '') + tags + `
(eventObj.description ? '<div>' + escaper.escapeForHtml(eventObj.description) + '</div>' : '') + tags + `
</div>`;
}

View File

@@ -3,6 +3,9 @@
padding: 0;
list-style-type: none;
}
div {
white-space: pre-line;
}
}
.draggable {

View File

@@ -78,6 +78,7 @@ tabler:
filter: fas fa-filter
help: far fa-question-circle
home: fas fa-home
info: fas fa-info-circle
import: fas fa-file-import
invoice: fas fa-file-contract
invoice-template: fas fa-file-signature
@@ -99,6 +100,7 @@ tabler:
password: fas fa-key
pause: fas fa-pause
pause-small: far fa-pause-circle
pending: fas fa-hourglass-half
pin: fas fa-thumbtack
pdf: fas fa-file-pdf
permissions: fas fa-user-lock
@@ -112,6 +114,7 @@ tabler:
reporting: far fa-chart-bar
report: far fa-chart-bar
review: fas fa-user-check
rejected: fas fa-ban
right: fas fa-chevron-right
right2: fas fa-angle-double-right
roles: fas fa-user-shield

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
"/build/app.99c7b6b4.js"
],
"css": [
"/build/app.a61c91dc.css"
"/build/app.f82609fb.css"
]
},
"export-pdf": {
@@ -45,7 +45,7 @@
"calendar": {
"js": [
"/build/runtime.f0079159.js",
"/build/calendar.25b368b9.js"
"/build/calendar.e7b6fa49.js"
],
"css": [
"/build/calendar.bb473428.css"
@@ -64,7 +64,7 @@
"integrity": {
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
"/build/app.99c7b6b4.js": "sha384-bPAdOMl3I6Ia3clnHgDiwoNKKe/bKnAdHUJdl7BXbsFNMg2wHsUDrGH+uNCQowqM",
"/build/app.a61c91dc.css": "sha384-7rtbsyyUucsBktCZZXFuqnSHX0cvCWQ0KQgHpolGcWlFhaIEPn+ipnOc5yySeYwT",
"/build/app.f82609fb.css": "sha384-X848pib0qXUB72LdaVSupYXnYPE/lNXHXNkJdlh4FRj534BVHyupi+ly3cfCkDRS",
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ",
@@ -72,7 +72,7 @@
"/build/invoice-pdf.ce9aace0.js": "sha384-mtZkXrpsXFMX2YvmmTlCm91QgsTlZLzLJnDAFcTuBDtDZ7TNkqLYkfCeLzFLVfwb",
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
"/build/calendar.25b368b9.js": "sha384-jKqj+OqQVIm8TXCEmnRveqG3348yVwvlb0k88kRn90DZGzbthjToMHUb5pwL/TtI",
"/build/calendar.e7b6fa49.js": "sha384-/lbV1KHFBLr1Y/aaY7Pfckynm/EAcZqBwJQdxum3gvbDVdcthoxPSNTvf++vKadj",
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"

View File

@@ -1,5 +1,5 @@
{
"build/app.css": "/build/app.a61c91dc.css",
"build/app.css": "/build/app.f82609fb.css",
"build/app.js": "/build/app.99c7b6b4.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
@@ -9,7 +9,7 @@
"build/invoice-pdf.js": "/build/invoice-pdf.ce9aace0.js",
"build/chart.js": "/build/chart.f5becfac.js",
"build/calendar.css": "/build/calendar.bb473428.css",
"build/calendar.js": "/build/calendar.25b368b9.js",
"build/calendar.js": "/build/calendar.e7b6fa49.js",
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
"build/dashboard.js": "/build/dashboard.6774a712.js",
"build/runtime.js": "/build/runtime.f0079159.js"

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);
}

View File

@@ -212,7 +212,7 @@
</td>
{% set dayCount = 0 %}
{% for day in month.days %}
{% set class = 'text-end contractDay' %}
{% set class = 'text-end contractDay text-nowrap' %}
{% if day.day is weekend %}
{% set class = class ~ ' weekend' %}
{% endif %}
@@ -223,6 +223,16 @@
{% endif %}
<td class="{{ class }}">
{% if (day.workingTime.expectedTime != 0 or day.workingTime.actualTime != 0) and (now > day.day or day.workingTime.actualTime > 0) %}
{% if day.hasAddons() %}
{% set statusTitle = '' %}
{% for addon in day.getAddons() %}
{% set statusTitle = statusTitle ~ (addon.title|trans) ~ ' (' ~ addon.duration|duration(decimal) ~ ')' %}
{% if not loop.last %}
{% set statusTitle = statusTitle ~ '<br>' %}
{% endif %}
{% endfor %}
<span class="status-dot status-azure" data-toggle="tooltip" data-placement="top" title="{{ statusTitle }}"></span>
{% endif %}
{{ work_times_result(day.workingTime.expectedTime, day.workingTime.actualTime, decimal) }}
{% endif %}
</td>

View File

@@ -22,7 +22,7 @@
}) %}
{% endif %}
{% set datagrid = datagrid|merge({
'profile.first_entry' : (firstTimesheet is not null ? firstTimesheet|date_short : ''),
'profile.first_entry' : (workingSince is not null ? workingSince|date_short : ''),
'profile.registration_date' : user.registeredAt|date_short,
}) %}
{% set seeOwnRate = false %}

View File

@@ -18,7 +18,7 @@ use PHPUnit\Framework\TestCase;
*/
class ConfigureMainMenuEventTest extends TestCase
{
public function testGetterAndSetter()
public function testGetterAndSetter(): void
{
$sut = new ConfigureMainMenuEvent();

View File

@@ -24,8 +24,9 @@ class WorkingTimeYearEventTest extends TestCase
$user = new User();
$date = new \DateTime('2023-02-10');
$year = new Year($date, $user);
$sut = new WorkingTimeYearEvent($year);
$sut = new WorkingTimeYearEvent($year, clone $date);
self::assertSame($year, $sut->getYear());
self::assertEquals('2023-02-10', $sut->getUntil()->format('Y-m-d'));
}
}

View File

@@ -22,12 +22,15 @@ class MonthTest extends TestCase
{
public function testDefaults(): void
{
$user = new User();
$user->setUsername('foo-bar');
$months = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
foreach($months as $key => $days) {
$index = ++$key;
$monthKey = ($index < 10) ? '0' . $index : $index;
$date = new \DateTimeImmutable(sprintf('2020-%s-25 13:00:00', $monthKey));
$month = new Month($date);
$month = new Month($date, $user);
self::assertEquals(sprintf('2020-%s-25', $monthKey), $month->getMonth()->format('Y-m-d'));
self::assertCount($days, $month->getDays());
self::assertFalse($month->isLocked());

View File

@@ -4982,11 +4982,6 @@ parameters:
count: 1
path: Event/CalendarGoogleSourceEventTest.php
-
message: "#^Method App\\\\Tests\\\\Event\\\\ConfigureMainMenuEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#"
count: 1
path: Event/ConfigureMainMenuEventTest.php
-
message: "#^Method App\\\\Tests\\\\Event\\\\CustomerMetaDefinitionEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#"
count: 1