Release 2.0.27 (#4107)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
41
src/Event/WorkingTimeApproveMonthEvent.php
Normal file
41
src/Event/WorkingTimeApproveMonthEvent.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\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;
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
45
src/Form/YearByUserForm.php
Normal file
45
src/Form/YearByUserForm.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user