Release 2.0.32 (#4256)

This commit is contained in:
Kevin Papst
2023-08-31 15:33:53 +02:00
committed by GitHub
parent 452de88e18
commit 31348da4c0
42 changed files with 412 additions and 130 deletions

1
.gitignore vendored
View File

@@ -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 ###

View File

@@ -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,

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

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

View File

@@ -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 = [

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

View 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';
}

View File

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

View File

@@ -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".
* *

View File

@@ -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
*/ */

View File

@@ -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,

View File

@@ -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 = [];

View File

@@ -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

View File

@@ -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)
{ {

View File

@@ -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

View File

@@ -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)) {

View File

@@ -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)
{ {

View File

@@ -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)
{ {

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

View File

@@ -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

View File

@@ -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)
{ {

View File

@@ -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;
} }
/** /**

View File

@@ -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
{ {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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;

View File

@@ -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']),

View File

@@ -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) => {

View File

@@ -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 %}

View File

@@ -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 %}

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

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

View File

@@ -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>