Release 2.32 (#5411)
* bump packages * dynamic invoice options * make sure that invoice previews can be detected * support for mpdf associated files * do not include any future times in work contract calculation * re-add username column in Excel spreadsheet * deactivate internal rate editing * show if plugin update exists * shorten name to Kimai only, without Time-Tracking * remove check for existing id in work contract * fix metafield already defined in search * helper methods to unlock months * new translation * send event on unlock month
This commit is contained in:
@@ -9,18 +9,13 @@
|
||||
|
||||
namespace App\API\Authentication;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
final class ApiTokenUpgradeBadge implements BadgeInterface
|
||||
{
|
||||
/**
|
||||
* @param string|null $plaintextApiToken
|
||||
* @param PasswordUpgraderInterface<User> $passwordUpgrader
|
||||
*/
|
||||
public function __construct(private ?string $plaintextApiToken, private PasswordUpgraderInterface $passwordUpgrader)
|
||||
public function __construct(private ?string $plaintextApiToken, private readonly PasswordUpgraderInterface $passwordUpgrader)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,9 +31,6 @@ final class ApiTokenUpgradeBadge implements BadgeInterface
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PasswordUpgraderInterface<User>
|
||||
*/
|
||||
public function getPasswordUpgrader(): PasswordUpgraderInterface
|
||||
{
|
||||
return $this->passwordUpgrader;
|
||||
|
||||
@@ -454,10 +454,12 @@ final class TranslationCommand extends Command
|
||||
|
||||
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
||||
$source = $unit->source;
|
||||
if (!isset($unit['resname'])) {
|
||||
if (!isset($unit['resname']) && $source !== null) {
|
||||
$unit['resname'] = $source;
|
||||
}
|
||||
$unit['id'] = $this->generateId($unit['resname']);
|
||||
if ($unit['resname'] !== null) {
|
||||
$unit['id'] = $this->generateId($unit['resname']); // @phpstan-ignore offsetAssign.valueType
|
||||
}
|
||||
}
|
||||
|
||||
$xmlDocument = new \DOMDocument('1.0');
|
||||
@@ -491,7 +493,7 @@ final class TranslationCommand extends Command
|
||||
);
|
||||
}
|
||||
$unit->target[0] = $translations[$key];
|
||||
$unit->target['state'] = 'needs-translation';
|
||||
$unit->target['state'] = 'needs-translation'; // @phpstan-ignore assign.propertyType
|
||||
$foundEmpty = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.31.0';
|
||||
public const VERSION = '2.32.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 23100;
|
||||
public const VERSION_ID = 23200;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -164,6 +164,7 @@ final class InvoiceController extends AbstractController
|
||||
try {
|
||||
$query->setCustomers([$customer]);
|
||||
$model = $this->service->createModel($query);
|
||||
$model->setPreview(true);
|
||||
|
||||
return $this->service->renderInvoice($model, $this->dispatcher, true);
|
||||
} catch (Exception $ex) {
|
||||
|
||||
@@ -34,11 +34,20 @@ final class PluginController extends AbstractController
|
||||
$page = new PageSetup('menu.plugin');
|
||||
$page->setHelp('plugins.html');
|
||||
|
||||
$all = $this->getPluginInformation($client, $cache);
|
||||
$bundles = [];
|
||||
foreach ($all as $item) {
|
||||
if ($item['bundle'] !== null) {
|
||||
$bundles[$item['bundle']] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('plugin/index.html.twig', [
|
||||
'page_setup' => $page,
|
||||
'plugins' => $plugins,
|
||||
'installed' => $installed,
|
||||
'extensions' => $this->getPluginInformation($client, $cache)
|
||||
'extensions' => $all,
|
||||
'bundles' => $bundles,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class InvoiceCreatedEvent extends Event
|
||||
{
|
||||
public function __construct(private Invoice $invoice, private InvoiceModel $model)
|
||||
public function __construct(private readonly Invoice $invoice, private readonly InvoiceModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ final class InvoiceDocumentsEvent extends Event
|
||||
$this->documents = $documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function setMaximumAllowedDocuments(int $max): void
|
||||
{
|
||||
$this->maximum = $max;
|
||||
|
||||
@@ -16,7 +16,11 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class InvoicePreRenderEvent extends Event
|
||||
{
|
||||
public function __construct(private InvoiceModel $model, private InvoiceDocument $document, private RendererInterface $renderer)
|
||||
public function __construct(
|
||||
private readonly InvoiceModel $model,
|
||||
private readonly InvoiceDocument $document,
|
||||
private readonly RendererInterface $renderer
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace App\Event;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @deprecated since 2.32 - use TranslatorInterface directly
|
||||
*/
|
||||
final class ThemeJavascriptTranslationsEvent extends Event
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -15,13 +15,19 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class WorkingTimeApproveMonthEvent extends Event
|
||||
{
|
||||
public function __construct(private User $user, private Month $month, private \DateTimeInterface $approvalDate, private User $approver)
|
||||
public function __construct(
|
||||
private readonly Month $month,
|
||||
private readonly User $approvedBy
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use getMonth()->getUser() instead)
|
||||
*/
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
return $this->getMonth()->getUser();
|
||||
}
|
||||
|
||||
public function getMonth(): Month
|
||||
@@ -29,13 +35,8 @@ final class WorkingTimeApproveMonthEvent extends Event
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
public function getApprovalDate(): \DateTimeInterface
|
||||
public function getApprovedBy(): User
|
||||
{
|
||||
return $this->approvalDate;
|
||||
}
|
||||
|
||||
public function getApprover(): User
|
||||
{
|
||||
return $this->approver;
|
||||
return $this->approvedBy;
|
||||
}
|
||||
}
|
||||
|
||||
34
src/Event/WorkingTimeUnlockMonthEvent.php
Normal file
34
src/Event/WorkingTimeUnlockMonthEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Entity\User;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class WorkingTimeUnlockMonthEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Month $month,
|
||||
private readonly User $unlockedBy
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getMonth(): Month
|
||||
{
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
public function getUnlockedBy(): User
|
||||
{
|
||||
return $this->unlockedBy;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,8 @@ final class SpreadsheetRenderer
|
||||
$columns[] = (new Column('fixedRate', new RateFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getFixedRate());
|
||||
}
|
||||
|
||||
$columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
$columns[] = (new Column('alias', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
$columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getUserIdentifier())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
$columns[] = (new Column('account_number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getAccountNumber());
|
||||
$columns[] = (new Column('customer', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
$columns[] = (new Column('project', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getName())->withColumnWidth(ColumnWidth::MEDIUM);
|
||||
|
||||
@@ -360,16 +360,13 @@ class TimesheetEditForm extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
$moneyOptions = ['currency' => $currency];
|
||||
|
||||
$builder
|
||||
->add('fixedRate', FixedRateType::class, [
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', HourlyRateType::class, [
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('internalRate', InternalRateType::class, [
|
||||
'currency' => $currency,
|
||||
]);
|
||||
->add('fixedRate', FixedRateType::class, $moneyOptions)
|
||||
->add('hourlyRate', HourlyRateType::class, $moneyOptions)
|
||||
//->add('internalRate', InternalRateType::class, $moneyOptions)
|
||||
;
|
||||
}
|
||||
|
||||
protected function addUser(FormBuilderInterface $builder, array $options): void
|
||||
|
||||
@@ -52,6 +52,11 @@ final class InvoiceModel
|
||||
private array $itemHydrator = [];
|
||||
private ?string $invoiceNumber = null;
|
||||
private bool $hideZeroTax = false;
|
||||
private bool $isPreview = false;
|
||||
/**
|
||||
* @var array<string, string|array<string|int, mixed>|null|bool|int|float>
|
||||
*/
|
||||
private array $options = [];
|
||||
|
||||
/**
|
||||
* @internal use InvoiceModelFactory
|
||||
@@ -68,6 +73,22 @@ final class InvoiceModel
|
||||
$this->addItemHydrator(new InvoiceItemDefaultHydrator());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array<string|int, mixed>|null|bool|int|float $value
|
||||
*/
|
||||
public function setOption(string $key, string|array|null|bool|int|float $value): void
|
||||
{
|
||||
$this->options[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|array<string|int, mixed>|null|bool|int|float>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getQuery(): ?InvoiceQuery
|
||||
{
|
||||
return $this->query;
|
||||
@@ -265,4 +286,14 @@ final class InvoiceModel
|
||||
{
|
||||
$this->hideZeroTax = $hideZeroTax;
|
||||
}
|
||||
|
||||
public function isPreview(): bool
|
||||
{
|
||||
return $this->isPreview;
|
||||
}
|
||||
|
||||
public function setPreview(bool $preview): void
|
||||
{
|
||||
$this->isPreview = $preview;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin
|
||||
{
|
||||
use PDFRendererTrait;
|
||||
|
||||
public function __construct(Environment $twig, private HtmlToPdfConverter $converter)
|
||||
public function __construct(Environment $twig, private readonly HtmlToPdfConverter $converter)
|
||||
{
|
||||
parent::__construct($twig);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin
|
||||
$context->setOption('margin_bottom', '8');
|
||||
|
||||
$content = $this->renderTwigTemplate($document, $model, ['pdfContext' => $context]);
|
||||
$content = $this->converter->convertToPdf($content, $context->getOptions());
|
||||
$content = $this->converter->convertToPdf($content, array_merge($model->getOptions(), $context->getOptions()));
|
||||
|
||||
return $this->createPdfResponse($content, $context);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ interface HtmlToPdfConverter
|
||||
* Returns the binary content of the PDF, which can be saved as file.
|
||||
* Throws an exception if conversion fails.
|
||||
*
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function convertToPdf(string $html, array $options = []): string;
|
||||
|
||||
@@ -18,22 +18,29 @@ use Mpdf\Output\Destination;
|
||||
|
||||
final class MPdfConverter implements HtmlToPdfConverter
|
||||
{
|
||||
public function __construct(private FileHelper $fileHelper, private string $cacheDirectory)
|
||||
public function __construct(
|
||||
private readonly FileHelper $fileHelper,
|
||||
private readonly string $cacheDirectory
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
* @return array<string, mixed|array<string, mixed>>
|
||||
*/
|
||||
private function sanitizeOptions(array $options): array
|
||||
{
|
||||
$configs = new ConfigVariables();
|
||||
$fonts = new FontVariables();
|
||||
$allowed = [
|
||||
'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top',
|
||||
'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts',
|
||||
];
|
||||
|
||||
$filtered = array_filter($options, function ($key) use ($allowed, $configs, $fonts): bool {
|
||||
$filtered = array_filter($options, function ($key): bool {
|
||||
$allowed = [
|
||||
'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top',
|
||||
'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts', 'associated_files'
|
||||
];
|
||||
if (!\in_array($key, $allowed)) {
|
||||
$configs = new ConfigVariables();
|
||||
if (!\array_key_exists($key, $configs->getDefaults())) {
|
||||
$fonts = new FontVariables();
|
||||
|
||||
return \array_key_exists($key, $fonts->getDefaults());
|
||||
}
|
||||
}
|
||||
@@ -49,10 +56,7 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @throws \Mpdf\MpdfException
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
*/
|
||||
public function convertToPdf(string $html, array $options = []): string
|
||||
{
|
||||
@@ -93,17 +97,26 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<mixed>> $options
|
||||
* @return Mpdf
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
*/
|
||||
private function initMpdf(array $options): Mpdf
|
||||
{
|
||||
$options['fontDir'] = $this->getFontDirectories();
|
||||
$options['fontdata'] = $this->mergeFontData($options);
|
||||
|
||||
$associatedFiles = [];
|
||||
if (\array_key_exists('associated_files', $options) && \is_array($options['associated_files'])) {
|
||||
$associatedFiles = $options['associated_files'];
|
||||
unset($options['associated_files']);
|
||||
}
|
||||
|
||||
$mpdf = new Mpdf($options);
|
||||
$mpdf->creator = Constants::SOFTWARE;
|
||||
|
||||
if (\count($associatedFiles) > 0) {
|
||||
$mpdf->SetAssociatedFiles($associatedFiles);
|
||||
}
|
||||
|
||||
return $mpdf;
|
||||
}
|
||||
|
||||
@@ -120,8 +133,8 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<mixed>> $options
|
||||
* @return array<string, array<mixed>>
|
||||
* @param array<string, mixed|array<string, mixed>> $options
|
||||
* @return array<string, mixed|array<string, mixed>>
|
||||
*/
|
||||
private function mergeFontData(array $options): array
|
||||
{
|
||||
|
||||
@@ -15,12 +15,9 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
*/
|
||||
class ApiUserRepository implements UserLoaderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
public function __construct(private UserRepository $userRepository)
|
||||
public function __construct(private readonly UserRepository $userRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ trait RepositorySearchTrait
|
||||
$alias = 'meta' . $a++;
|
||||
$paramName = 'metaName' . $i++;
|
||||
$paramValue = 'metaValue' . $c++;
|
||||
$subqueryName = 'metaNotExists' . $metaName;
|
||||
|
||||
if ($metaValue === '*') {
|
||||
$qb->leftJoin($rootAlias . '.meta', $alias);
|
||||
@@ -76,7 +77,7 @@ trait RepositorySearchTrait
|
||||
$and->add($qb->expr()->isNotNull($alias . '.value'));
|
||||
} elseif ($metaValue === '~') {
|
||||
$and->add(
|
||||
\sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias)
|
||||
\sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id)', $subqueryName, $this->getMetaFieldClass(), $subqueryName, $subqueryName, $this->getMetaFieldName(), $rootAlias)
|
||||
);
|
||||
} elseif ($metaValue === '' || $metaValue === null) {
|
||||
$qb->leftJoin($rootAlias . '.meta', $alias);
|
||||
@@ -86,7 +87,7 @@ trait RepositorySearchTrait
|
||||
$qb->expr()->eq($alias . '.name', ':' . $paramName),
|
||||
$qb->expr()->isNull($alias . '.value')
|
||||
),
|
||||
\sprintf('NOT EXISTS(SELECT metaNotExists FROM %s metaNotExists WHERE metaNotExists.%s = %s.id)', $this->getMetaFieldClass(), $this->getMetaFieldName(), $rootAlias)
|
||||
\sprintf('NOT EXISTS(SELECT %s FROM %s %s WHERE %s.%s = %s.id)', $subqueryName, $this->getMetaFieldClass(), $subqueryName, $subqueryName, $this->getMetaFieldName(), $rootAlias)
|
||||
)
|
||||
);
|
||||
$qb->setParameter($paramName, $metaName);
|
||||
|
||||
@@ -35,7 +35,6 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* @extends EntityRepository<User>
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
* @template-implements UserProviderInterface<User>
|
||||
*/
|
||||
class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface
|
||||
|
||||
@@ -21,18 +21,10 @@ class WorkingTimeRepository extends EntityRepository
|
||||
{
|
||||
private bool $pendingUpdate = false;
|
||||
|
||||
public function deleteWorkingTime(WorkingTime $workingTime): void
|
||||
public function scheduleWorkingTimeDelete(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($workingTime);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
public function saveWorkingTime(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($workingTime);
|
||||
$entityManager->flush();
|
||||
$this->pendingUpdate = true;
|
||||
$this->getEntityManager()->remove($workingTime);
|
||||
}
|
||||
|
||||
public function scheduleWorkingTimeUpdate(WorkingTime $workingTime): void
|
||||
|
||||
@@ -19,7 +19,6 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
* @template-implements UserProviderInterface<User>
|
||||
*/
|
||||
final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
@@ -29,7 +28,7 @@ final class KimaiUserProvider implements UserProviderInterface, PasswordUpgrader
|
||||
/**
|
||||
* @param iterable<UserProviderInterface<User>> $providers
|
||||
*/
|
||||
public function __construct(private iterable $providers, private SystemConfiguration $configuration)
|
||||
public function __construct(private readonly iterable $providers, private readonly SystemConfiguration $configuration)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -166,12 +166,7 @@ final class DateTimeFactory
|
||||
return $date->modify('23:59:59');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $format
|
||||
* @param null|string $datetime
|
||||
* @return bool|DateTime
|
||||
*/
|
||||
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now'): bool|DateTime
|
||||
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now'): false|DateTime
|
||||
{
|
||||
return DateTime::createFromFormat($format, $datetime ?? 'now', $this->getTimezone());
|
||||
}
|
||||
|
||||
@@ -24,15 +24,17 @@ use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class ThemeExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private TranslatorInterface $translator, private SystemConfiguration $configuration, private Security $security)
|
||||
public function __construct(
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly Security $security
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Environment $environment
|
||||
* @param string $eventName
|
||||
* @param array<string, mixed> $payload
|
||||
* @return ThemeEvent
|
||||
*/
|
||||
public function trigger(Environment $environment, string $eventName, array $payload = []): ThemeEvent
|
||||
{
|
||||
@@ -61,7 +63,7 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
public function getJavascriptTranslations(): array
|
||||
{
|
||||
$event = new ThemeJavascriptTranslationsEvent();
|
||||
$event = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
|
||||
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
@@ -97,7 +99,7 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
public function generateTitle(?string $prefix = null, string $delimiter = ' – '): string
|
||||
{
|
||||
return ($prefix ?? '') . Constants::SOFTWARE . $delimiter . $this->translator->trans('time_tracking', [], 'messages');
|
||||
return ($prefix ?? '') . Constants::SOFTWARE;
|
||||
}
|
||||
|
||||
public function colorize(?string $color, ?string $identifier = null): string
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use App\Event\WorkingTimeApproveMonthEvent;
|
||||
use App\Event\WorkingTimeQueryStatsEvent;
|
||||
use App\Event\WorkingTimeUnlockMonthEvent;
|
||||
use App\Event\WorkingTimeYearEvent;
|
||||
use App\Event\WorkingTimeYearSummaryEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
@@ -155,12 +156,14 @@ final class WorkingTimeService
|
||||
$dayDate = $day->getDay();
|
||||
$result = new WorkingTime($user, $dayDate);
|
||||
|
||||
if (($firstDay === null || $firstDay <= $dayDate) && ($lastDay === null || $lastDay >= $dayDate)) {
|
||||
$result->setExpectedTime($calculator->getWorkHoursForDay($dayDate));
|
||||
}
|
||||
if ($dayDate <= $until) {
|
||||
if (($firstDay === null || $firstDay <= $dayDate) && ($lastDay === null || $lastDay >= $dayDate)) {
|
||||
$result->setExpectedTime($calculator->getWorkHoursForDay($dayDate));
|
||||
}
|
||||
|
||||
if (\array_key_exists($key, $stats)) {
|
||||
$result->setActualTime($stats[$key]);
|
||||
if (\array_key_exists($key, $stats)) {
|
||||
$result->setActualTime($stats[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$day->setWorkingTime($result);
|
||||
@@ -181,6 +184,7 @@ final class WorkingTimeService
|
||||
return $year->getMonth($monthDate);
|
||||
}
|
||||
|
||||
// deprecated 3.0 remove $user, fetch from $month->getUser() instead
|
||||
public function approveMonth(User $user, Month $month, \DateTimeInterface $approvalDate, User $approvedBy): void
|
||||
{
|
||||
foreach ($month->getDays() as $day) {
|
||||
@@ -189,10 +193,6 @@ final class WorkingTimeService
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($workingTime->getId() !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($month->isLocked() || $workingTime->isApproved()) {
|
||||
continue;
|
||||
}
|
||||
@@ -205,15 +205,38 @@ final class WorkingTimeService
|
||||
|
||||
$this->workingTimeRepository->persistScheduledWorkingTimes();
|
||||
|
||||
// $user = $month->getUser();
|
||||
$user->setPreferenceValue(self::LATEST_APPROVAL_PREF, $this->workingTimeRepository->getLatestApprovalDate($user)?->format(self::LATEST_APPROVAL_FORMAT));
|
||||
$this->userRepository->saveUser($user);
|
||||
|
||||
$this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($user, $month, $approvalDate, $approvedBy));
|
||||
$this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($month, $approvedBy));
|
||||
}
|
||||
|
||||
public function unlockMonth(Month $month, User $unlockedBy): void
|
||||
{
|
||||
foreach ($month->getDays() as $day) {
|
||||
$workingTime = $day->getWorkingTime();
|
||||
if ($workingTime === null || $workingTime->getId() === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$workingTime->isApproved()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->workingTimeRepository->scheduleWorkingTimeDelete($workingTime);
|
||||
}
|
||||
|
||||
$this->workingTimeRepository->persistScheduledWorkingTimes();
|
||||
|
||||
$user = $month->getUser();
|
||||
$user->setPreferenceValue(self::LATEST_APPROVAL_PREF, $this->workingTimeRepository->getLatestApprovalDate($user)?->format(self::LATEST_APPROVAL_FORMAT));
|
||||
$this->userRepository->saveUser($user);
|
||||
|
||||
$this->eventDispatcher->dispatch(new WorkingTimeUnlockMonthEvent($month, $unlockedBy));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeInterface $year
|
||||
* @param User $user
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function getYearStatistics(\DateTimeInterface $year, User $user): array
|
||||
|
||||
Reference in New Issue
Block a user