Release 2.0.27 (#4107)

This commit is contained in:
Kevin Papst
2023-07-04 17:01:29 +02:00
committed by GitHub
parent 6a99ad41ca
commit 651f812da8
62 changed files with 506 additions and 427 deletions

View File

@@ -52,7 +52,7 @@ final class ReloadCommand extends Command
{
$io = new SymfonyStyle($input, $output);
$io->title('Reloading configurations ...');
$io->text('Validating config file syntax ...');
// many users execute the bin/console command from arbitrary locations
$path = getcwd();

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.0.26';
public const VERSION = '2.0.27';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20026;
public const VERSION_ID = 20027;
/**
* The software name
*/

View File

@@ -122,17 +122,12 @@ abstract class AbstractController extends BaseAbstractController implements Serv
* Adds an "error" flash message to the stack.
*
* @param string $translationKey
* @param array<string, string>|string $reason passing an array is deprecated
* @param string $reason
* @return void
* @throws \Exception
*/
protected function flashError(string $translationKey, array|string $reason = ''): void
protected function flashError(string $translationKey, string $reason = ''): void
{
if (\is_array($reason)) {
@trigger_error('Calling "flashError" with an array $reason is deprecated and will be removed soon. Refactor and pass a string instead.', E_USER_DEPRECATED);
$reason = \array_key_exists('%reason%', $reason) ? $reason['%reason%'] : '';
}
$this->addFlashTranslated('error', $translationKey, ['%reason%' => $reason]);
}

View File

@@ -47,7 +47,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
* Controller used to manage activities.
*/
#[Route(path: '/admin/activity')]
#[IsGranted(new Expression("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')"))]
final class ActivityController extends AbstractController
{
public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService)
@@ -56,6 +55,7 @@ final class ActivityController extends AbstractController
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_activity', methods: ['GET'])]
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_activity_paginated', methods: ['GET'])]
#[IsGranted(new Expression("is_granted('listing', 'activity')"))]
public function indexAction(int $page, Request $request): Response
{
$query = new ActivityQuery();
@@ -329,7 +329,11 @@ final class ActivityController extends AbstractController
$this->activityService->updateActivity($activity);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
if ($this->isGranted('view', $activity)) {
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
} else {
return new Response();
}
} catch (Exception $ex) {
$this->flashUpdateException($ex);
}
@@ -389,6 +393,7 @@ final class ActivityController extends AbstractController
}
#[Route(path: '/export', name: 'activity_export', methods: ['GET'])]
#[IsGranted(new Expression("is_granted('listing', 'activity')"))]
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
{
$query = new ActivityQuery();

View File

@@ -47,10 +47,9 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
* Controller used to manage customer in the admin part of the site.
* Controller used to manage customers.
*/
#[Route(path: '/admin/customer')]
#[IsGranted(new Expression("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')"))]
final class CustomerController extends AbstractController
{
public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher)
@@ -59,6 +58,7 @@ final class CustomerController extends AbstractController
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_customer', methods: ['GET'])]
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_customer_paginated', methods: ['GET'])]
#[IsGranted(new Expression("is_granted('listing', 'customer')"))]
public function indexAction(int $page, Request $request): Response
{
$query = new CustomerQuery();
@@ -452,6 +452,7 @@ final class CustomerController extends AbstractController
}
#[Route(path: '/export', name: 'customer_export', methods: ['GET'])]
#[IsGranted(new Expression("is_granted('listing', 'customer')"))]
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
{
$query = new CustomerQuery();
@@ -492,7 +493,11 @@ final class CustomerController extends AbstractController
return $this->redirectToRouteAfterCreate('customer_details', ['id' => $customer->getId()]);
}
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
if ($this->isGranted('view', $customer)) {
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
} else {
return new Response();
}
} catch (\Exception $ex) {
$this->handleFormUpdateException($ex, $editForm);
}

View File

@@ -433,7 +433,7 @@ final class InvoiceController extends AbstractController
$table->addColumn('calculator', ['class' => 'd-none', 'orderBy' => false, 'title' => 'invoice_calculator', 'translation_domain' => 'invoice-calculator']);
$table->addColumn('renderer', ['class' => 'd-none', 'orderBy' => false, 'title' => 'invoice_renderer', 'translation_domain' => 'invoice-renderer']);
$table->addColumn('language', ['class' => 'd-none text-nowrap', 'orderBy' => false]);
$table->addColumn('actions', ['class' => 'actions', 'orderBy' => false]);
$table->addColumn('actions', ['class' => 'actions']);
$page = $this->createPageSetup('admin_invoice_template.title');
$page->setDataTable($table);

View File

@@ -54,7 +54,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
* Controller used to manage projects.
*/
#[Route(path: '/admin/project')]
#[IsGranted(new Expression("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')"))]
final class ProjectController extends AbstractController
{
public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService)
@@ -63,6 +62,7 @@ final class ProjectController extends AbstractController
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_project', methods: ['GET'])]
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_project_paginated', methods: ['GET'])]
#[IsGranted(new Expression("is_granted('listing', 'project')"))]
public function indexAction(int $page, Request $request): Response
{
$query = new ProjectQuery();
@@ -435,7 +435,11 @@ final class ProjectController extends AbstractController
$this->projectService->updateProject($project);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
if ($this->isGranted('view', $project)) {
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
} else {
return new Response();
}
} catch (\Exception $ex) {
$this->flashUpdateException($ex);
}
@@ -512,6 +516,7 @@ final class ProjectController extends AbstractController
}
#[Route(path: '/export', name: 'project_export', methods: ['GET'])]
#[IsGranted(new Expression("is_granted('listing', 'project')"))]
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
{
$query = new ProjectQuery();

View File

@@ -789,7 +789,9 @@ final class Configuration implements ConfigurationInterface
->defaultValue('Login with SAML')
->end()
->scalarNode('provider')
->defaultNull()
// the "default" was only added, to prevent support requests by people who did not
// adjust their config between 1.x and 2.0
->defaultValue('default')
->end()
->arrayNode('roles')
->addDefaultsIfNotSet()

View File

@@ -11,7 +11,7 @@ namespace App\Doctrine;
/**
* Used to identify EventSubscribers, that work upon EntityManager events and listen on data changes.
* These Subscribers will deactivated on batch imports, for performance gains and reduced DB queries.
* These Subscribers will be deactivated on batch imports, for performance gains and reduced DB queries.
*/
interface DataSubscriberInterface
{

View File

@@ -13,17 +13,17 @@ use App\Entity\User;
/**
* This event is triggered for every action:
* - once per side load for table actions
* - once for every row item
* - once per side load for page actions
* - once for every entity item (table row)
*
* @property array{'actions': array, 'view': string} $payload
*/
class PageActionsEvent extends ThemeEvent
{
private string $action;
private string $view;
private int $divider = 0;
private ?string $locale = null;
public function __construct(User $user, array $payload, string $action, string $view)
public function __construct(User $user, array $payload, private string $action, private string $view)
{
// only for BC reasons, do not access it directly!
if (!\array_key_exists('actions', $payload)) {
@@ -34,8 +34,6 @@ class PageActionsEvent extends ThemeEvent
$payload['view'] = $view;
}
parent::__construct($user, $payload);
$this->action = $action;
$this->view = $view;
}
public function getActionName(): string
@@ -101,7 +99,7 @@ class PageActionsEvent extends ThemeEvent
public function hasSubmenu(string $submenu): bool
{
if (!\array_key_exists($submenu, $this->payload['actions'])) {
if (!$this->hasAction($submenu)) {
return false;
}
@@ -110,7 +108,7 @@ class PageActionsEvent extends ThemeEvent
public function addActionToSubmenu(string $submenu, string $key, array $action): void
{
if (\array_key_exists($submenu, $this->payload['actions'])) {
if ($this->hasAction($submenu)) {
if (!\array_key_exists('children', $this->payload['actions'][$submenu])) {
$this->payload['actions'][$submenu]['children'] = [];
}
@@ -125,14 +123,14 @@ class PageActionsEvent extends ThemeEvent
public function addAction(string $key, array $action): void
{
if (!\array_key_exists($key, $this->payload['actions'])) {
if (!$this->hasAction($key)) {
$this->payload['actions'][$key] = $action;
}
}
public function removeAction(string $key): void
{
if (\array_key_exists($key, $this->payload['actions'])) {
if ($this->hasAction($key)) {
unset($this->payload['actions'][$key]);
}
}
@@ -153,6 +151,11 @@ class PageActionsEvent extends ThemeEvent
$this->addAction('create', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' : ''), 'title' => 'create', 'accesskey' => 'a']);
}
public function addEdit(string $url, bool $modal = true): void
{
$this->addAction('edit', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' : ''), 'translation_domain' => 'actions', 'title' => 'edit']);
}
/**
* Link to a configuration section.
*

View File

@@ -16,9 +16,9 @@ use Symfony\Contracts\EventDispatcher\Event;
final class ReportingEvent extends Event
{
/**
* @var ReportInterface[]
* @var array<string, ReportInterface>
*/
private $reports = [];
private array $reports = [];
public function __construct(private User $user)
{
@@ -37,7 +37,7 @@ final class ReportingEvent extends Event
}
/**
* @return ReportInterface[]
* @return array<ReportInterface>
*/
public function getReports(): array
{

View File

@@ -23,11 +23,12 @@ class ThemeEvent extends Event
public const CONTENT_AFTER = 'app.theme.content_after';
private string $content = '';
protected mixed $payload;
public function __construct(private ?User $user = null, mixed $payload = null)
/**
* @param array<string, mixed|array<mixed>> $payload
*/
public function __construct(private ?User $user = null, protected array $payload = [])
{
$this->payload = $payload;
}
public function getUser(): ?User
@@ -47,18 +48,11 @@ class ThemeEvent extends Event
return $this;
}
public function getPayload(): mixed
/**
* @return array<string, mixed|array<mixed>>
*/
public function getPayload(): array
{
return $this->payload;
}
/**
* @deprecated since 2.0.19, will be removed with 2.1
*/
public function setPayload(mixed $payload): void
{
@trigger_error('ThemeEvent::setPayload() is deprecated, use AbstractActionsSubscriber instead.', E_USER_DEPRECATED);
$this->payload = $payload;
}
}

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\Event;
use App\Entity\User;
use App\WorkingTime\Model\Month;
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 getUser(): User
{
return $this->user;
}
public function getMonth(): Month
{
return $this->month;
}
public function getApprovalDate(): \DateTimeInterface
{
return $this->approvalDate;
}
public function getApprover(): User
{
return $this->approver;
}
}

View File

@@ -33,8 +33,7 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('edit', $timesheet)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
$event->addEdit($this->path($routeEdit, ['id' => $timesheet->getId()]), !$event->isView('edit'));
}
if ($this->isGranted('duplicate', $timesheet)) {

View File

@@ -35,8 +35,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('edit', $activity)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity_edit', ['id' => $activity->getId()]), 'class' => $class]);
$event->addEdit($this->path('admin_activity_edit', ['id' => $activity->getId()]), !$event->isView('edit'));
}
if ($this->isGranted('permissions', $activity)) {

View File

@@ -38,8 +38,7 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('edit', $customer)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_customer_edit', ['id' => $customer->getId()]), 'class' => $class]);
$event->addEdit($this->path('admin_customer_edit', ['id' => $customer->getId()]), !$event->isView('edit'));
}
if ($this->isGranted('permissions', $customer)) {

View File

@@ -35,7 +35,7 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
$allowCustomer = $this->isGranted('access', $invoice->getCustomer());
if ($allowCustomer && $allowCreate) {
$event->addAction('edit', ['url' => $this->path('admin_invoice_edit', ['id' => $invoice->getId()]), 'class' => 'modal-ajax-form']);
$event->addEdit($this->path('admin_invoice_edit', ['id' => $invoice->getId()]));
}
if ($allowCustomer && $allowView) {

View File

@@ -31,7 +31,7 @@ final class InvoiceTemplateSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('manage_invoice_template')) {
$event->addAction('edit', ['url' => $this->path('admin_invoice_template_edit', ['id' => $template->getId()]), 'class' => 'modal-ajax-form']);
$event->addEdit($this->path('admin_invoice_template_edit', ['id' => $template->getId()]));
$event->addAction('copy', ['url' => $this->path('admin_invoice_template_copy', ['id' => $template->getId()]), 'class' => 'modal-ajax-form']);
$event->addDelete($this->path('admin_invoice_template_delete', ['id' => $template->getId(), 'csrfToken' => $payload['token']]), false);
}

View File

@@ -38,8 +38,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('edit', $project)) {
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_edit', ['id' => $project->getId()]), 'class' => $class]);
$event->addEdit($this->path('admin_project_edit', ['id' => $project->getId()]), !$event->isView('edit'));
}
if ($this->isGranted('permissions', $project)) {

View File

@@ -42,8 +42,7 @@ final class TagSubscriber extends AbstractActionsSubscriber
}
if ($this->isGranted('manage_tag')) {
$class = ($event->isView('edit') ? '' : 'modal-ajax-form');
$event->addAction('edit', ['url' => $this->path('tags_edit', ['id' => $id]), 'class' => $class]);
$event->addEdit($this->path('tags_edit', ['id' => $id]));
}
if ($this->isGranted('view_other_timesheet')) {

View File

@@ -32,7 +32,7 @@ final class TeamSubscriber extends AbstractActionsSubscriber
if ($this->isGranted('edit', $team)) {
if (!$event->isView('edit')) {
$event->addAction('edit', ['url' => $this->path('admin_team_edit', ['id' => $team->getId()]), 'title' => 'action.edit']);
$event->addEdit($this->path('admin_team_edit', ['id' => $team->getId()]));
}
if ($this->isGranted('create_team')) {

View File

@@ -9,40 +9,15 @@
namespace App\Form;
use App\Form\Type\UserType;
use App\Form\Type\YearPickerType;
use App\Reporting\YearByUser\YearByUser;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @internal
*/
final class ContractByUserForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
public function getParent(): ?string
{
$builder->add('date', YearPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
]);
if ($options['include_user']) {
$builder->add('user', UserType::class, ['width' => false]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => YearByUser::class,
'timezone' => date_default_timezone_get(),
'start_date' => new \DateTime(),
'include_user' => false,
'csrf_protection' => false,
'method' => 'GET',
]);
return YearByUserForm::class;
}
}

View File

@@ -0,0 +1,45 @@
<?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\Form;
use App\Form\Type\UserType;
use App\Form\Type\YearPickerType;
use App\Reporting\YearByUser\YearByUser;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class YearByUserForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('date', YearPickerType::class, [
'model_timezone' => $options['timezone'],
'view_timezone' => $options['timezone'],
'start_date' => $options['start_date'],
]);
if ($options['include_user']) {
$builder->add('user', UserType::class, ['width' => false]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => YearByUser::class,
'timezone' => date_default_timezone_get(),
'start_date' => new \DateTime(),
'include_user' => false,
'csrf_protection' => false,
'method' => 'GET',
]);
}
}

View File

@@ -23,17 +23,10 @@ abstract class AbstractRenderer
/**
* @return string[]
*/
abstract protected function getFileExtensions();
abstract protected function getFileExtensions(): array;
/**
* @return string
*/
abstract protected function getContentType();
abstract protected function getContentType(): string;
/**
* @param InvoiceDocument $document
* @return bool
*/
public function supports(InvoiceDocument $document): bool
{
foreach ($this->getFileExtensions() as $extension) {
@@ -50,12 +43,7 @@ abstract class AbstractRenderer
return (string) new InvoiceFilename($model);
}
/**
* @param mixed $file
* @param string $filename
* @return BinaryFileResponse
*/
protected function getFileResponse($file, $filename)
protected function getFileResponse(mixed $file, string $filename): BinaryFileResponse
{
$response = new BinaryFileResponse($file);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);

View File

@@ -25,12 +25,8 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
{
/**
* Saves the Spreadhseet and returns the filename.
*
* @param Spreadsheet $spreadsheet
* @return string
* @throws \Exception
*/
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet);
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet): string;
/**
* Render the given InvoiceDocument with the data from the InvoiceModel.

View File

@@ -25,7 +25,7 @@ final class XlsxRenderer extends AbstractSpreadsheetRenderer implements Renderer
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
}
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
{
$filename = @tempnam(sys_get_temp_dir(), 'kimai-invoice-xlsx');
if (false === $filename) {

View File

@@ -11,8 +11,18 @@ namespace App\Model;
class PermissionSection implements PermissionSectionInterface
{
public function __construct(private string $title, private string $filter)
/** @var array<string> */
private array $filter;
/**
* @param string|array<string> $filter
*/
public function __construct(private string $title, string|array $filter)
{
if (!\is_array($filter)) {
$filter = [$filter];
}
$this->filter = $filter;
}
public function getTitle(): string
@@ -22,6 +32,12 @@ class PermissionSection implements PermissionSectionInterface
public function filter(string $permission): bool
{
return str_contains($permission, $this->filter);
foreach ($this->filter as $filter) {
if (str_contains($permission, $filter)) {
return true;
}
}
return false;
}
}

View File

@@ -95,6 +95,13 @@ class ProjectStatisticService
->andWhere($qb->expr()->eq('p.visible', true))
->andWhere($qb->expr()->eq('c.visible', true))
->andWhere($qb->expr()->not($qb->expr()->exists($qb2)))
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('p.start'),
$qb->expr()->lte('p.start', ':project_start')
)
)
->setParameter('project_start', $now, Types::DATETIME_MUTABLE)
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('p.end'),

View File

@@ -19,6 +19,8 @@ use Doctrine\ORM\EntityRepository;
*/
class WorkingTimeRepository extends EntityRepository
{
private bool $pendingUpdate = false;
public function deleteWorkingTime(WorkingTime $workingTime): void
{
$entityManager = $this->getEntityManager();
@@ -35,12 +37,15 @@ class WorkingTimeRepository extends EntityRepository
public function scheduleWorkingTimeUpdate(WorkingTime $workingTime): void
{
$this->pendingUpdate = true;
$this->getEntityManager()->persist($workingTime);
}
public function persistScheduledWorkingTimes(): void
{
$this->getEntityManager()->flush();
if ($this->pendingUpdate) {
$this->getEntityManager()->flush();
}
}
/**

View File

@@ -79,7 +79,7 @@ class SamlAuthenticator extends AbstractAuthenticator
$oneLoginAuth->processResponse();
// $this->logger->debug('Received SAML response: ' . $oneLoginAuth->getLastResponseXML());
// file_put_contents(__DIR__ . '/../../var/log/saml.xml', $oneLoginAuth->getLastResponseXML());
if ($oneLoginAuth->getErrors()) {
throw new AuthenticationException($oneLoginAuth->getLastErrorReason());

View File

@@ -17,7 +17,7 @@ use App\Event\ThemeEvent;
use App\Event\ThemeJavascriptTranslationsEvent;
use App\Utils\Color;
use App\Utils\FormFormatConverter;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
@@ -25,22 +25,20 @@ use Twig\Extension\RuntimeExtensionInterface;
final class ThemeExtension implements RuntimeExtensionInterface
{
public function __construct(private EventDispatcherInterface $eventDispatcher, private TranslatorInterface $translator, private SystemConfiguration $configuration)
public function __construct(private EventDispatcherInterface $eventDispatcher, private TranslatorInterface $translator, private SystemConfiguration $configuration, private Security $security)
{
}
/**
* @param Environment $environment
* @param string $eventName
* @param mixed|null $payload
* @param array<string, mixed> $payload
* @return ThemeEvent
*/
public function trigger(Environment $environment, string $eventName, $payload = null): ThemeEvent
public function trigger(Environment $environment, string $eventName, array $payload = []): ThemeEvent
{
/** @var AppVariable $app */
$app = $environment->getGlobals()['app'];
/** @var User $user */
$user = $app->getUser();
$user = $this->security->getUser();
$themeEvent = new ThemeEvent($user, $payload);

View File

@@ -30,6 +30,7 @@ final class EntityMultiRoleVoter extends Voter
'budget_time',
'budget_any',
'details',
'listing',
];
private const ALLOWED_SUBJECTS = [
'customer',
@@ -99,6 +100,13 @@ final class EntityMultiRoleVoter extends Voter
$permissions[] = 'time_teamlead';
$permissions[] = 'time_team';
}
if ($attribute === 'listing') {
$permissions[] = 'view';
$permissions[] = 'view_team';
$permissions[] = 'view_teamlead';
}
foreach ($permissions as $permission) {
if ($this->permissionManager->hasRolePermission($user, $permission . '_' . $suffix)) {
return true;

View File

@@ -18,9 +18,9 @@ use App\Model\Year as BaseYear;
*/
final class Year extends BaseYear
{
public function __construct(\DateTimeInterface $month, private User $user)
public function __construct(\DateTimeInterface $year, private User $user)
{
parent::__construct($month);
parent::__construct($year);
}
public function getUser(): User

View File

@@ -11,6 +11,7 @@ namespace App\WorkingTime;
use App\Entity\User;
use App\Entity\WorkingTime;
use App\Event\WorkingTimeApproveMonthEvent;
use App\Event\WorkingTimeYearEvent;
use App\Event\WorkingTimeYearSummaryEvent;
use App\Repository\TimesheetRepository;
@@ -94,10 +95,8 @@ final class WorkingTimeService
return $year->getMonth($monthDate);
}
public function approveMonth(Month $month, \DateTimeInterface $approvalDate, User $approver): void
public function approveMonth(User $user, Month $month, \DateTimeInterface $approvalDate, User $approver): void
{
$update = false;
foreach ($month->getDays() as $day) {
$workingTime = $day->getWorkingTime();
if ($workingTime === null) {
@@ -115,12 +114,11 @@ final class WorkingTimeService
$workingTime->setApprovedBy($approver);
$workingTime->setApprovedAt($approvalDate);
$this->workingTimeRepository->scheduleWorkingTimeUpdate($workingTime);
$update = true;
}
if ($update) {
$this->workingTimeRepository->persistScheduledWorkingTimes();
}
$this->workingTimeRepository->persistScheduledWorkingTimes();
$this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($user, $month, $approvalDate, $approver));
}
/**