Release 2.13 (#4659)
This commit is contained in:
@@ -44,10 +44,10 @@ final class ActivityController extends BaseApiController
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private ActivityRepository $repository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private ActivityRateRepository $activityRateRepository
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly ActivityRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ActivityRateRepository $activityRateRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ final class ActivityController extends BaseApiController
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_activity', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[IsGranted('view', 'activity')]
|
||||
public function getAction(Activity $activity): Response
|
||||
{
|
||||
$view = new View($activity, 200);
|
||||
|
||||
@@ -44,10 +44,10 @@ final class CustomerController extends BaseApiController
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private CustomerRepository $repository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private CustomerRateRepository $customerRateRepository
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly CustomerRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly CustomerRateRepository $customerRateRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ final class CustomerController extends BaseApiController
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_customer', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[IsGranted('view', 'customer')]
|
||||
public function getAction(Customer $customer): Response
|
||||
{
|
||||
$view = new View($customer, 200);
|
||||
|
||||
@@ -46,11 +46,11 @@ final class ProjectController extends BaseApiController
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private ProjectRepository $repository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private ProjectRateRepository $projectRateRepository,
|
||||
private ProjectService $projectService
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly ProjectRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ProjectRateRepository $projectRateRepository,
|
||||
private readonly ProjectService $projectService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ final class ProjectController extends BaseApiController
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_project', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[IsGranted('view', 'project')]
|
||||
public function getAction(Project $project): Response
|
||||
{
|
||||
$view = new View($project, 200);
|
||||
|
||||
@@ -54,11 +54,11 @@ final class TimesheetController extends BaseApiController
|
||||
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private TimesheetRepository $repository,
|
||||
private TagRepository $tagRepository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private TimesheetService $service
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly TimesheetRepository $repository,
|
||||
private readonly TagRepository $tagRepository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly TimesheetService $service
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ final class TimesheetController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||
{
|
||||
$query = new TimesheetQuery(false);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$seeAll = false;
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.12.0';
|
||||
public const VERSION = '2.13.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 21200;
|
||||
public const VERSION_ID = 21300;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -67,7 +67,7 @@ final class CustomerController extends AbstractController
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form = $this->getToolbarForm($query, $request);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
@@ -473,7 +473,7 @@ final class CustomerController extends AbstractController
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form = $this->getToolbarForm($query, $request);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
@@ -526,12 +526,12 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return FormInterface<CustomerQuery>
|
||||
*/
|
||||
private function getToolbarForm(CustomerQuery $query): FormInterface
|
||||
private function getToolbarForm(CustomerQuery $query, Request $request): FormInterface
|
||||
{
|
||||
return $this->createSearchForm(CustomerToolbarForm::class, $query, [
|
||||
'locale' => $request->getLocale(),
|
||||
'action' => $this->generateUrl('admin_customer', [
|
||||
'page' => $query->getPage(),
|
||||
])
|
||||
@@ -539,7 +539,6 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerComment $comment
|
||||
* @return FormInterface<CustomerComment>
|
||||
*/
|
||||
private function getCommentForm(CustomerComment $comment): FormInterface
|
||||
@@ -555,7 +554,6 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return FormInterface<Customer>
|
||||
*/
|
||||
private function createEditForm(Customer $customer): FormInterface
|
||||
|
||||
@@ -45,21 +45,28 @@ final class PluginController extends AbstractController
|
||||
private function getPluginInformation(HttpClientInterface $client, CacheInterface $cache): array
|
||||
{
|
||||
return $cache->get('kimai.marketplace_extensions', function (ItemInterface $item) use ($client) {
|
||||
$response = $client->request('GET', 'https://www.kimai.org/plugins.json');
|
||||
try {
|
||||
$response = $client->request('GET', 'https://www.kimai.org/plugins.json');
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return [];
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
|
||||
if ($json === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$item->expiresAfter(86400); // one day
|
||||
|
||||
return $response->toArray();
|
||||
} catch (\Exception $exception) {
|
||||
$this->logException($exception);
|
||||
$this->flashError('Could not download plugin information');
|
||||
}
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
|
||||
if ($json === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$item->expiresAfter(86400); // one day
|
||||
|
||||
return $response->toArray();
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
final class PasswordResetController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private UserService $userService,
|
||||
private SystemConfiguration $configuration
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly UserService $userService,
|
||||
private readonly SystemConfiguration $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ final class PasswordResetController extends AbstractController
|
||||
}
|
||||
|
||||
$username = $request->request->get('username');
|
||||
if (!\is_string($username) || trim($username) === '') {
|
||||
throw $this->createAccessDeniedException('Username cannot be empty');
|
||||
}
|
||||
|
||||
$user = $this->userService->findUserByUsernameOrEmail($username);
|
||||
|
||||
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
|
||||
@@ -18,7 +18,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
final class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private Security $security, private ContextHelper $helper)
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly ContextHelper $helper
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -134,19 +137,19 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
$menu = $event->getAdminMenu();
|
||||
|
||||
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
|
||||
$customers = new MenuItemModel('customer_admin', 'customers', 'admin_customer', [], 'customer');
|
||||
$customers = new MenuItemModel('customers', 'customers', 'admin_customer', [], 'customer');
|
||||
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);
|
||||
$menu->addChild($customers);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
|
||||
$projects = new MenuItemModel('project_admin', 'projects', 'admin_project', [], 'project');
|
||||
$projects = new MenuItemModel('projects', 'projects', 'admin_project', [], 'project');
|
||||
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'project_details', 'admin_project_edit', 'admin_project_delete']);
|
||||
$menu->addChild($projects);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_activity') || $auth->isGranted('view_teamlead_activity') || $auth->isGranted('view_team_activity')) {
|
||||
$activities = new MenuItemModel('activity_admin', 'activities', 'admin_activity', [], 'activity');
|
||||
$activities = new MenuItemModel('activities', 'activities', 'admin_activity', [], 'activity');
|
||||
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
|
||||
$menu->addChild($activities);
|
||||
}
|
||||
@@ -163,18 +166,18 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
$menu = $event->getSystemMenu();
|
||||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$users = new MenuItemModel('user_admin', 'users', 'admin_user', [], 'users');
|
||||
$users = new MenuItemModel('users', 'users', 'admin_user', [], 'users');
|
||||
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences', 'user_profile_2fa']);
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('role_permissions')) {
|
||||
$users = new MenuItemModel('admin_user_permissions', 'profile.roles', 'admin_user_permissions', [], 'permissions');
|
||||
$users = new MenuItemModel('roles', 'profile.roles', 'admin_user_permissions', [], 'permissions');
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_team')) {
|
||||
$teams = new MenuItemModel('user_team', 'teams', 'admin_team', [], 'team');
|
||||
$teams = new MenuItemModel('teams', 'teams', 'admin_team', [], 'team');
|
||||
$teams->setChildRoutes(['admin_team_create', 'admin_team_edit']);
|
||||
$menu->addChild($teams);
|
||||
}
|
||||
@@ -190,7 +193,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
if ($auth->isGranted('system_configuration')) {
|
||||
$systemConfig = new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], 'configuration');
|
||||
$systemConfig = new MenuItemModel('configurations', 'menu.system_configuration', 'system_configuration', [], 'configuration');
|
||||
$systemConfig->setChildRoutes(['system_configuration_update', 'system_configuration_section']);
|
||||
$menu->addChild($systemConfig);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Intl\Countries;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -22,9 +25,38 @@ final class CustomerToolbarForm extends AbstractType
|
||||
{
|
||||
use ToolbarFormTrait;
|
||||
|
||||
public function __construct(private readonly CustomerRepository $customerRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
|
||||
// fetch countries
|
||||
$qb = $this->customerRepository->createQueryBuilder('c');
|
||||
$qb
|
||||
->select('c.country')
|
||||
->distinct(true);
|
||||
|
||||
$countries = $qb->getQuery()->getSingleColumnResult();
|
||||
$choices = [];
|
||||
foreach ($countries as $country) {
|
||||
if (\is_string($country) && \is_string($options['locale'])) {
|
||||
$choices[$country] = Countries::getName($country, $options['locale']);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($choices) > 0) {
|
||||
$choices = array_flip($choices);
|
||||
ksort($choices);
|
||||
$builder->add('country', ChoiceType::class, [
|
||||
'label' => 'country',
|
||||
'choices' => $choices,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
@@ -37,6 +69,8 @@ final class CustomerToolbarForm extends AbstractType
|
||||
$resolver->setDefaults([
|
||||
'data_class' => CustomerQuery::class,
|
||||
'csrf_protection' => false,
|
||||
'locale' => locale_get_default(),
|
||||
]);
|
||||
$resolver->setAllowedTypes('locale', ['string']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ final class TagsType extends AbstractType
|
||||
private ?int $count = null;
|
||||
|
||||
public function __construct(
|
||||
private AuthorizationCheckerInterface $auth,
|
||||
private TagRepository $repository
|
||||
private readonly AuthorizationCheckerInterface $auth,
|
||||
private readonly TagRepository $repository
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
{
|
||||
private ?InvoiceModel $model = null;
|
||||
|
||||
public function __construct(private InvoiceRepository $repository, private SystemConfiguration $configuration)
|
||||
public function __construct(
|
||||
private readonly InvoiceRepository $repository,
|
||||
private readonly SystemConfiguration $configuration
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -33,9 +36,6 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInvoiceNumber(): string
|
||||
{
|
||||
$format = $this->configuration->find('invoice.number_format');
|
||||
@@ -68,6 +68,10 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
'm' => $invoiceDate->format('n'),
|
||||
'D' => $invoiceDate->format('d'),
|
||||
'd' => $invoiceDate->format('j'),
|
||||
'YY' => (int) $invoiceDate->format('Y') + $increaseBy,
|
||||
'yy' => (int) $invoiceDate->format('y') + $increaseBy,
|
||||
'MM' => (int) $invoiceDate->format('m') + $increaseBy,
|
||||
'DD' => (int) $invoiceDate->format('d') + $increaseBy,
|
||||
'date' => $invoiceDate->format('ymd'),
|
||||
'cc' => $this->repository->getCounterForCustomerAllTime($this->model->getCustomer()) + $increaseBy,
|
||||
'ccy' => $this->repository->getCounterForYear($invoiceDate, $this->model->getCustomer()) + $increaseBy,
|
||||
@@ -92,7 +96,7 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
do {
|
||||
$result = $numberGenerator->getNumber($increaseBy);
|
||||
$increaseBy++;
|
||||
} while ($this->repository->hasInvoice($result) && $loops++ < 99);
|
||||
} while ((int) $result < 0 || ($this->repository->hasInvoice($result) && $loops++ < 99));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
|
||||
// lowercase all font names, otherwise they cannot be loaded
|
||||
// see https://github.com/kimai/www.kimai.org/issues/280
|
||||
if (\array_key_exists('fonts', $options)) {
|
||||
if (\array_key_exists('fonts', $options) && \is_array($options['fonts'])) {
|
||||
$fonts = [];
|
||||
foreach ($options['fonts'] as $name => $values) {
|
||||
$fonts[strtolower($name)] = $values;
|
||||
|
||||
@@ -20,15 +20,15 @@ use Doctrine\ORM\Query\Expr\Join;
|
||||
|
||||
final class CustomerMonthlyProjectsRepository
|
||||
{
|
||||
public function __construct(private TimesheetRepository $repository, private EntityManagerInterface $entityManager)
|
||||
public function __construct(
|
||||
private readonly TimesheetRepository $repository,
|
||||
private readonly EntityManagerInterface $entityManager
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @param User[] $users
|
||||
* @param Customer|null $customer
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
|
||||
@@ -55,10 +55,6 @@ class CustomerRepository extends EntityRepository
|
||||
return $customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function saveCustomer(Customer $customer): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
@@ -75,7 +71,7 @@ class CustomerRepository extends EntityRepository
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
|
||||
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
|
||||
{
|
||||
$permissions = $this->getPermissionCriteria($qb, $user, $teams);
|
||||
if ($permissions->count() > 0) {
|
||||
@@ -124,9 +120,6 @@ class CustomerRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* Returns a query builder that is used for CustomerType and your own 'query_builder' option.
|
||||
*
|
||||
* @param CustomerFormTypeQuery $query
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder
|
||||
{
|
||||
@@ -177,6 +170,13 @@ class CustomerRepository extends EntityRepository
|
||||
->from(Customer::class, 'c')
|
||||
;
|
||||
|
||||
if ($query->getCountry() !== null) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->eq('c.country', ':country'))
|
||||
->setParameter('country', $query->getCountry())
|
||||
;
|
||||
}
|
||||
|
||||
foreach ($query->getOrderGroups() as $orderBy => $order) {
|
||||
switch ($orderBy) {
|
||||
case 'vat_id':
|
||||
|
||||
@@ -18,11 +18,24 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
|
||||
'phone', 'currency', 'address', 'contact', 'company', 'vat_id', 'budget', 'timeBudget', 'visible'
|
||||
];
|
||||
|
||||
private ?string $country = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDefaults([
|
||||
'orderBy' => 'name',
|
||||
'visibility' => VisibilityInterface::SHOW_VISIBLE,
|
||||
'country' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCountry(): ?string
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
public function setCountry(?string $country): void
|
||||
{
|
||||
$this->country = $country;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ use Symfony\Component\HttpFoundation\RequestStack;
|
||||
class SamlAuthFactory
|
||||
{
|
||||
public function __construct(
|
||||
private RequestStack $request,
|
||||
private SamlConfigurationInterface $configuration
|
||||
private readonly RequestStack $request,
|
||||
private readonly SamlConfigurationInterface $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +35,12 @@ class SamlAuthenticator extends AbstractAuthenticator
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private HttpUtils $httpUtils,
|
||||
private SamlAuthenticationSuccessHandler $successHandler,
|
||||
private SamlAuthenticationFailureHandler $failureHandler,
|
||||
private SamlAuthFactory $samlAuthFactory,
|
||||
private SamlProvider $samlProvider,
|
||||
private SamlConfigurationInterface $configuration
|
||||
private readonly HttpUtils $httpUtils,
|
||||
private readonly SamlAuthenticationSuccessHandler $successHandler,
|
||||
private readonly SamlAuthenticationFailureHandler $failureHandler,
|
||||
private readonly SamlAuthFactory $samlAuthFactory,
|
||||
private readonly SamlProvider $samlProvider,
|
||||
private readonly SamlConfigurationInterface $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
final class SamlBadge implements BadgeInterface
|
||||
{
|
||||
public function __construct(private SamlLoginAttributes $samlToken)
|
||||
public function __construct(private readonly SamlLoginAttributes $samlToken)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
|
||||
final class SamlLogoutSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private SamlAuthFactory $samlAuth)
|
||||
public function __construct(private readonly SamlAuthFactory $samlAuth)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ final class SamlLogoutSubscriber implements EventSubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
public function logout(LogoutEvent $event)
|
||||
public function logout(LogoutEvent $event): void
|
||||
{
|
||||
$token = $event->getToken();
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ final class SamlProvider
|
||||
* @param UserProviderInterface<User> $userProvider
|
||||
*/
|
||||
public function __construct(
|
||||
private UserRepository $repository,
|
||||
private UserProviderInterface $userProvider,
|
||||
private SamlConfigurationInterface $configuration
|
||||
private readonly UserRepository $repository,
|
||||
private readonly UserProviderInterface $userProvider,
|
||||
private readonly SamlConfigurationInterface $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ final class SamlProvider
|
||||
$user->setAuth(User::AUTH_SAML);
|
||||
}
|
||||
|
||||
private function getPropertyValue(SamlLoginAttributes $token, $attribute)
|
||||
private function getPropertyValue(SamlLoginAttributes $token, $attribute): string
|
||||
{
|
||||
$results = [];
|
||||
$attributes = $token->getAttributes();
|
||||
|
||||
@@ -29,11 +29,14 @@ final class RolePermissionManager
|
||||
private bool $isInitialized = false;
|
||||
|
||||
/**
|
||||
* @param PermissionService $service
|
||||
* @param array<string, array<string, bool>> $permissions as defined in kimai.yaml
|
||||
* @param array<string, bool> $permissionNames as defined in kimai.yaml
|
||||
*/
|
||||
public function __construct(private PermissionService $service, private array $permissions, private array $permissionNames)
|
||||
public function __construct(
|
||||
private readonly PermissionService $service,
|
||||
private array $permissions,
|
||||
private readonly array $permissionNames
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -47,11 +50,6 @@ final class RolePermissionManager
|
||||
$perm = (string) $item['permission'];
|
||||
$role = (string) $item['role'];
|
||||
|
||||
// these permissions may not be revoked at any time, because super admin would lose the ability to reactivate any permission
|
||||
if ($role === User::ROLE_SUPER_ADMIN && \array_key_exists($perm, self::SUPER_ADMIN_PERMISSIONS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\array_key_exists($role, $this->permissions)) {
|
||||
$this->permissions[$role] = [];
|
||||
}
|
||||
@@ -59,6 +57,7 @@ final class RolePermissionManager
|
||||
$this->permissions[$role][$perm] = (bool) $item['allowed'];
|
||||
}
|
||||
|
||||
// these permissions may not be revoked at any time, because super admin would lose the ability to reactivate any permission
|
||||
foreach (self::SUPER_ADMIN_PERMISSIONS as $perm => $value) {
|
||||
$this->permissions[User::ROLE_SUPER_ADMIN][$perm] = $value;
|
||||
}
|
||||
@@ -68,14 +67,9 @@ final class RolePermissionManager
|
||||
|
||||
/**
|
||||
* Only permissions which were registered through the Symfony configuration stack will be acknowledged here.
|
||||
*
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function isRegisteredPermission(string $permission): bool
|
||||
{
|
||||
$this->init();
|
||||
|
||||
return \array_key_exists($permission, $this->permissionNames);
|
||||
}
|
||||
|
||||
@@ -89,7 +83,7 @@ final class RolePermissionManager
|
||||
return false;
|
||||
}
|
||||
|
||||
return \array_key_exists($permission, $this->permissions[$role]) ? $this->permissions[$role][$permission] : false;
|
||||
return \array_key_exists($permission, $this->permissions[$role]) && $this->permissions[$role][$permission];
|
||||
}
|
||||
|
||||
public function hasRolePermission(User $user, string $permission): bool
|
||||
@@ -112,8 +106,6 @@ final class RolePermissionManager
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
$this->init();
|
||||
|
||||
return array_keys($this->permissionNames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use DateTimeInterface;
|
||||
|
||||
final class TimesheetStatisticService
|
||||
{
|
||||
public function __construct(private TimesheetRepository $repository)
|
||||
public function __construct(private readonly TimesheetRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ final class TimesheetStatisticService
|
||||
->addSelect('MONTH(t.date) as month')
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
->groupBy('year')
|
||||
->addGroupBy('month')
|
||||
@@ -125,10 +125,10 @@ final class TimesheetStatisticService
|
||||
->addSelect('IDENTITY(t.activity) as activity')
|
||||
->addSelect('DATE(t.date) as date')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
->groupBy('date')
|
||||
->addGroupBy('project')
|
||||
@@ -173,8 +173,6 @@ final class TimesheetStatisticService
|
||||
|
||||
/**
|
||||
* @internal only for core development
|
||||
* @param DateTimeInterface $begin
|
||||
* @param DateTimeInterface $end
|
||||
* @param User[] $users
|
||||
* @return array
|
||||
*/
|
||||
@@ -203,10 +201,10 @@ final class TimesheetStatisticService
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->addSelect('MONTH(t.date) as month')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
->groupBy('year')
|
||||
->addGroupBy('month')
|
||||
@@ -253,7 +251,7 @@ final class TimesheetStatisticService
|
||||
public function findFirstRecordDate(User $user): ?\DateTimeImmutable
|
||||
{
|
||||
$result = $this->repository->createQueryBuilder('t')
|
||||
->select('MIN(t.begin)')
|
||||
->select('MIN(t.date)')
|
||||
->where('t.user = :user')
|
||||
->setParameter('user', $user)
|
||||
->getQuery()
|
||||
@@ -295,10 +293,10 @@ final class TimesheetStatisticService
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->addSelect('IDENTITY(t.user) as user')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
->groupBy('year')
|
||||
->addGroupBy('month')
|
||||
|
||||
@@ -192,6 +192,8 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
||||
public function getJavascriptConfiguration(User $user): array
|
||||
{
|
||||
return [
|
||||
'locale' => $this->locale,
|
||||
'language' => $user->getLanguage(),
|
||||
'formatDuration' => $this->localeService->getDurationFormat($this->locale),
|
||||
'formatDate' => $this->localeService->getDateFormat($this->locale),
|
||||
'defaultColor' => Constants::DEFAULT_COLOR,
|
||||
|
||||
@@ -76,7 +76,7 @@ final class PaginationExtension extends AbstractExtension
|
||||
$propertyAccessor = PropertyAccess::createPropertyAccessor();
|
||||
$propertyAccessor->setValue($routeParams, $pagePropertyPath, $page);
|
||||
|
||||
return $router->generate($routeName, $routeParams);
|
||||
return $router->generate($routeName, $routeParams); // @phpstan-ignore-line
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ final class QuickEntryTimesheetValidator extends ConstraintValidator
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$timesheet = $value;
|
||||
|
||||
if ($timesheet->getId() === null && $timesheet->getDuration(false) === null) {
|
||||
if ($value->getId() === null && $value->getDuration(false) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,7 +47,7 @@ final class QuickEntryTimesheetValidator extends ConstraintValidator
|
||||
->getValidator()
|
||||
->inContext($this->context)
|
||||
->atPath('duration')
|
||||
->validate($timesheet, $innerConstraint, [Constraint::DEFAULT_GROUP]);
|
||||
->validate($value, $innerConstraint, [Constraint::DEFAULT_GROUP]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetBasicValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $systemConfiguration)
|
||||
public function __construct(private readonly SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ final class TimesheetBasicValidator extends ConstraintValidator
|
||||
$this->validateActivityAndProject($value, $this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context): void
|
||||
{
|
||||
$begin = $timesheet->getBegin();
|
||||
@@ -64,10 +60,6 @@ final class TimesheetBasicValidator extends ConstraintValidator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context): void
|
||||
{
|
||||
$activity = $timesheet->getActivity();
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Activity\ActivityStatisticService;
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Customer\CustomerStatisticService;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use App\Model\BudgetStatisticModel;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\TimesheetRepository;
|
||||
@@ -29,53 +29,49 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(
|
||||
private SystemConfiguration $configuration,
|
||||
private CustomerStatisticService $customerStatisticService,
|
||||
private ProjectStatisticService $projectStatisticService,
|
||||
private ActivityStatisticService $activityStatisticService,
|
||||
private TimesheetRepository $timesheetRepository,
|
||||
private RateServiceInterface $rateService,
|
||||
private AuthorizationCheckerInterface $security,
|
||||
private LocaleService $localeService
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly CustomerStatisticService $customerStatisticService,
|
||||
private readonly ProjectStatisticService $projectStatisticService,
|
||||
private readonly ActivityStatisticService $activityStatisticService,
|
||||
private readonly TimesheetRepository $timesheetRepository,
|
||||
private readonly RateServiceInterface $rateService,
|
||||
private readonly AuthorizationCheckerInterface $security,
|
||||
private readonly LocaleService $localeService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetBudgetUsed)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetBudgetUsed::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof Timesheet)) {
|
||||
throw new UnexpectedTypeException($timesheet, Timesheet::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($this->configuration->isTimesheetAllowOverbookingBudget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$begin = $timesheet->getBegin();
|
||||
$begin = $value->getBegin();
|
||||
|
||||
// we can only work with stopped entries
|
||||
if ($begin === null || $timesheet->getEnd() === null || $timesheet->getUser() === null) {
|
||||
if ($begin === null || $value->getEnd() === null || $value->getUser() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// budgets need only be calculated for billable records
|
||||
if (!$timesheet->isBillable()) {
|
||||
if (!$value->isBillable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this validator needs a project to calculate the rates
|
||||
if ($timesheet->getProject() === null) {
|
||||
if ($value->getProject() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $timesheet->getId();
|
||||
$id = $value->getId();
|
||||
|
||||
// when changing the date via the calendar and/or the API, the duration will not be reset by the
|
||||
// duration calculator (which runs after validation!) so we manually reset the duration before
|
||||
@@ -92,9 +88,9 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
// $timesheet->setDuration(null);
|
||||
// $duration = $timesheet->getDuration();
|
||||
|
||||
$duration = $timesheet->getCalculatedDuration();
|
||||
$duration = $value->getCalculatedDuration();
|
||||
|
||||
$timeRate = $this->rateService->calculate($timesheet);
|
||||
$timeRate = $this->rateService->calculate($value);
|
||||
$rate = $timeRate->getRate();
|
||||
|
||||
$activityDuration = $duration ?? 0;
|
||||
@@ -116,10 +112,10 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
// this could for example happen when export flag is changed OR if "prevent overbooking" config was recently activated and this is an old entry
|
||||
if ($duration === $rawData['duration'] &&
|
||||
$rate === $rawData['rate'] &&
|
||||
$timesheet->isBillable() === $rawData['billable'] &&
|
||||
$value->isBillable() === $rawData['billable'] &&
|
||||
$begin->format('Y.m.d') === $rawData['begin']->format('Y.m.d') &&
|
||||
$timesheet->getProject()->getId() === $projectId &&
|
||||
($timesheet->getActivity() === null || $timesheet->getActivity()->getId() === $activityId)
|
||||
$value->getProject()->getId() === $projectId &&
|
||||
($value->getActivity() === null || $value->getActivity()->getId() === $activityId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -129,18 +125,18 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
// only subtract the previously logged data in case the record was billable
|
||||
// if it wasn't billable, then its values are not included in the statistic models used later on
|
||||
if ($rawData['billable']) {
|
||||
if (null !== $timesheet->getActivity() && $activityId === $timesheet->getActivity()->getId()) {
|
||||
if (null !== $value->getActivity() && $activityId === $value->getActivity()->getId()) {
|
||||
$activityDuration -= $rawData['duration'];
|
||||
$activityRate -= $rawData['rate'];
|
||||
}
|
||||
|
||||
if (null !== $timesheet->getProject()) {
|
||||
if ($projectId === $timesheet->getProject()->getId()) {
|
||||
if (null !== $value->getProject()) {
|
||||
if ($projectId === $value->getProject()->getId()) {
|
||||
$projectDuration -= $rawData['duration'];
|
||||
$projectRate -= $rawData['rate'];
|
||||
}
|
||||
|
||||
if ($customerId === $timesheet->getProject()->getCustomer()->getId()) {
|
||||
if ($customerId === $value->getProject()->getCustomer()->getId()) {
|
||||
$customerDuration -= $rawData['duration'];
|
||||
$customerRate -= $rawData['rate'];
|
||||
}
|
||||
@@ -153,23 +149,23 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
$now = new DateTime('now', $begin->getTimezone());
|
||||
$recordDate = $begin;
|
||||
|
||||
if (null !== ($activity = $timesheet->getActivity()) && $activity->hasBudgets()) {
|
||||
if (null !== ($activity = $value->getActivity()) && $activity->hasBudgets()) {
|
||||
$dateTime = $activity->isMonthlyBudget() ? $recordDate : $now;
|
||||
if ($activity->isMonthlyBudget() && $monthWasChanged) {
|
||||
$activityDuration = $duration;
|
||||
}
|
||||
$stat = $this->activityStatisticService->getBudgetStatisticModel($activity, $dateTime);
|
||||
$this->checkBudgets($constraint, $stat, $timesheet, $activityDuration, $activityRate, 'activity');
|
||||
$this->checkBudgets($constraint, $stat, $value, $activityDuration, $activityRate, 'activity');
|
||||
}
|
||||
|
||||
if (null !== ($project = $timesheet->getProject())) {
|
||||
if (null !== ($project = $value->getProject())) {
|
||||
if ($project->hasBudgets()) {
|
||||
$dateTime = $project->isMonthlyBudget() ? $recordDate : $now;
|
||||
if ($project->isMonthlyBudget() && $monthWasChanged) {
|
||||
$projectDuration = $duration;
|
||||
}
|
||||
$stat = $this->projectStatisticService->getBudgetStatisticModel($project, $dateTime);
|
||||
$this->checkBudgets($constraint, $stat, $timesheet, $projectDuration, $projectRate, 'project');
|
||||
$this->checkBudgets($constraint, $stat, $value, $projectDuration, $projectRate, 'project');
|
||||
}
|
||||
if (null !== ($customer = $project->getCustomer()) && $customer->hasBudgets()) {
|
||||
$dateTime = $customer->isMonthlyBudget() ? $recordDate : $now;
|
||||
@@ -177,12 +173,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
$customerDuration = $duration;
|
||||
}
|
||||
$stat = $this->customerStatisticService->getBudgetStatisticModel($customer, $dateTime);
|
||||
$this->checkBudgets($constraint, $stat, $timesheet, $customerDuration, $customerRate, 'customer');
|
||||
$this->checkBudgets($constraint, $stat, $value, $customerDuration, $customerRate, 'customer');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkBudgets(TimesheetBudgetUsed $constraint, BudgetStatisticModel $stat, Timesheet $timesheet, int $duration, float $rate, string $field): bool
|
||||
private function checkBudgets(TimesheetBudgetUsed $constraint, BudgetStatisticModel $stat, TimesheetEntity $timesheet, int $duration, float $rate, string $field): bool
|
||||
{
|
||||
$fullRate = ($stat->getBudgetSpent() + $rate);
|
||||
|
||||
@@ -203,7 +199,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
return false;
|
||||
}
|
||||
|
||||
private function addBudgetViolation(TimesheetBudgetUsed $constraint, Timesheet $timesheet, string $field, float $budget, float $rate): void
|
||||
private function addBudgetViolation(TimesheetBudgetUsed $constraint, TimesheetEntity $timesheet, string $field, float $budget, float $rate): void
|
||||
{
|
||||
// using the locale of the assigned user is not the best solution, but allows to be independent of the request stack
|
||||
$helper = new LocaleFormatter($this->localeService, $timesheet->getUser()?->getLocale() ?? 'en');
|
||||
|
||||
@@ -17,29 +17,25 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetExportedValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private Security $security)
|
||||
public function __construct(private readonly Security $security)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetExported)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetExported::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($timesheet->getId() === null) {
|
||||
if ($value->getId() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$timesheet->isExported()) {
|
||||
if (!$value->isExported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +43,7 @@ final class TimesheetExportedValidator extends ConstraintValidator
|
||||
// can trigger is right when the "export" flag ist set from the "edit form".
|
||||
// most teamleads should not have "edit_exported_timesheet" but only "edit_export_other_timesheet"
|
||||
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('edit_export', $timesheet)) {
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('edit_export', $value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,14 +17,10 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetFutureTimesValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $configuration)
|
||||
public function __construct(private readonly SystemConfiguration $configuration)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $value
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetFutureTimes)) {
|
||||
|
||||
@@ -18,29 +18,28 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetLockdownValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private Security $security, private LockdownService $lockdownService)
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly LockdownService $lockdownService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetLockdown)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetLockdown::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if (!$this->lockdownService->isLockdownActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === ($timesheetStart = $timesheet->getBegin())) {
|
||||
if (null === ($timesheetStart = $value->getBegin())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,7 +66,7 @@ final class TimesheetLockdownValidator extends ConstraintValidator
|
||||
$allowEditInGracePeriod = true;
|
||||
}
|
||||
|
||||
if ($this->lockdownService->isEditable($timesheet, $now, $allowEditInGracePeriod)) {
|
||||
if ($this->lockdownService->isEditable($value, $now, $allowEditInGracePeriod)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,30 +17,26 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetLongRunningValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $systemConfiguration)
|
||||
public function __construct(private readonly SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetLongRunning)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetLongRunning::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($timesheet->isRunning()) {
|
||||
if ($value->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var int $duration */
|
||||
$duration = $timesheet->getCalculatedDuration();
|
||||
$duration = $value->getCalculatedDuration();
|
||||
|
||||
// one year is currently the maximum that can be logged (which is already not logically)
|
||||
// the database column could hold more data, but let's limit it here
|
||||
|
||||
@@ -18,26 +18,25 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $configuration, private TimesheetRepository $repository)
|
||||
public function __construct(
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly TimesheetRepository $repository
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetOverlapping)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetOverlapping::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$begin = $timesheet->getBegin();
|
||||
$end = $timesheet->getEnd();
|
||||
$begin = $value->getBegin();
|
||||
$end = $value->getEnd();
|
||||
|
||||
// this case is handled in TimesheetValidator and should not raise a second validation
|
||||
if ($begin !== null && $end !== null && $begin > $end) {
|
||||
@@ -48,7 +47,7 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->repository->hasRecordForTime($timesheet)) {
|
||||
if (!$this->repository->hasRecordForTime($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,28 +18,27 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetRestartValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private Security $security, private TrackingModeService $trackingModeService)
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly TrackingModeService $trackingModeService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetRestart)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetRestart::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
// special case that would otherwise need to be validated in several controllers:
|
||||
// an entry is edited and the end date is removed (or duration deleted) would restart the record,
|
||||
// which might be disallowed for the current user
|
||||
if (null !== $timesheet->getEnd()) {
|
||||
if (null !== $value->getEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@ final class TimesheetRestartValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('start', $timesheet)) {
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('start', $value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,18 +28,14 @@ final class TimesheetValidator extends ConstraintValidator
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetEntityConstraint)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetEntityConstraint::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$groups = [Constraint::DEFAULT_GROUP];
|
||||
@@ -51,7 +47,7 @@ final class TimesheetValidator extends ConstraintValidator
|
||||
$this->context
|
||||
->getValidator()
|
||||
->inContext($this->context)
|
||||
->validate($timesheet, $innerConstraint, $groups);
|
||||
->validate($value, $innerConstraint, $groups);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,35 +17,31 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetZeroDurationValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $configuration)
|
||||
public function __construct(private readonly SystemConfiguration $configuration)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetZeroDuration)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetZeroDuration::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($this->configuration->isTimesheetAllowZeroDuration()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($timesheet->isRunning()) {
|
||||
if ($value->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$duration = 0;
|
||||
if ($timesheet->getEnd() !== null && $timesheet->getBegin() !== null) {
|
||||
$duration = $timesheet->getCalculatedDuration();
|
||||
if ($value->getEnd() !== null && $value->getBegin() !== null) {
|
||||
$duration = $value->getCalculatedDuration();
|
||||
}
|
||||
|
||||
if ($duration <= 0) {
|
||||
|
||||
@@ -35,21 +35,23 @@ final class ActivityVoter extends Voter
|
||||
'permissions',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Activity::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Activity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Activity && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
@@ -65,7 +67,7 @@ final class ActivityVoter extends Voter
|
||||
}
|
||||
|
||||
// those cannot be assigned to teams
|
||||
if (\in_array($attribute, ['create', 'delete'])) {
|
||||
if (\in_array($attribute, ['create', 'delete'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,21 +39,23 @@ final class CustomerVoter extends Voter
|
||||
'access',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Customer::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Customer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Customer && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -38,17 +38,22 @@ final class EntityMultiRoleVoter extends Voter
|
||||
'activity',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$this->supportsAttribute($attribute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) {
|
||||
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,7 +74,7 @@ final class EntityMultiRoleVoter extends Voter
|
||||
|
||||
$suffix = null;
|
||||
|
||||
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) {
|
||||
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS, true)) {
|
||||
$suffix = $subject;
|
||||
} elseif ($subject instanceof Activity) {
|
||||
$suffix = 'activity';
|
||||
|
||||
@@ -37,21 +37,23 @@ final class ProjectVoter extends Voter
|
||||
'details',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Project::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Project)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Project && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -20,13 +20,21 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
*/
|
||||
final class QuickEntryVoter extends Voter
|
||||
{
|
||||
public function __construct(private RolePermissionManager $permissionManager, private TrackingModeService $trackingModeService)
|
||||
public function __construct(
|
||||
private readonly RolePermissionManager $permissionManager,
|
||||
private readonly TrackingModeService $trackingModeService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return 'quick-entry' === $attribute;
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return 'quick-entry' === $attribute;
|
||||
return $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -26,13 +26,23 @@ final class ReportingVoter extends Voter
|
||||
'report:user',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return $subjectType === 'null';
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return $subject === null && \in_array($attribute, self::ALLOWED_ATTRIBUTES);
|
||||
return $subject === null && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -21,18 +21,24 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
*/
|
||||
final class RolePermissionVoter extends Voter
|
||||
{
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return $this->permissionManager->isRegisteredPermission($attribute);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
// we only work on single strings that have no subject
|
||||
return $subjectType === 'null';
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
// we only work on single strings that have no subject
|
||||
if (null !== $subject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->permissionManager->isRegisteredPermission($attribute);
|
||||
return $subject === null && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -29,21 +29,23 @@ final class TeamVoter extends Voter
|
||||
'delete',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Team::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Team)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Team && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Voter;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\MultiUserTimesheet;
|
||||
use App\Security\RolePermissionManager;
|
||||
use App\Timesheet\LockdownService;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
@@ -55,21 +56,26 @@ final class TimesheetVoter extends Voter
|
||||
private ?bool $editExported = null;
|
||||
private ?\DateTime $now = null;
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager, private LockdownService $lockdownService)
|
||||
public function __construct(
|
||||
private readonly RolePermissionManager $permissionManager,
|
||||
private readonly LockdownService $lockdownService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Timesheet::class) || str_contains($subjectType, MultiUserTimesheet::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Timesheet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Timesheet && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -38,21 +38,23 @@ final class UserVoter extends Voter
|
||||
'supervisor',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, User::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof User)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof User && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -25,7 +25,7 @@ use DateTimeInterface;
|
||||
*/
|
||||
class DailyWorkingTimeChartProvider
|
||||
{
|
||||
public function __construct(private TimesheetRepository $repository)
|
||||
public function __construct(private readonly TimesheetRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -33,9 +33,6 @@ class DailyWorkingTimeChartProvider
|
||||
* In case this method is called with one timezone and the results are from another timezone,
|
||||
* it might return rows outside the time-range.
|
||||
*
|
||||
* @param DateTimeInterface $begin
|
||||
* @param DateTimeInterface $end
|
||||
* @param User|null $user
|
||||
* @return array<mixed>
|
||||
*/
|
||||
protected function getDailyData(DateTimeInterface $begin, DateTimeInterface $end, ?User $user = null): array
|
||||
|
||||
@@ -144,10 +144,10 @@ final class WorkingTimeService
|
||||
->select('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('DATE(t.date) as day')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->eq('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $user->getId())
|
||||
->addGroupBy('day')
|
||||
;
|
||||
|
||||
Reference in New Issue
Block a user