Release 2.25 (#5109)
This commit is contained in:
@@ -17,7 +17,7 @@ use App\Entity\Timesheet;
|
||||
use App\Event\PageActionsEvent;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\API\Model\TimesheetConfig;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\API\Model\Version;
|
||||
use App\Plugin\PluginManager;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Attribute\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
@@ -15,11 +15,11 @@ use App\Plugin\Plugin;
|
||||
use App\Plugin\PluginManager;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Process\PhpSubprocess;
|
||||
|
||||
#[AsCommand(name: 'kimai:plugins', description: 'Manage Kimai plugins')]
|
||||
final class PluginCommand extends Command
|
||||
@@ -69,14 +69,28 @@ final class PluginCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$command = $this->getApplication()?->find('doctrine:migrations:migrate');
|
||||
if ($command === null) {
|
||||
throw new \RuntimeException('Failed finding doctrine migrations command');
|
||||
}
|
||||
$cmdInput = new ArrayInput(['--allow-no-migration' => true, '--configuration' => $config]);
|
||||
$cmdInput->setInteractive(false);
|
||||
if (0 !== $command->run($cmdInput, $output)) {
|
||||
$io->error('Failed to install bundle database: ' . $config);
|
||||
// using getApplication()->find('doctrine:migrations:migrate') does NOT work here
|
||||
// because the Doctrine command can only be executed once
|
||||
// if run more than once it fails with a "Container is frozen" exception
|
||||
|
||||
$process = new PhpSubprocess([
|
||||
'bin/console',
|
||||
'doctrine:migrations:migrate',
|
||||
'--allow-no-migration',
|
||||
'--no-interaction',
|
||||
'--configuration=' . $config
|
||||
]);
|
||||
$process->run();
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
$io->error('Failed to install bundle database: ' . PHP_EOL . $config);
|
||||
$io->error($process->getErrorOutput());
|
||||
} else {
|
||||
if ($io->isVerbose()) {
|
||||
$io->write($process->getOutput());
|
||||
} else {
|
||||
$io->success('Successfully installed: ' . $plugin->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,15 @@ final class RegenerateLocalesCommand extends Command
|
||||
$shortTime = new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT);
|
||||
|
||||
$settings['date'] = $shortDate->getPattern();
|
||||
if ($settings['date'] === false) {
|
||||
$io->error('Invalid date pattern for locale: ' . $locale);
|
||||
continue;
|
||||
}
|
||||
$settings['time'] = $shortTime->getPattern();
|
||||
if ($settings['time'] === false) {
|
||||
$io->error('Invalid time pattern for locale: ' . $locale);
|
||||
continue;
|
||||
}
|
||||
|
||||
// see https://github.com/kimai/kimai/issues/4402 - Korean time format failed parsing
|
||||
// special case when time pattern starts with A / a => this will lead to an error
|
||||
|
||||
@@ -48,7 +48,9 @@ final class TranslationCommand extends Command
|
||||
->addOption('fill-empty', null, InputOption::VALUE_NONE, 'Pre-fills empty translations with the english version')
|
||||
->addOption('delete-empty', null, InputOption::VALUE_NONE, 'Delete all empty keys and files which have no translated key at all')
|
||||
->addOption('move-resname', null, InputOption::VALUE_REQUIRED, 'Move a resname from one file to another (needs "source" and "target" options)')
|
||||
->addOption('move-all', null, InputOption::VALUE_NONE, 'Move all keys from one file to another (needs "source" and "target" options)')
|
||||
->addOption('source', null, InputOption::VALUE_REQUIRED, 'Single source file to use')
|
||||
->addOption('only-core', null, InputOption::VALUE_NONE, 'Do not include plugin and theme directories')
|
||||
->addOption('target', null, InputOption::VALUE_REQUIRED, 'Single target file to use')
|
||||
// DEEPL TRANSLATION FEATURE - UNTESTED
|
||||
->addOption('translate-locale', null, InputOption::VALUE_REQUIRED, 'Translate into the given locale with Deepl')
|
||||
@@ -68,10 +70,13 @@ final class TranslationCommand extends Command
|
||||
|
||||
$bases = [
|
||||
'core' => $this->projectDirectory . '/translations/*.xlf',
|
||||
'plugins' => $this->projectDirectory . Kernel::PLUGIN_DIRECTORY . '/*/Resources/translations/*.xlf',
|
||||
'theme' => $this->projectDirectory . '/vendor/kevinpapst/tabler-bundle/translations/*.xlf',
|
||||
];
|
||||
|
||||
if (!$input->getOption('only-core')) {
|
||||
$bases['plugins'] = $this->projectDirectory . Kernel::PLUGIN_DIRECTORY . '/*/Resources/translations/*.xlf';
|
||||
$bases['theme'] = $this->projectDirectory . '/vendor/kevinpapst/tabler-bundle/translations/*.xlf';
|
||||
}
|
||||
|
||||
$sources = [];
|
||||
if ($input->getOption('source') !== null) {
|
||||
/** @var string $tmp */
|
||||
@@ -146,6 +151,19 @@ final class TranslationCommand extends Command
|
||||
return $this->moveResname($io, $moveResname, $sources, $targets);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Move all keys from source to target
|
||||
// ==========================================================================
|
||||
if ($input->getOption('move-all')) {
|
||||
if (\count($sources) === 0 || \count($targets) === 0) {
|
||||
$io->error('Moving all keys only works with one source and one target file');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return $this->moveAll($io, $sources, $targets);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Fill empty translations with english version
|
||||
// ==========================================================================
|
||||
@@ -634,4 +652,66 @@ final class TranslationCommand extends Command
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $sources
|
||||
* @param array<string> $targets
|
||||
*/
|
||||
private function moveAll(SymfonyStyle $io, array $sources, array $targets): int
|
||||
{
|
||||
foreach ($sources as $source) {
|
||||
$tmp = basename($source);
|
||||
$pos = strpos($tmp, '.');
|
||||
if ($pos === false) {
|
||||
$io->error('Unexpected filename: ' . $source);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$suffix = substr($tmp, $pos);
|
||||
$target = null;
|
||||
foreach ($targets as $t) {
|
||||
if (str_ends_with($t, $suffix)) {
|
||||
$target = $t;
|
||||
}
|
||||
}
|
||||
|
||||
if ($target === null) {
|
||||
$io->error('Cannot find translation target file for source: ' . $source);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$sourceDocument = new \DOMDocument('1.0');
|
||||
$sourceDocument->load($source);
|
||||
|
||||
$targetDocument = new \DOMDocument('1.0');
|
||||
$targetDocument->load($target);
|
||||
$removeNodes = [];
|
||||
|
||||
/** @var \DOMElement $element */
|
||||
foreach ($sourceDocument->getElementsByTagName('trans-unit') as $element) {
|
||||
$newNode = $targetDocument->importNode($element, true);
|
||||
$targetDocument->documentElement->firstElementChild->firstElementChild->appendChild($newNode); // @phpstan-ignore-line
|
||||
$removeNodes[] = $element;
|
||||
}
|
||||
|
||||
foreach ($removeNodes as $node) {
|
||||
$sourceDocument->documentElement->firstElementChild->firstElementChild->removeChild($node); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
$xmlDocument = new \DOMDocument('1.0');
|
||||
$xmlDocument->preserveWhiteSpace = false;
|
||||
$xmlDocument->formatOutput = true;
|
||||
$xmlDocument->loadXML($sourceDocument->saveXML()); // @phpstan-ignore-line
|
||||
file_put_contents($source, $xmlDocument->saveXML());
|
||||
|
||||
$xmlDocument = new \DOMDocument('1.0');
|
||||
$xmlDocument->preserveWhiteSpace = false;
|
||||
$xmlDocument->formatOutput = true;
|
||||
$xmlDocument->loadXML($targetDocument->saveXML()); // @phpstan-ignore-line
|
||||
file_put_contents($target, $xmlDocument->saveXML());
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.24.0';
|
||||
public const VERSION = '2.25.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 22400;
|
||||
public const VERSION_ID = 22500;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -111,6 +111,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
|
||||
$tags[] = $tag;
|
||||
}
|
||||
|
||||
$newTimesheets = [];
|
||||
foreach ($allUsers as $user) {
|
||||
$newTimesheet = $entry->createCopy();
|
||||
$newTimesheet->setUser($user);
|
||||
@@ -118,6 +119,11 @@ final class TimesheetTeamController extends TimesheetAbstractController
|
||||
$newTimesheet->addTag($tag);
|
||||
}
|
||||
$this->service->prepareNewTimesheet($newTimesheet, $request);
|
||||
$this->service->validateTimesheet($newTimesheet);
|
||||
$newTimesheets[] = $newTimesheet;
|
||||
}
|
||||
|
||||
foreach ($newTimesheets as $newTimesheet) {
|
||||
$this->service->saveNewTimesheet($newTimesheet);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ final class UTCDateTimeImmutableType extends DateTimeImmutableType
|
||||
/**
|
||||
* @param T $value
|
||||
* @return (T is null ? null : string)
|
||||
* @template T<\DateTimeImmutable>
|
||||
* @template T
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
|
||||
@@ -25,7 +25,7 @@ final class UTCDateTimeType extends DateTimeType
|
||||
* @param T $value
|
||||
* @param AbstractPlatform $platform
|
||||
* @return (T is null ? null : string)
|
||||
* @template T<\DateTime>
|
||||
* @template T
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
|
||||
@@ -109,16 +109,14 @@ trait MetaTableTypeTrait
|
||||
|
||||
// unchecked checkboxes / false bool would save an empty string in the database
|
||||
// those cannot be searched in the database
|
||||
if (null !== $value) {
|
||||
switch ($this->type) {
|
||||
case YesNoType::class:
|
||||
case CheckboxType::class:
|
||||
if (!\is_int($value) && !\is_bool($value) && !\is_string($value)) {
|
||||
throw new \InvalidArgumentException('Failed converting meta-field bool value');
|
||||
} else {
|
||||
$value = (string) $value;
|
||||
}
|
||||
}
|
||||
switch ($this->type) {
|
||||
case YesNoType::class:
|
||||
case CheckboxType::class:
|
||||
if ($value === false || $value === '' || !\is_scalar($value)) {
|
||||
$value = 0;
|
||||
} else {
|
||||
$value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
|
||||
@@ -178,13 +178,13 @@ class Team
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User[]
|
||||
* @return list<User>
|
||||
*/
|
||||
public function getTeamleads(): array
|
||||
{
|
||||
$leads = [];
|
||||
foreach ($this->members as $member) {
|
||||
if ($member->isTeamlead()) {
|
||||
if ($member->isTeamlead() && $member->getUser() !== null) {
|
||||
$leads[] = $member->getUser();
|
||||
}
|
||||
}
|
||||
@@ -273,13 +273,15 @@ class Team
|
||||
/**
|
||||
* Returns all users in the team, both teamlead and normal member.
|
||||
*
|
||||
* @return User[]
|
||||
* @return list<User>
|
||||
*/
|
||||
public function getUsers(): array
|
||||
{
|
||||
$users = [];
|
||||
foreach ($this->members as $member) {
|
||||
$users[] = $member->getUser();
|
||||
if ($member->getUser() !== null) {
|
||||
$users[] = $member->getUser();
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
|
||||
@@ -624,11 +624,9 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function createCopy(?Timesheet $timesheet = null): Timesheet
|
||||
public function createCopy(): Timesheet
|
||||
{
|
||||
if (null === $timesheet) {
|
||||
$timesheet = new Timesheet();
|
||||
}
|
||||
$timesheet = new Timesheet();
|
||||
|
||||
$values = get_object_vars($this);
|
||||
foreach ($values as $k => $v) {
|
||||
@@ -639,7 +637,9 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
|
||||
/** @var TimesheetMeta $meta */
|
||||
foreach ($this->meta as $meta) {
|
||||
$timesheet->setMetaField(clone $meta);
|
||||
$tmp = clone $meta;
|
||||
$tmp->setEntity($timesheet);
|
||||
$timesheet->setMetaField($tmp);
|
||||
}
|
||||
|
||||
$timesheet->tags = new ArrayCollection();
|
||||
|
||||
@@ -752,9 +752,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
* This method should not be called by plugins and returns true on success or false on a failure.
|
||||
*
|
||||
* @internal immutable property that cannot be set by plugins
|
||||
* @param bool $canSeeAllData
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function initCanSeeAllData(bool $canSeeAllData): bool
|
||||
{
|
||||
|
||||
@@ -150,11 +150,26 @@ class UserPreference
|
||||
* integer, float, string, boolean or null
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return UserPreference
|
||||
*/
|
||||
public function setValue($value): UserPreference
|
||||
{
|
||||
$this->value = $value;
|
||||
// unchecked checkboxes / false bool would save an empty string in the database
|
||||
// those cannot be searched in the database
|
||||
switch ($this->type) {
|
||||
case YesNoType::class:
|
||||
case CheckboxType::class:
|
||||
if ($value === false || $value === '' || !\is_scalar($value)) {
|
||||
$value = 0;
|
||||
} else {
|
||||
$value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$this->value = $value;
|
||||
} elseif (\is_scalar($value)) {
|
||||
$this->value = (string) $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
public function addQuickImport(string $url): void
|
||||
{
|
||||
$this->addAction('import', ['url' => $url, 'class' => 'toolbar-action', 'title' => 'import', 'icon' => 'upload', 'translation_domain' => 'actions']);
|
||||
$this->addAction('import', ['url' => $url, 'class' => 'toolbar-action', 'title' => 'import', 'icon' => 'upload']);
|
||||
}
|
||||
|
||||
public function addQuickExport(string $url): void
|
||||
@@ -172,7 +172,7 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
public function addEdit(string $url, bool $modal = true, string $class = ''): void
|
||||
{
|
||||
$this->addAction('edit', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' . ($class === '' ? '' : ' ' . $class) : $class), 'translation_domain' => 'actions', 'title' => 'edit']);
|
||||
$this->addAction('edit', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' . ($class === '' ? '' : ' ' . $class) : $class), 'title' => 'edit']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,20 +180,20 @@ class PageActionsEvent extends ThemeEvent
|
||||
*/
|
||||
public function addSettings(string $url): void
|
||||
{
|
||||
$this->addAction('settings', ['url' => $url, 'class' => 'modal-ajax-form', 'title' => 'settings', 'translation_domain' => 'actions', 'accesskey' => 'h']);
|
||||
$this->addAction('settings', ['url' => $url, 'class' => 'modal-ajax-form', 'title' => 'settings', 'accesskey' => 'h']);
|
||||
}
|
||||
|
||||
public function addConfig(string $url): void
|
||||
{
|
||||
$this->addAction('settings', ['url' => $url, 'title' => 'settings', 'translation_domain' => 'actions']);
|
||||
$this->addAction('settings', ['url' => $url, 'title' => 'settings']);
|
||||
}
|
||||
|
||||
public function addDelete(string $url, bool $remoteConfirm = true): void
|
||||
{
|
||||
if ($remoteConfirm) {
|
||||
$this->addAction('trash', ['url' => $url, 'class' => 'modal-ajax-form text-red', 'translation_domain' => 'actions', 'title' => 'trash']);
|
||||
$this->addAction('trash', ['url' => $url, 'class' => 'modal-ajax-form text-red', 'title' => 'trash']);
|
||||
} else {
|
||||
$this->addAction('trash', ['url' => $url, 'class' => 'confirmation-link text-red', 'attr' => ['data-question' => 'confirm.delete'], 'translation_domain' => 'actions', 'title' => 'trash']);
|
||||
$this->addAction('trash', ['url' => $url, 'class' => 'confirmation-link text-red', 'attr' => ['data-question' => 'confirm.delete'], 'title' => 'trash']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ abstract class AbstractActionsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private ?string $locale = null;
|
||||
|
||||
public function __construct(private AuthorizationCheckerInterface $auth, private UrlGeneratorInterface $urlGenerator)
|
||||
public function __construct(private readonly AuthorizationCheckerInterface $auth, private readonly UrlGeneratorInterface $urlGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if (!$timesheet->isRunning() && $this->isGranted('start', $timesheet)) {
|
||||
$event->addAction('repeat', ['title' => 'repeat', 'translation_domain' => 'actions', 'url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link dd-ts-repeat', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
|
||||
$event->addAction('repeat', ['title' => 'repeat', 'url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link dd-ts-repeat', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $timesheet)) {
|
||||
@@ -38,7 +38,7 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('duplicate', $timesheet)) {
|
||||
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
|
||||
$event->addAction('copy', ['title' => 'copy', 'translation_domain' => 'actions', 'url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class . ' dd-ts-duplicate']);
|
||||
$event->addAction('copy', ['title' => 'copy', 'url' => $this->path($routeDuplicate, ['id' => $timesheet->getId()]), 'class' => $class . ' dd-ts-duplicate']);
|
||||
}
|
||||
|
||||
if ($event->countActions() > 0) {
|
||||
@@ -49,7 +49,6 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
|
||||
$event->addAction('trash', [
|
||||
'url' => $this->path('delete_timesheet', ['id' => $timesheet->getId()]),
|
||||
'class' => 'api-link text-red dd-ts-trash',
|
||||
'translation_domain' => 'actions',
|
||||
'attr' => [
|
||||
'data-event' => 'kimai.timesheetDelete',
|
||||
'data-method' => 'DELETE',
|
||||
|
||||
@@ -31,7 +31,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if (!$event->isView('activity_details') && $this->isGranted('view', $activity)) {
|
||||
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('activity_details', ['id' => $activity->getId()])]);
|
||||
$event->addAction('details', ['title' => 'details', 'url' => $this->path('activity_details', ['id' => $activity->getId()])]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $activity)) {
|
||||
@@ -40,7 +40,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('permissions', $activity)) {
|
||||
$class = $event->isView('permissions') ? '' : 'modal-ajax-form';
|
||||
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity_permissions', ['id' => $activity->getId()]), 'class' => $class]);
|
||||
$event->addAction('permissions', ['title' => 'permissions', 'url' => $this->path('admin_activity_permissions', ['id' => $activity->getId()]), 'class' => $class]);
|
||||
}
|
||||
|
||||
if ($event->countActions() > 0) {
|
||||
@@ -53,7 +53,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
|
||||
$parameters['customers[]'] = $activity->getProject()->getCustomer()->getId();
|
||||
$parameters['projects[]'] = $activity->getProject()->getId();
|
||||
}
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', $parameters)]);
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', $parameters)]);
|
||||
}
|
||||
|
||||
if ($event->hasSubmenu('filter')) {
|
||||
@@ -65,7 +65,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
|
||||
if (!$activity->isGlobal()) {
|
||||
$parameters['project'] = $activity->getProject()->getId();
|
||||
}
|
||||
$event->addAction('create-timesheet', ['title' => 'create-timesheet', 'translation_domain' => 'actions', 'icon' => 'start', 'url' => $this->path('admin_timesheet_create', $parameters), 'class' => 'modal-ajax-form']);
|
||||
$event->addAction('create-timesheet', ['title' => 'create-timesheet', 'icon' => 'start', 'url' => $this->path('admin_timesheet_create', $parameters), 'class' => 'modal-ajax-form']);
|
||||
}
|
||||
|
||||
if (($event->isIndexView() || $event->isView('project_details')) && $this->isGranted('delete', $activity)) {
|
||||
|
||||
@@ -34,7 +34,7 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
|
||||
$isListingView = $event->isIndexView() || $event->isCustomView();
|
||||
|
||||
if (!$event->isView('customer_details') && $canView) {
|
||||
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('customer_details', ['id' => $customer->getId()])]);
|
||||
$event->addAction('details', ['title' => 'details', 'url' => $this->path('customer_details', ['id' => $customer->getId()])]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $customer)) {
|
||||
@@ -43,7 +43,7 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('permissions', $customer)) {
|
||||
$class = $event->isView('permissions') ? '' : 'modal-ajax-form';
|
||||
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_customer_permissions', ['id' => $customer->getId()]), 'class' => $class]);
|
||||
$event->addAction('permissions', ['title' => 'permissions', 'url' => $this->path('admin_customer_permissions', ['id' => $customer->getId()]), 'class' => $class]);
|
||||
}
|
||||
|
||||
if ($isListingView) {
|
||||
@@ -61,15 +61,15 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_project') || $this->isGranted('view_teamlead_project') || $this->isGranted('view_team_project')) {
|
||||
$event->addActionToSubmenu('filter', 'project', ['title' => 'project.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_project', ['customers[]' => $customer->getId()])]);
|
||||
$event->addActionToSubmenu('filter', 'project', ['title' => 'project.filter', 'url' => $this->path('admin_project', ['customers[]' => $customer->getId()])]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_activity')) {
|
||||
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId()])]);
|
||||
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId()])]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId()])]);
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId()])]);
|
||||
}
|
||||
|
||||
if ($event->hasSubmenu('filter')) {
|
||||
|
||||
@@ -56,7 +56,7 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
|
||||
$allowDelete = $this->isGranted('delete_invoice');
|
||||
if (!$invoice->isCanceled()) {
|
||||
$id = $allowDelete ? 'invoice.cancel' : 'trash';
|
||||
$event->addAction($id, ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'canceled', 'token' => $payload['token']]), 'title' => 'invoice.cancel', 'translation_domain' => 'actions']);
|
||||
$event->addAction($id, ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'canceled', 'token' => $payload['token']]), 'title' => 'invoice.cancel']);
|
||||
}
|
||||
|
||||
if ($this->isGranted('delete_invoice')) {
|
||||
|
||||
@@ -34,7 +34,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
|
||||
$isListingView = $event->isIndexView() || $event->isCustomView();
|
||||
|
||||
if (!$event->isView('project_details') && $this->isGranted('view', $project)) {
|
||||
$event->addAction('details', ['title' => 'details', 'translation_domain' => 'actions', 'url' => $this->path('project_details', ['id' => $project->getId()])]);
|
||||
$event->addAction('details', ['title' => 'details', 'url' => $this->path('project_details', ['id' => $project->getId()])]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $project)) {
|
||||
@@ -43,7 +43,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('permissions', $project)) {
|
||||
$class = $event->isView('permissions') ? '' : 'modal-ajax-form';
|
||||
$event->addAction('permissions', ['title' => 'permissions', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_permissions', ['id' => $project->getId()]), 'class' => $class]);
|
||||
$event->addAction('permissions', ['title' => 'permissions', 'url' => $this->path('admin_project_permissions', ['id' => $project->getId()]), 'class' => $class]);
|
||||
}
|
||||
|
||||
if ($event->countActions() > 0) {
|
||||
@@ -51,11 +51,11 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_activity')) {
|
||||
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
|
||||
$event->addActionToSubmenu('filter', 'activity', ['title' => 'activity.filter', 'url' => $this->path('admin_activity', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', ['customers[]' => $customer->getId(), 'projects[]' => $project->getId()])]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('create_export')) {
|
||||
@@ -79,7 +79,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
|
||||
if (\array_key_exists('token', $payload) && $this->isGranted('edit', $project) && $this->isGranted('create_project')) {
|
||||
$event->addAction(
|
||||
'copy',
|
||||
['title' => 'copy', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_duplicate', ['id' => $project->getId(), 'token' => $payload['token']])]
|
||||
['title' => 'copy', 'url' => $this->path('admin_project_duplicate', ['id' => $project->getId(), 'token' => $payload['token']])]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,14 +46,13 @@ final class TagSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet', ['tags' => $name])]);
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['title' => 'timesheet.filter', 'url' => $this->path('admin_timesheet', ['tags' => $name])]);
|
||||
}
|
||||
|
||||
if ($event->isIndexView() && $this->isGranted('delete_tag')) {
|
||||
$event->addAction('trash', [
|
||||
'url' => $this->path('delete_tag', ['id' => $id]),
|
||||
'class' => 'api-link text-red',
|
||||
'translation_domain' => 'actions',
|
||||
'attr' => [
|
||||
'data-event' => 'kimai.tagDelete kimai.tagUpdate',
|
||||
'data-method' => 'DELETE',
|
||||
|
||||
@@ -36,7 +36,7 @@ final class TeamSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('create_team')) {
|
||||
$event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId()]), 'title' => 'copy', 'translation_domain' => 'actions', 'class' => 'modal-ajax-form']);
|
||||
$event->addAction('copy', ['url' => $this->path('team_duplicate', ['id' => $team->getId()]), 'title' => 'copy', 'class' => 'modal-ajax-form']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ final class TeamSubscriber extends AbstractActionsSubscriber
|
||||
$event->addAction('trash', [
|
||||
'url' => $this->path('delete_team', ['id' => $team->getId()]),
|
||||
'class' => 'api-link text-red',
|
||||
'translation_domain' => 'actions',
|
||||
'attr' => [
|
||||
'data-event' => 'kimai.teamDelete kimai.teamUpdate',
|
||||
'data-method' => 'DELETE',
|
||||
|
||||
@@ -22,7 +22,7 @@ final class TimesheetsTeamSubscriber extends AbstractActionsSubscriber
|
||||
{
|
||||
if ($this->isGranted('create_other_timesheet')) {
|
||||
$event->addAction('create', ['title' => 'create', 'url' => $this->path('admin_timesheet_create'), 'class' => 'create-ts modal-ajax-form']);
|
||||
$event->addAction('multi-user', ['title' => 'create-timesheet-multiuser', 'translation_domain' => 'actions', 'url' => $this->path('admin_timesheet_create_multiuser'), 'class' => 'create-ts-mu modal-ajax-form', 'icon' => 'fas fa-user-plus']);
|
||||
$event->addAction('multi-user', ['title' => 'create-timesheet-multiuser', 'url' => $this->path('admin_timesheet_create_multiuser'), 'class' => 'create-ts-mu modal-ajax-form', 'icon' => 'fas fa-user-plus']);
|
||||
}
|
||||
|
||||
if ($this->isGranted('export_other_timesheet')) {
|
||||
|
||||
@@ -31,7 +31,7 @@ final class UserFormsSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $user)) {
|
||||
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'profile-stats', 'translation_domain' => 'actions']);
|
||||
$event->addAction('edit', ['url' => $this->path('user_profile_edit', ['username' => $user->getUserIdentifier()]), 'title' => 'profile-stats']);
|
||||
}
|
||||
if ($this->isGranted('preferences', $user)) {
|
||||
$event->addConfig($this->path('user_profile_preferences', ['username' => $user->getUserIdentifier()]));
|
||||
|
||||
@@ -40,7 +40,7 @@ final class UserSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('view', $user)) {
|
||||
$event->addAction('profile-stats', ['icon' => 'avatar', 'url' => $this->path('user_profile', ['username' => $user->getUserIdentifier()]), 'translation_domain' => 'actions', 'title' => 'profile-stats']);
|
||||
$event->addAction('profile-stats', ['icon' => 'avatar', 'url' => $this->path('user_profile', ['username' => $user->getUserIdentifier()]), 'title' => 'profile-stats']);
|
||||
$event->addDivider();
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ final class UserSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($user->isEnabled() && $this->isGranted('view_other_timesheet')) {
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['url' => $this->path('admin_timesheet', ['users[]' => $user->getId()]), 'title' => 'timesheet.filter', 'translation_domain' => 'actions']);
|
||||
$event->addActionToSubmenu('filter', 'timesheet', ['url' => $this->path('admin_timesheet', ['users[]' => $user->getId()]), 'title' => 'timesheet.filter']);
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_team')) {
|
||||
|
||||
@@ -28,7 +28,7 @@ final class AnnotationExtractor implements ExtractorInterface
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return ColumnDefinition[]
|
||||
* @return list<ColumnDefinition>
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array
|
||||
@@ -154,11 +154,7 @@ final class AnnotationExtractor implements ExtractorInterface
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($columns as $name => $definition) {
|
||||
if (null === $definition) {
|
||||
unset($columns[$name]);
|
||||
}
|
||||
}
|
||||
$columns = array_filter($columns, function ($value) { return $value !== null; });
|
||||
|
||||
return array_values($columns);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ final class MultiUpdateTable extends AbstractType
|
||||
|
||||
$builder->add('entities', HiddenType::class, [
|
||||
'required' => false,
|
||||
'attr' => ['class' => 'multi_update_ids']
|
||||
]);
|
||||
|
||||
$builder->get('entities')->addModelTransformer(
|
||||
|
||||
@@ -38,6 +38,8 @@ final class ActivityType extends AbstractType
|
||||
public function groupBy(Activity $activity, $key, $index): string
|
||||
{
|
||||
if (null === $activity->getProject()) {
|
||||
// this creates a optgroup with an empty title. previously this was null, which resulted in options without optgroup
|
||||
// and those are ordered by Tomselect at the top - so globals always came first, see #4674
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Configuration\MailConfiguration;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
@@ -31,7 +32,11 @@ final class KimaiMailer implements MailerInterface
|
||||
}
|
||||
|
||||
if (\count($message->getFrom()) === 0) {
|
||||
$message->from($this->configuration->getFromAddress());
|
||||
$fallback = $this->configuration->getFromAddress();
|
||||
if ($fallback === null) {
|
||||
throw new \RuntimeException('Missing email "from" address');
|
||||
}
|
||||
$message->from(new Address($fallback, 'Kimai'));
|
||||
}
|
||||
|
||||
$this->mailer->send($message);
|
||||
|
||||
@@ -17,7 +17,7 @@ use DateTimeInterface;
|
||||
final class MonthlyStatistic implements DateStatisticInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string|int, array<int<1, 12>, StatisticDate>>
|
||||
* @var array<string, array<int<1, 12>, StatisticDate>>
|
||||
*/
|
||||
private array $years = [];
|
||||
private DateTimeInterface $begin;
|
||||
@@ -50,7 +50,7 @@ final class MonthlyStatistic implements DateStatisticInterface
|
||||
$day = (int) $begin->format('d');
|
||||
|
||||
while ($tmp < $this->end) {
|
||||
$curYear = $tmp->format('Y');
|
||||
$curYear = (string) $tmp->format('Y');
|
||||
if (!isset($years[$curYear])) {
|
||||
$year = [];
|
||||
for ($i = 1; $i < 13; $i++) {
|
||||
@@ -67,7 +67,7 @@ final class MonthlyStatistic implements DateStatisticInterface
|
||||
}
|
||||
$tmp->modify('+1 month');
|
||||
}
|
||||
$this->years = $years;
|
||||
$this->years = $years; // @phpstan-ignore assign.propertyType
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\Project;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
|
||||
final class ProjectDuplicationService
|
||||
{
|
||||
@@ -53,7 +54,11 @@ final class ProjectDuplicationService
|
||||
$this->projectRateRepository->saveRate($newRate);
|
||||
}
|
||||
|
||||
$allActivities = $this->activityRepository->findByProject($project);
|
||||
$query = new ActivityQuery();
|
||||
$query->addProject($project);
|
||||
$query->setExcludeGlobals(true);
|
||||
|
||||
$allActivities = $this->activityRepository->getActivitiesForQuery($query);
|
||||
foreach ($allActivities as $activity) {
|
||||
$newActivity = clone $activity;
|
||||
$newActivity->setProject($newProject);
|
||||
|
||||
@@ -37,18 +37,6 @@ class ActivityRepository extends EntityRepository
|
||||
{
|
||||
use RepositorySearchTrait;
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return array<Activity>
|
||||
*/
|
||||
public function findByProject(Project $project): array
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->addProject($project);
|
||||
|
||||
return $this->getActivitiesForQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $activityIds
|
||||
* @return array<Activity>
|
||||
|
||||
@@ -64,7 +64,7 @@ trait RepositorySearchTrait
|
||||
$c = 0;
|
||||
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
|
||||
$and = $qb->expr()->andX();
|
||||
/** @var non-falsy-string&literal-string $alias */
|
||||
/** @var non-falsy-string&lowercase-string $alias */
|
||||
$alias = 'meta' . $a++;
|
||||
$paramName = 'metaName' . $i++;
|
||||
$paramValue = 'metaValue' . $c++;
|
||||
|
||||
@@ -108,8 +108,6 @@ final class TimesheetService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @return Timesheet
|
||||
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
|
||||
* @throws InvalidArgumentException for already persisted timesheets
|
||||
* @throws AccessDeniedException if user is not allowed to start timesheet
|
||||
@@ -233,11 +231,10 @@ final class TimesheetService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param string[] $groups
|
||||
* @throws ValidationFailedException
|
||||
*/
|
||||
private function validateTimesheet(Timesheet $timesheet, array $groups = []): void
|
||||
public function validateTimesheet(Timesheet $timesheet, array $groups = []): void
|
||||
{
|
||||
$errors = $this->validator->validate($timesheet, null, $groups);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Utils\LocaleFormatter;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\DeprecatedCallableInfo;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
@@ -41,7 +42,7 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
||||
new TwigFilter('date_short', [$this, 'dateShort']),
|
||||
new TwigFilter('date_time', [$this, 'dateTime']),
|
||||
// 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_full', [$this, 'dateTime'], ['deprecation_info' => new DeprecatedCallableInfo('Kimai', '2.0', 'date_time')]),
|
||||
new TwigFilter('date_format', [$this, 'dateFormat']),
|
||||
new TwigFilter('date_weekday', [$this, 'dateWeekday']),
|
||||
new TwigFilter('time', [$this, 'time']),
|
||||
|
||||
@@ -31,7 +31,7 @@ final class LocaleFormatter
|
||||
private ?NumberFormatter $moneyFormatter = null;
|
||||
private ?NumberFormatter $moneyFormatterNoCurrency = null;
|
||||
|
||||
public function __construct(private LocaleService $localeService, private string $locale)
|
||||
public function __construct(private readonly LocaleService $localeService, private readonly string $locale)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Validator\Constraints;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use App\Form\Model\MultiUserTimesheet;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
@@ -35,6 +36,10 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($value instanceof MultiUserTimesheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
$begin = $value->getBegin();
|
||||
$end = $value->getEnd();
|
||||
|
||||
|
||||
@@ -28,13 +28,13 @@ final class WorkingTimeCalculatorDay implements WorkingTimeCalculator
|
||||
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
|
||||
{
|
||||
return (int) match ($dateTime->format('N')) {
|
||||
'1' => $this->user->getPreferenceValue(self::WORK_HOURS_MONDAY, 0),
|
||||
'2' => $this->user->getPreferenceValue(self::WORK_HOURS_TUESDAY, 0),
|
||||
'3' => $this->user->getPreferenceValue(self::WORK_HOURS_WEDNESDAY, 0),
|
||||
'4' => $this->user->getPreferenceValue(self::WORK_HOURS_THURSDAY, 0),
|
||||
'5' => $this->user->getPreferenceValue(self::WORK_HOURS_FRIDAY, 0),
|
||||
'6' => $this->user->getPreferenceValue(self::WORK_HOURS_SATURDAY, 0),
|
||||
'7' => $this->user->getPreferenceValue(self::WORK_HOURS_SUNDAY, 0),
|
||||
'1' => $this->user->getPreferenceValue(self::WORK_HOURS_MONDAY, 0, false),
|
||||
'2' => $this->user->getPreferenceValue(self::WORK_HOURS_TUESDAY, 0, false),
|
||||
'3' => $this->user->getPreferenceValue(self::WORK_HOURS_WEDNESDAY, 0, false),
|
||||
'4' => $this->user->getPreferenceValue(self::WORK_HOURS_THURSDAY, 0, false),
|
||||
'5' => $this->user->getPreferenceValue(self::WORK_HOURS_FRIDAY, 0, false),
|
||||
'6' => $this->user->getPreferenceValue(self::WORK_HOURS_SATURDAY, 0, false),
|
||||
'7' => $this->user->getPreferenceValue(self::WORK_HOURS_SUNDAY, 0, false),
|
||||
default => throw new \Exception('Unknown day: ' . $dateTime->format('Y-m-d'))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use App\Model\Month as BaseMonth;
|
||||
*/
|
||||
final class Month extends BaseMonth
|
||||
{
|
||||
public function __construct(\DateTimeImmutable $month, private User $user)
|
||||
public function __construct(\DateTimeImmutable $month, private readonly User $user)
|
||||
{
|
||||
parent::__construct($month);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user