Release 2.0.32 (#4256)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,6 +38,7 @@ php.ini
|
|||||||
/var/sessions/*
|
/var/sessions/*
|
||||||
/var/packages/*
|
/var/packages/*
|
||||||
/var/plugins/*
|
/var/plugins/*
|
||||||
|
/var/plugins_old/
|
||||||
*.disabled
|
*.disabled
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
|
|||||||
@@ -458,6 +458,23 @@ export default class KimaiCalendar {
|
|||||||
name: 'google',
|
name: 'google',
|
||||||
editable: false,
|
editable: false,
|
||||||
}};
|
}};
|
||||||
|
} else if (source.type === 'json') {
|
||||||
|
calendarSource = {...calendarSource, ...{
|
||||||
|
id: 'json-' + source.id,
|
||||||
|
editable: false,
|
||||||
|
events: (fetchInfo, successCallback, failureCallback) => {
|
||||||
|
let url = source.url.replace('{from}', DATES.formatForAPI(fetchInfo.start));
|
||||||
|
url = url.replace('{to}', DATES.formatForAPI(fetchInfo.end));
|
||||||
|
|
||||||
|
API.get(url, {}, result => {
|
||||||
|
let apiEvents = [];
|
||||||
|
for (const record of result) {
|
||||||
|
apiEvents.push(record);
|
||||||
|
}
|
||||||
|
successCallback(apiEvents);
|
||||||
|
}, failureCallback);
|
||||||
|
},
|
||||||
|
}};
|
||||||
} else if (source.type === 'ical') {
|
} else if (source.type === 'ical') {
|
||||||
calendarSource = {...calendarSource, ...{
|
calendarSource = {...calendarSource, ...{
|
||||||
id: 'ical-' + source.id,
|
id: 'ical-' + source.id,
|
||||||
|
|||||||
15
phpstan.neon
15
phpstan.neon
@@ -2131,11 +2131,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Event/CalendarConfigurationEvent.php
|
path: src/Event/CalendarConfigurationEvent.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Event\\\\CalendarConfigurationEvent\\:\\:setConfiguration\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Event/CalendarConfigurationEvent.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Event\\\\CalendarConfigurationEvent\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#"
|
message: "#^Method App\\\\Event\\\\CalendarConfigurationEvent\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -2161,11 +2156,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Event/PageActionsEvent.php
|
path: src/Event/PageActionsEvent.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Property App\\\\Event\\\\PermissionSectionsEvent\\:\\:\\$sections type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Event/PermissionSectionsEvent.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Event\\\\PermissionsEvent\\:\\:getPermissions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
message: "#^Method App\\\\Event\\\\PermissionsEvent\\:\\:getPermissions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -2176,11 +2166,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Event/PermissionsEvent.php
|
path: src/Event/PermissionsEvent.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Property App\\\\Event\\\\PermissionsEvent\\:\\:\\$sections type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Event/PermissionsEvent.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Event\\\\RevenueStatisticEvent\\:\\:getRevenue\\(\\) return type has no value type specified in iterable type array\\.$#"
|
message: "#^Method App\\\\Event\\\\RevenueStatisticEvent\\:\\:getRevenue\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -45,7 +45,7 @@
|
|||||||
"calendar": {
|
"calendar": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.f0079159.js",
|
"/build/runtime.f0079159.js",
|
||||||
"/build/calendar.e7b6fa49.js"
|
"/build/calendar.ea514f59.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/calendar.bb473428.css"
|
"/build/calendar.bb473428.css"
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"/build/invoice-pdf.ce9aace0.js": "sha384-mtZkXrpsXFMX2YvmmTlCm91QgsTlZLzLJnDAFcTuBDtDZ7TNkqLYkfCeLzFLVfwb",
|
"/build/invoice-pdf.ce9aace0.js": "sha384-mtZkXrpsXFMX2YvmmTlCm91QgsTlZLzLJnDAFcTuBDtDZ7TNkqLYkfCeLzFLVfwb",
|
||||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||||
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
|
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
|
||||||
"/build/calendar.e7b6fa49.js": "sha384-/lbV1KHFBLr1Y/aaY7Pfckynm/EAcZqBwJQdxum3gvbDVdcthoxPSNTvf++vKadj",
|
"/build/calendar.ea514f59.js": "sha384-s162N0JRCI0twVtDROON8kABO+riWgiXM7O6TA175IOkAh94twcLqKoAfycd2BrA",
|
||||||
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
||||||
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
|
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
|
||||||
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"build/invoice-pdf.js": "/build/invoice-pdf.ce9aace0.js",
|
"build/invoice-pdf.js": "/build/invoice-pdf.ce9aace0.js",
|
||||||
"build/chart.js": "/build/chart.f5becfac.js",
|
"build/chart.js": "/build/chart.f5becfac.js",
|
||||||
"build/calendar.css": "/build/calendar.bb473428.css",
|
"build/calendar.css": "/build/calendar.bb473428.css",
|
||||||
"build/calendar.js": "/build/calendar.e7b6fa49.js",
|
"build/calendar.js": "/build/calendar.ea514f59.js",
|
||||||
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
||||||
"build/dashboard.js": "/build/dashboard.6774a712.js",
|
"build/dashboard.js": "/build/dashboard.6774a712.js",
|
||||||
"build/runtime.js": "/build/runtime.f0079159.js"
|
"build/runtime.js": "/build/runtime.f0079159.js"
|
||||||
|
|||||||
93
src/API/Model/CalendarEvent.php
Normal file
93
src/API/Model/CalendarEvent.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?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\API\Model;
|
||||||
|
|
||||||
|
use App\Utils\Color;
|
||||||
|
use JMS\Serializer\Annotation as Serializer;
|
||||||
|
|
||||||
|
#[Serializer\ExclusionPolicy('all')]
|
||||||
|
final class CalendarEvent
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calendar entry title
|
||||||
|
*/
|
||||||
|
#[Serializer\Expose]
|
||||||
|
#[Serializer\Groups(['Default'])]
|
||||||
|
#[Serializer\Type(name: 'string')]
|
||||||
|
private string $title; // @phpstan-ignore-line
|
||||||
|
/**
|
||||||
|
* Calendar background color
|
||||||
|
*/
|
||||||
|
#[Serializer\Expose]
|
||||||
|
#[Serializer\Groups(['Default'])]
|
||||||
|
#[Serializer\Type(name: 'string')]
|
||||||
|
private ?string $color = null; // @phpstan-ignore-line
|
||||||
|
/**
|
||||||
|
* Calendar text color
|
||||||
|
*/
|
||||||
|
#[Serializer\Expose]
|
||||||
|
#[Serializer\Groups(['Default'])]
|
||||||
|
#[Serializer\Type(name: 'string')]
|
||||||
|
private ?string $textColor = null;
|
||||||
|
/**
|
||||||
|
* If this entry is all-day long
|
||||||
|
*/
|
||||||
|
#[Serializer\Expose]
|
||||||
|
#[Serializer\Groups(['Default'])]
|
||||||
|
#[Serializer\Type(name: 'boolean')]
|
||||||
|
private bool $allDay = false; // @phpstan-ignore-line
|
||||||
|
/**
|
||||||
|
* Calendar entry start date
|
||||||
|
*/
|
||||||
|
#[Serializer\Expose]
|
||||||
|
#[Serializer\Groups(['Default'])]
|
||||||
|
#[Serializer\Type(name: 'DateTime')]
|
||||||
|
private \DateTimeInterface $start; // @phpstan-ignore-line
|
||||||
|
/**
|
||||||
|
* Calendar entry end date
|
||||||
|
*/
|
||||||
|
#[Serializer\Expose]
|
||||||
|
#[Serializer\Groups(['Default'])]
|
||||||
|
#[Serializer\Type(name: 'DateTime')]
|
||||||
|
private \DateTimeInterface $end; // @phpstan-ignore-line
|
||||||
|
|
||||||
|
public function setTitle(string $title): void
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStart(\DateTimeInterface $start): void
|
||||||
|
{
|
||||||
|
$this->start = $start;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEnd(\DateTimeInterface $end): void
|
||||||
|
{
|
||||||
|
$this->end = $end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setColor(?string $color): void
|
||||||
|
{
|
||||||
|
$this->color = $color;
|
||||||
|
if ($color !== null && $this->textColor === null) {
|
||||||
|
$this->textColor = (new Color())->getFontContrastColor($color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAllDay(bool $allDay): void
|
||||||
|
{
|
||||||
|
$this->allDay = $allDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTextColor(?string $textColor): void
|
||||||
|
{
|
||||||
|
$this->textColor = $textColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ use App\Entity\User;
|
|||||||
use App\Event\CalendarConfigurationEvent;
|
use App\Event\CalendarConfigurationEvent;
|
||||||
use App\Event\CalendarDragAndDropSourceEvent;
|
use App\Event\CalendarDragAndDropSourceEvent;
|
||||||
use App\Event\CalendarGoogleSourceEvent;
|
use App\Event\CalendarGoogleSourceEvent;
|
||||||
|
use App\Event\CalendarSourceEvent;
|
||||||
use App\Event\RecentActivityEvent;
|
use App\Event\RecentActivityEvent;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
use App\Utils\Color;
|
use App\Utils\Color;
|
||||||
@@ -81,6 +82,23 @@ final class CalendarService
|
|||||||
return new Google($apiKey, $sources);
|
return new Google($apiKey, $sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<CalendarSource>
|
||||||
|
*/
|
||||||
|
public function getSources(User $user): array
|
||||||
|
{
|
||||||
|
$sources = [];
|
||||||
|
|
||||||
|
$event = new CalendarSourceEvent($user);
|
||||||
|
$this->dispatcher->dispatch($event);
|
||||||
|
|
||||||
|
foreach ($event->getSources() as $source) {
|
||||||
|
$sources[] = $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sources;
|
||||||
|
}
|
||||||
|
|
||||||
public function getConfiguration(): array
|
public function getConfiguration(): array
|
||||||
{
|
{
|
||||||
$config = [
|
$config = [
|
||||||
|
|||||||
58
src/Calendar/CalendarSource.php
Normal file
58
src/Calendar/CalendarSource.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?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\Calendar;
|
||||||
|
|
||||||
|
class CalendarSource
|
||||||
|
{
|
||||||
|
/** @var array<string, string|bool|int> */
|
||||||
|
private array $options = [];
|
||||||
|
|
||||||
|
public function __construct(private CalendarSourceType $type, private string $id, private string $uri, private ?string $color = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): CalendarSourceType
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeName(): string
|
||||||
|
{
|
||||||
|
return $this->type->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUri(): string
|
||||||
|
{
|
||||||
|
return $this->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColor(): ?string
|
||||||
|
{
|
||||||
|
return $this->color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addOption(string $name, int|bool|string $value): void
|
||||||
|
{
|
||||||
|
$this->options[$name] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, bool|int|string>
|
||||||
|
*/
|
||||||
|
public function getOptions(): array
|
||||||
|
{
|
||||||
|
return $this->options;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Calendar/CalendarSourceType.php
Normal file
18
src/Calendar/CalendarSourceType.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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\Calendar;
|
||||||
|
|
||||||
|
enum CalendarSourceType: string
|
||||||
|
{
|
||||||
|
case GOOGLE = 'google';
|
||||||
|
case ICAL = 'ical';
|
||||||
|
case JSON = 'json';
|
||||||
|
case TIMESHEET = 'timesheet';
|
||||||
|
}
|
||||||
@@ -9,24 +9,10 @@
|
|||||||
|
|
||||||
namespace App\Calendar;
|
namespace App\Calendar;
|
||||||
|
|
||||||
final class GoogleSource
|
final class GoogleSource extends CalendarSource
|
||||||
{
|
{
|
||||||
public function __construct(private string $id, private string $uri, private ?string $color = null)
|
public function __construct(string $id, string $uri, ?string $color = null)
|
||||||
{
|
{
|
||||||
}
|
parent::__construct(CalendarSourceType::GOOGLE, $id, $uri, $color);
|
||||||
|
|
||||||
public function getId(): string
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUri(): string
|
|
||||||
{
|
|
||||||
return $this->uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getColor(): ?string
|
|
||||||
{
|
|
||||||
return $this->color;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
|
|
||||||
namespace App\Configuration;
|
namespace App\Configuration;
|
||||||
|
|
||||||
use App\Constants;
|
|
||||||
|
|
||||||
final class LocaleService
|
final class LocaleService
|
||||||
{
|
{
|
||||||
public function __construct(private array $languageSettings)
|
public function __construct(private array $languageSettings)
|
||||||
@@ -32,11 +30,6 @@ final class LocaleService
|
|||||||
return \in_array($language, $this->getAllLocales());
|
return \in_array($language, $this->getAllLocales());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDefaultLocale(): string
|
|
||||||
{
|
|
||||||
return Constants::DEFAULT_LOCALE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the locale specific date format, which should be used in combination with the twig filter "|date".
|
* Returns the locale specific date format, which should be used in combination with the twig filter "|date".
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.0.31';
|
public const VERSION = '2.0.32';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 20031;
|
public const VERSION_ID = 20032;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
@@ -38,10 +38,6 @@ class Constants
|
|||||||
* Homepage, used in multiple views
|
* Homepage, used in multiple views
|
||||||
*/
|
*/
|
||||||
public const HOMEPAGE = 'https://www.kimai.org';
|
public const HOMEPAGE = 'https://www.kimai.org';
|
||||||
/**
|
|
||||||
* Application wide default locale
|
|
||||||
*/
|
|
||||||
public const DEFAULT_LOCALE = 'en';
|
|
||||||
/**
|
/**
|
||||||
* Default color for Customer, Project and Activity entities
|
* Default color for Customer, Project and Activity entities
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ final class CalendarController extends AbstractController
|
|||||||
'config' => $config,
|
'config' => $config,
|
||||||
'dragAndDrop' => $dragAndDrop,
|
'dragAndDrop' => $dragAndDrop,
|
||||||
'google' => $this->calendarService->getGoogleSources($profile),
|
'google' => $this->calendarService->getGoogleSources($profile),
|
||||||
|
'sources' => $this->calendarService->getSources($profile),
|
||||||
'now' => $factory->createDateTime(),
|
'now' => $factory->createDateTime(),
|
||||||
'defaultStartTime' => $defaultStart,
|
'defaultStartTime' => $defaultStart,
|
||||||
'is_punch_mode' => $isPunchMode,
|
'is_punch_mode' => $isPunchMode,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ final class HomepageController extends AbstractController
|
|||||||
// if a user somehow managed to get a wrong locale into hos account (eg. an imported user from Kimai 1)
|
// if a user somehow managed to get a wrong locale into hos account (eg. an imported user from Kimai 1)
|
||||||
// make sure that he will still see a beautiful page and not a 404
|
// make sure that he will still see a beautiful page and not a 404
|
||||||
if (!$service->isKnownLocale($userLanguage)) {
|
if (!$service->isKnownLocale($userLanguage)) {
|
||||||
$userLanguage = $service->getDefaultLocale();
|
$userLanguage = 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
$routes = [];
|
$routes = [];
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use App\Constants;
|
|
||||||
use App\Export\Annotation as Exporter;
|
use App\Export\Annotation as Exporter;
|
||||||
use App\Utils\StringHelper;
|
use App\Utils\StringHelper;
|
||||||
use App\Validator\Constraints as Constraints;
|
use App\Validator\Constraints as Constraints;
|
||||||
@@ -55,7 +54,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
public const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
|
public const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
|
||||||
|
|
||||||
public const DEFAULT_ROLE = self::ROLE_USER;
|
public const DEFAULT_ROLE = self::ROLE_USER;
|
||||||
public const DEFAULT_LANGUAGE = Constants::DEFAULT_LOCALE;
|
public const DEFAULT_LANGUAGE = 'en';
|
||||||
public const DEFAULT_FIRST_WEEKDAY = 'monday';
|
public const DEFAULT_FIRST_WEEKDAY = 'monday';
|
||||||
|
|
||||||
public const AUTH_INTERNAL = 'kimai';
|
public const AUTH_INTERNAL = 'kimai';
|
||||||
@@ -169,7 +168,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
#[Assert\Email(mode: 'html5', groups: ['Registration', 'UserCreate', 'Profile'])]
|
#[Assert\Email(mode: 'html5', groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||||
private ?string $email = null;
|
private ?string $email = null;
|
||||||
#[ORM\Column(name: 'account', type: 'string', length: 30, nullable: true)]
|
#[ORM\Column(name: 'account', type: 'string', length: 30, nullable: true)]
|
||||||
#[Assert\Length(max: 30, groups: ['Registration', 'UserCreate', 'Profile'])]
|
#[Assert\Length(max: 30)]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Default'])]
|
#[Serializer\Groups(['Default'])]
|
||||||
#[Exporter\Expose(label: 'account_number')]
|
#[Exporter\Expose(label: 'account_number')]
|
||||||
@@ -195,6 +194,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
* Random string sent to the user email address in order to verify it.
|
* Random string sent to the user email address in order to verify it.
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(name: 'confirmation_token', type: 'string', length: 180, unique: true, nullable: true)]
|
#[ORM\Column(name: 'confirmation_token', type: 'string', length: 180, unique: true, nullable: true)]
|
||||||
|
#[Assert\Length(max: 180)]
|
||||||
private ?string $confirmationToken = null;
|
private ?string $confirmationToken = null;
|
||||||
#[ORM\Column(name: 'password_requested_at', type: 'datetime', nullable: true)]
|
#[ORM\Column(name: 'password_requested_at', type: 'datetime', nullable: true)]
|
||||||
private ?\DateTime $passwordRequestedAt = null;
|
private ?\DateTime $passwordRequestedAt = null;
|
||||||
@@ -1055,7 +1055,8 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
|
|
||||||
public function setAccountNumber(?string $accountNumber): void
|
public function setAccountNumber(?string $accountNumber): void
|
||||||
{
|
{
|
||||||
$this->accountNumber = $accountNumber;
|
// @CloudRequired because SAML mapping could include a longer value
|
||||||
|
$this->accountNumber = StringHelper::ensureMaxLength($accountNumber, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isSystemAccount(): bool
|
public function isSystemAccount(): bool
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ abstract class AbstractMetaDisplayEvent extends Event implements MetaDisplayEven
|
|||||||
/**
|
/**
|
||||||
* @var MetaTableTypeInterface[]
|
* @var MetaTableTypeInterface[]
|
||||||
*/
|
*/
|
||||||
private $fields = [];
|
private array $fields = [];
|
||||||
|
|
||||||
public function __construct(private BaseQuery $query, private string $location)
|
public function __construct(private BaseQuery $query, private string $location)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,16 +14,9 @@ use App\Model\ActivityStatistic;
|
|||||||
|
|
||||||
final class ActivityStatisticEvent extends AbstractActivityEvent
|
final class ActivityStatisticEvent extends AbstractActivityEvent
|
||||||
{
|
{
|
||||||
private $statistic;
|
public function __construct(Activity $activity, private ActivityStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null)
|
||||||
private $begin;
|
|
||||||
private $end;
|
|
||||||
|
|
||||||
public function __construct(Activity $activity, ActivityStatistic $statistic, \DateTime $begin = null, \DateTime $end = null)
|
|
||||||
{
|
{
|
||||||
parent::__construct($activity);
|
parent::__construct($activity);
|
||||||
$this->statistic = $statistic;
|
|
||||||
$this->begin = $begin;
|
|
||||||
$this->end = $end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatistic(): ActivityStatistic
|
public function getStatistic(): ActivityStatistic
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ final class CalendarConfigurationEvent extends Event
|
|||||||
return $this->configuration;
|
return $this->configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setConfiguration(array $configuration)
|
public function setConfiguration(array $configuration): void
|
||||||
{
|
{
|
||||||
foreach ($configuration as $key => $value) {
|
foreach ($configuration as $key => $value) {
|
||||||
if (\array_key_exists($key, $this->configuration)) {
|
if (\array_key_exists($key, $this->configuration)) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ final class CalendarDragAndDropSourceEvent extends Event
|
|||||||
/**
|
/**
|
||||||
* @var DragAndDropSource[]
|
* @var DragAndDropSource[]
|
||||||
*/
|
*/
|
||||||
private $sources = [];
|
private array $sources = [];
|
||||||
|
|
||||||
public function __construct(private User $user, private int $maxEntries)
|
public function __construct(private User $user, private int $maxEntries)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ final class CalendarGoogleSourceEvent extends Event
|
|||||||
/**
|
/**
|
||||||
* @var GoogleSource[]
|
* @var GoogleSource[]
|
||||||
*/
|
*/
|
||||||
private $sources = [];
|
private array $sources = [];
|
||||||
|
|
||||||
public function __construct(private User $user)
|
public function __construct(private User $user)
|
||||||
{
|
{
|
||||||
|
|||||||
44
src/Event/CalendarSourceEvent.php
Normal file
44
src/Event/CalendarSourceEvent.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?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\Event;
|
||||||
|
|
||||||
|
use App\Calendar\CalendarSource;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
|
final class CalendarSourceEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CalendarSource[]
|
||||||
|
*/
|
||||||
|
private array $sources = [];
|
||||||
|
|
||||||
|
public function __construct(private User $user)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addSource(CalendarSource $source): void
|
||||||
|
{
|
||||||
|
$this->sources[] = $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return CalendarSource[]
|
||||||
|
*/
|
||||||
|
public function getSources(): array
|
||||||
|
{
|
||||||
|
return $this->sources;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,16 +14,9 @@ use App\Model\CustomerStatistic;
|
|||||||
|
|
||||||
final class CustomerStatisticEvent extends AbstractCustomerEvent
|
final class CustomerStatisticEvent extends AbstractCustomerEvent
|
||||||
{
|
{
|
||||||
private $statistic;
|
public function __construct(Customer $customer, private CustomerStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null)
|
||||||
private $begin;
|
|
||||||
private $end;
|
|
||||||
|
|
||||||
public function __construct(Customer $customer, CustomerStatistic $statistic, \DateTime $begin = null, \DateTime $end = null)
|
|
||||||
{
|
{
|
||||||
parent::__construct($customer);
|
parent::__construct($customer);
|
||||||
$this->statistic = $statistic;
|
|
||||||
$this->begin = $begin;
|
|
||||||
$this->end = $end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatistic(): CustomerStatistic
|
public function getStatistic(): CustomerStatistic
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ final class DashboardEvent extends Event
|
|||||||
/**
|
/**
|
||||||
* @var array<string>
|
* @var array<string>
|
||||||
*/
|
*/
|
||||||
private $widgets = [];
|
private array $widgets = [];
|
||||||
|
|
||||||
public function __construct(private User $user)
|
public function __construct(private User $user)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ use Symfony\Contracts\EventDispatcher\Event;
|
|||||||
|
|
||||||
final class InvoiceDocumentsEvent extends Event
|
final class InvoiceDocumentsEvent extends Event
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var array<InvoiceDocument>
|
|
||||||
*/
|
|
||||||
private array $documents;
|
|
||||||
/**
|
/**
|
||||||
* Maximum amount of allowed invoice documents.
|
* Maximum amount of allowed invoice documents.
|
||||||
*/
|
*/
|
||||||
@@ -26,9 +22,8 @@ final class InvoiceDocumentsEvent extends Event
|
|||||||
/**
|
/**
|
||||||
* @param InvoiceDocument[] $documents
|
* @param InvoiceDocument[] $documents
|
||||||
*/
|
*/
|
||||||
public function __construct(array $documents)
|
public function __construct(private array $documents)
|
||||||
{
|
{
|
||||||
$this->documents = $documents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ use Symfony\Contracts\EventDispatcher\Event;
|
|||||||
final class PermissionSectionsEvent extends Event
|
final class PermissionSectionsEvent extends Event
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array<PermissionSectionInterface>
|
||||||
*/
|
*/
|
||||||
private $sections;
|
private array $sections = [];
|
||||||
|
|
||||||
public function addSection(PermissionSectionInterface $section): PermissionSectionsEvent
|
public function addSection(PermissionSectionInterface $section): PermissionSectionsEvent
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ use Symfony\Contracts\EventDispatcher\Event;
|
|||||||
final class PermissionsEvent extends Event
|
final class PermissionsEvent extends Event
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array<string, array<string>>
|
||||||
*/
|
*/
|
||||||
private $sections = [];
|
private array $sections = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $section
|
* @param string $section
|
||||||
|
|||||||
@@ -14,16 +14,9 @@ use App\Model\ProjectStatistic;
|
|||||||
|
|
||||||
final class ProjectStatisticEvent extends AbstractProjectEvent
|
final class ProjectStatisticEvent extends AbstractProjectEvent
|
||||||
{
|
{
|
||||||
private $statistic;
|
public function __construct(Project $project, private ProjectStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null)
|
||||||
private $begin;
|
|
||||||
private $end;
|
|
||||||
|
|
||||||
public function __construct(Project $project, ProjectStatistic $statistic, \DateTime $begin = null, \DateTime $end = null)
|
|
||||||
{
|
{
|
||||||
parent::__construct($project);
|
parent::__construct($project);
|
||||||
$this->statistic = $statistic;
|
|
||||||
$this->begin = $begin;
|
|
||||||
$this->end = $end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatistic(): ProjectStatistic
|
public function getStatistic(): ProjectStatistic
|
||||||
|
|||||||
@@ -13,15 +13,9 @@ use App\Entity\Timesheet;
|
|||||||
|
|
||||||
final class TimesheetDuplicatePostEvent extends AbstractTimesheetEvent
|
final class TimesheetDuplicatePostEvent extends AbstractTimesheetEvent
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(Timesheet $new, private Timesheet $original)
|
||||||
* @var Timesheet
|
|
||||||
*/
|
|
||||||
private $original;
|
|
||||||
|
|
||||||
public function __construct(Timesheet $new, Timesheet $original)
|
|
||||||
{
|
{
|
||||||
parent::__construct($new);
|
parent::__construct($new);
|
||||||
$this->original = $original;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOriginalTimesheet(): Timesheet
|
public function getOriginalTimesheet(): Timesheet
|
||||||
|
|||||||
@@ -13,15 +13,9 @@ use App\Entity\Timesheet;
|
|||||||
|
|
||||||
final class TimesheetDuplicatePreEvent extends AbstractTimesheetEvent
|
final class TimesheetDuplicatePreEvent extends AbstractTimesheetEvent
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(Timesheet $new, private Timesheet $original)
|
||||||
* @var Timesheet
|
|
||||||
*/
|
|
||||||
private $original;
|
|
||||||
|
|
||||||
public function __construct(Timesheet $new, Timesheet $original)
|
|
||||||
{
|
{
|
||||||
parent::__construct($new);
|
parent::__construct($new);
|
||||||
$this->original = $original;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOriginalTimesheet(): Timesheet
|
public function getOriginalTimesheet(): Timesheet
|
||||||
|
|||||||
@@ -13,15 +13,9 @@ use App\Entity\Timesheet;
|
|||||||
|
|
||||||
final class TimesheetRestartPostEvent extends AbstractTimesheetEvent
|
final class TimesheetRestartPostEvent extends AbstractTimesheetEvent
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(Timesheet $new, private Timesheet $original)
|
||||||
* @var Timesheet
|
|
||||||
*/
|
|
||||||
private $original;
|
|
||||||
|
|
||||||
public function __construct(Timesheet $new, Timesheet $original)
|
|
||||||
{
|
{
|
||||||
parent::__construct($new);
|
parent::__construct($new);
|
||||||
$this->original = $original;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOriginalTimesheet(): Timesheet
|
public function getOriginalTimesheet(): Timesheet
|
||||||
|
|||||||
@@ -13,15 +13,9 @@ use App\Entity\Timesheet;
|
|||||||
|
|
||||||
final class TimesheetRestartPreEvent extends AbstractTimesheetEvent
|
final class TimesheetRestartPreEvent extends AbstractTimesheetEvent
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(Timesheet $new, private Timesheet $original)
|
||||||
* @var Timesheet
|
|
||||||
*/
|
|
||||||
private $original;
|
|
||||||
|
|
||||||
public function __construct(Timesheet $new, Timesheet $original)
|
|
||||||
{
|
{
|
||||||
parent::__construct($new);
|
parent::__construct($new);
|
||||||
$this->original = $original;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOriginalTimesheet(): Timesheet
|
public function getOriginalTimesheet(): Timesheet
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ final class RedirectToLocaleSubscriber implements EventSubscriberInterface
|
|||||||
|
|
||||||
// Add the default locale at the first position of the array, because getPreferredLanguage()
|
// Add the default locale at the first position of the array, because getPreferredLanguage()
|
||||||
// returns the first element when no appropriate language is found
|
// returns the first element when no appropriate language is found
|
||||||
array_unshift($allLanguages, $this->localeService->getDefaultLocale());
|
array_unshift($allLanguages, 'en');
|
||||||
|
|
||||||
$preferredLanguage = $request->getPreferredLanguage(array_unique($allLanguages));
|
$preferredLanguage = $request->getPreferredLanguage(array_unique($allLanguages));
|
||||||
|
|
||||||
|
|||||||
@@ -130,10 +130,14 @@ final class SamlProvider
|
|||||||
}
|
}
|
||||||
if ($part[0] === '$') {
|
if ($part[0] === '$') {
|
||||||
$key = substr($part, 1);
|
$key = substr($part, 1);
|
||||||
if (!isset($attributes[$key])) {
|
if (!\array_key_exists($key, $attributes)) {
|
||||||
throw new \RuntimeException('Missing user attribute: ' . $key);
|
throw new \RuntimeException('Missing user attribute: ' . $key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists(0, $attributes[$key])) {
|
||||||
|
throw new \RuntimeException('Missing user attribute value: ' . $key);
|
||||||
|
}
|
||||||
|
|
||||||
$results[] = $attributes[$key][0];
|
$results[] = $attributes[$key][0];
|
||||||
} else {
|
} else {
|
||||||
$results[] = $part;
|
$results[] = $part;
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
|||||||
new TwigFilter('day_name', [$this, 'dayName']),
|
new TwigFilter('day_name', [$this, 'dayName']),
|
||||||
new TwigFilter('date_short', [$this, 'dateShort']),
|
new TwigFilter('date_short', [$this, 'dateShort']),
|
||||||
new TwigFilter('date_time', [$this, 'dateTime']),
|
new TwigFilter('date_time', [$this, 'dateTime']),
|
||||||
new TwigFilter('date_full', [$this, 'dateTime']), // deprecated: needs to be kept for invoice and export templates
|
// cannot be deleted right now, needs to be kept for invoice and export templates
|
||||||
|
new TwigFilter('date_full', [$this, 'dateTime'], ['deprecated' => true, 'alternative' => 'date_time']),
|
||||||
new TwigFilter('date_format', [$this, 'dateFormat']),
|
new TwigFilter('date_format', [$this, 'dateFormat']),
|
||||||
new TwigFilter('date_weekday', [$this, 'dateWeekday']),
|
new TwigFilter('date_weekday', [$this, 'dateWeekday']),
|
||||||
new TwigFilter('time', [$this, 'time']),
|
new TwigFilter('time', [$this, 'time']),
|
||||||
|
|||||||
@@ -141,8 +141,8 @@
|
|||||||
{% for source in google.sources %}
|
{% for source in google.sources %}
|
||||||
,
|
,
|
||||||
{
|
{
|
||||||
id: 'googleCompanyCalendar',
|
id: 'googleCompanyCalendar{{ loop.index }}',
|
||||||
type: 'google',
|
type: '{{ source.typeName }}',
|
||||||
options: {
|
options: {
|
||||||
googleCalendarId: '{{ source.uri }}',
|
googleCalendarId: '{{ source.uri }}',
|
||||||
name: '{{ source.id }}',
|
name: '{{ source.id }}',
|
||||||
@@ -152,6 +152,18 @@
|
|||||||
}
|
}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% for source in sources %}
|
||||||
|
,
|
||||||
|
{
|
||||||
|
id: '{{ source.id }}{{ loop.index }}',
|
||||||
|
type: '{{ source.typeName }}',
|
||||||
|
url: '{{ source.uri }}',
|
||||||
|
options: {...{{ source.options|json_encode|raw }}, ...{
|
||||||
|
color: '{{ source.color }}',
|
||||||
|
textColor: '{{ source.color|font_contrast }}',
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
{% endfor %}
|
||||||
],
|
],
|
||||||
url: {
|
url: {
|
||||||
update: (timesheetId) => {
|
update: (timesheetId) => {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ mpdf-->
|
|||||||
<table class="items">
|
<table class="items">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
{% block summary_head_row_start %}{% endblock %}
|
||||||
{% for summaryColumn in summaryColumns %}
|
{% for summaryColumn in summaryColumns %}
|
||||||
{% if summaryColumn == 'customer' %}
|
{% if summaryColumn == 'customer' %}
|
||||||
<th>{{ 'customer'|trans }}</th>
|
<th>{{ 'customer'|trans }}</th>
|
||||||
@@ -93,6 +94,7 @@ mpdf-->
|
|||||||
<th class="center">{{ summaryColumn|trans }}</th>
|
<th class="center">{{ summaryColumn|trans }}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% block summary_head_row_end %}{% endblock %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -120,6 +122,7 @@ mpdf-->
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if customer is not same as(summary.customer) %}
|
{% if customer is not same as(summary.customer) %}
|
||||||
<tr class="summary">
|
<tr class="summary">
|
||||||
|
{% block summary_customer_row_start %}{% endblock %}
|
||||||
{% for summaryColumn in summaryColumns %}
|
{% for summaryColumn in summaryColumns %}
|
||||||
{% if summaryColumn == 'duration' %}
|
{% if summaryColumn == 'duration' %}
|
||||||
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
|
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
|
||||||
@@ -131,6 +134,7 @@ mpdf-->
|
|||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% block summary_customer_row_end %}{% endblock %}
|
||||||
</tr>
|
</tr>
|
||||||
{% set customerCurrency = summary.currency %}
|
{% set customerCurrency = summary.currency %}
|
||||||
{% set customer = summary.customer %}
|
{% set customer = summary.customer %}
|
||||||
@@ -140,6 +144,7 @@ mpdf-->
|
|||||||
{% set customerCount = 0 %}
|
{% set customerCount = 0 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="{{ cycle(['odd', 'even'], customerCount) }}">
|
<tr class="{{ cycle(['odd', 'even'], customerCount) }}">
|
||||||
|
{% block summary_project_row_start %}{% endblock %}
|
||||||
{% for summaryColumn in summaryColumns %}
|
{% for summaryColumn in summaryColumns %}
|
||||||
{% if summaryColumn == 'customer' %}
|
{% if summaryColumn == 'customer' %}
|
||||||
<td>{{ summary.customer }}</td>
|
<td>{{ summary.customer }}</td>
|
||||||
@@ -168,6 +173,7 @@ mpdf-->
|
|||||||
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
|
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% block summary_project_row_end %}{% endblock %}
|
||||||
</tr>
|
</tr>
|
||||||
{% set customerDuration = customerDuration + summary.duration %}
|
{% set customerDuration = customerDuration + summary.duration %}
|
||||||
{% set customerRate = customerRate + summary.rate %}
|
{% set customerRate = customerRate + summary.rate %}
|
||||||
@@ -176,6 +182,7 @@ mpdf-->
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if showCustomerSummary and customer is not same as(null) %}
|
{% if showCustomerSummary and customer is not same as(null) %}
|
||||||
<tr class="summary">
|
<tr class="summary">
|
||||||
|
{% block summary_customer_summary_row_start %}{% endblock %}
|
||||||
{% for summaryColumn in summaryColumns %}
|
{% for summaryColumn in summaryColumns %}
|
||||||
{% if summaryColumn == 'duration' %}
|
{% if summaryColumn == 'duration' %}
|
||||||
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
|
<td class="totals duration">{{ customerDuration|duration(decimal) }}</td>
|
||||||
@@ -187,10 +194,12 @@ mpdf-->
|
|||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% block summary_customer_summary_row_end %}{% endblock %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if showTotalSummary and not multiCurrency %}
|
{% if showTotalSummary and not multiCurrency %}
|
||||||
<tr class="{% if not showCustomerSummary %}summary{% else %}summary-total{% endif %}">
|
<tr class="{% if not showCustomerSummary %}summary{% else %}summary-total{% endif %}">
|
||||||
|
{% block summary_summary_row_start %}{% endblock %}
|
||||||
<td class="totals" colspan="2">
|
<td class="totals" colspan="2">
|
||||||
{{ 'sum.total'|trans }}
|
{{ 'sum.total'|trans }}
|
||||||
</td>
|
</td>
|
||||||
@@ -205,6 +214,7 @@ mpdf-->
|
|||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% block summary_summary_row_end %}{% endblock %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -224,6 +234,7 @@ mpdf-->
|
|||||||
<table class="items">
|
<table class="items">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
{% block items_head_row_start %}{% endblock %}
|
||||||
<th>{{ 'date'|trans }}</th>
|
<th>{{ 'date'|trans }}</th>
|
||||||
{% if showUserColumn %}
|
{% if showUserColumn %}
|
||||||
<th>{{ 'user'|trans }}</th>
|
<th>{{ 'user'|trans }}</th>
|
||||||
@@ -239,6 +250,7 @@ mpdf-->
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<th class="center">{{ 'rate'|trans }}</th>
|
<th class="center">{{ 'rate'|trans }}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% block items_head_row_end %}{% endblock %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -253,6 +265,7 @@ mpdf-->
|
|||||||
{% set currency = null %}
|
{% set currency = null %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
||||||
|
{% block items_entry_row_start %}{% endblock %}
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
{% block date_begin %}{% if not showDateTimeShort %}{{ entry.begin|date_time }}{% else %}{{ entry.begin|date_short }}{% endif %}{% endblock %}
|
{% block date_begin %}{% if not showDateTimeShort %}{{ entry.begin|date_time }}{% else %}{{ entry.begin|date_short }}{% endif %}{% endblock %}
|
||||||
{% if entry.end %}
|
{% if entry.end %}
|
||||||
@@ -299,9 +312,12 @@ mpdf-->
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="cost">{{ entryRate }}</td>
|
<td class="cost">{{ entryRate }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% block items_entry_row_end %}{% endblock %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% block items_summary %}
|
||||||
<tr class="summary">
|
<tr class="summary">
|
||||||
|
{% block items_summary_row_start %}{% endblock %}
|
||||||
{% if showUserColumn %}
|
{% if showUserColumn %}
|
||||||
<td colspan="3">{{ 'sum.total'|trans }}</td>
|
<td colspan="3">{{ 'sum.total'|trans }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -317,7 +333,9 @@ mpdf-->
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="totals cost">{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
|
<td class="totals cost">{% if currency is not null %}{{ rate|money(currency) }}{% endif %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% block items_summary_row_end %}{% endblock %}
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endblock %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -58,13 +58,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% set length = 12 %}
|
||||||
|
{% if form.supervisor is defined %}
|
||||||
|
{% set length = 6 %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-{{ length }}">
|
||||||
{{ form_row(form.accountNumber) }}
|
{{ form_row(form.accountNumber) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
{% if form.supervisor is defined %}
|
||||||
|
<div class="col-md-{{ length }}">
|
||||||
{{ form_row(form.supervisor) }}
|
{{ form_row(form.supervisor) }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if form.teams is defined or form.roles is defined %}
|
{% if form.teams is defined or form.roles is defined %}
|
||||||
|
|||||||
41
tests/Calendar/CalendarSourceTest.php
Normal file
41
tests/Calendar/CalendarSourceTest.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?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\Tests\Calendar;
|
||||||
|
|
||||||
|
use App\Calendar\CalendarSource;
|
||||||
|
use App\Calendar\CalendarSourceType;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Calendar\CalendarSource
|
||||||
|
*/
|
||||||
|
class CalendarSourceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testSource(): void
|
||||||
|
{
|
||||||
|
$sut = new CalendarSource(CalendarSourceType::JSON, '0815', 'askdjfhlaksjdhflaksjhdflkjasdlkfjh', '#fffccc');
|
||||||
|
$sut->addOption('foo', 'bar');
|
||||||
|
$sut->addOption('hello', false);
|
||||||
|
$sut->addOption('world', 123);
|
||||||
|
|
||||||
|
$this->assertEquals(CalendarSourceType::JSON, $sut->getType());
|
||||||
|
$this->assertEquals('json', $sut->getTypeName());
|
||||||
|
$this->assertEquals('0815', $sut->getId());
|
||||||
|
$this->assertEquals('askdjfhlaksjdhflaksjhdflkjasdlkfjh', $sut->getUri());
|
||||||
|
$this->assertEquals('#fffccc', $sut->getColor());
|
||||||
|
$this->assertEquals(['foo' => 'bar', 'hello' => false, 'world' => 123], $sut->getOptions());
|
||||||
|
|
||||||
|
$sut = new CalendarSource(CalendarSourceType::TIMESHEET, '0815', 'askdjfhlaksjdhflaksjhdflkjasdlkfjh', null);
|
||||||
|
|
||||||
|
$this->assertEquals('0815', $sut->getId());
|
||||||
|
$this->assertEquals('askdjfhlaksjdhflaksjhdflkjasdlkfjh', $sut->getUri());
|
||||||
|
$this->assertNull($sut->getColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
38
tests/Event/CalendarSourceEventTest.php
Normal file
38
tests/Event/CalendarSourceEventTest.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Event;
|
||||||
|
|
||||||
|
use App\Calendar\CalendarSource;
|
||||||
|
use App\Calendar\CalendarSourceType;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Event\CalendarSourceEvent;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Event\CalendarSourceEvent
|
||||||
|
*/
|
||||||
|
class CalendarSourceEventTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testEvent(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setAlias('foo');
|
||||||
|
|
||||||
|
$sut = new CalendarSourceEvent($user);
|
||||||
|
|
||||||
|
self::assertSame($user, $sut->getUser());
|
||||||
|
self::assertIsArray($sut->getSources());
|
||||||
|
self::assertEmpty($sut->getSources());
|
||||||
|
|
||||||
|
$sut->addSource(new CalendarSource(CalendarSourceType::TIMESHEET, '', ''));
|
||||||
|
|
||||||
|
self::assertCount(1, $sut->getSources());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1623,7 +1623,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="CDTC1gc" resname="supervisor">
|
<trans-unit id="CDTC1gc" resname="supervisor">
|
||||||
<source>supervisor</source>
|
<source>supervisor</source>
|
||||||
<target>Vorgesetzter</target>
|
<target>Supervisor</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
Reference in New Issue
Block a user