diff --git a/composer.json b/composer.json index 070a91a1..e3ad7ac4 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "friendsofsymfony/rest-bundle": "^3.0", "gedmo/doctrine-extensions": "^3.6", "jms/serializer-bundle": "^5.0", - "kevinpapst/tabler-bundle": "^1.1", + "kevinpapst/tabler-bundle": "dev-main", "league/csv": "^9.4", "mpdf/mpdf": "^8.0", "nelmio/api-doc-bundle": "^4.0", diff --git a/composer.lock b/composer.lock index b5d53ec8..a7b2b07e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a307b9a36c22711439283a3e5bc3249c", + "content-hash": "fa716c8a640639ed9a9b64089f64a6ba", "packages": [ { "name": "azuyalabs/yasumi", @@ -2447,16 +2447,16 @@ }, { "name": "kevinpapst/tabler-bundle", - "version": "1.1.0", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/kevinpapst/TablerBundle.git", - "reference": "57dd328fe98cee4321d0b77d63eebd118180ab61" + "reference": "c9a9dbf160094f2b2987f74cc15eade974793f14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/57dd328fe98cee4321d0b77d63eebd118180ab61", - "reference": "57dd328fe98cee4321d0b77d63eebd118180ab61", + "url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/c9a9dbf160094f2b2987f74cc15eade974793f14", + "reference": "c9a9dbf160094f2b2987f74cc15eade974793f14", "shasum": "" }, "require": { @@ -2485,6 +2485,7 @@ "suggest": { "knplabs/knp-menu-bundle": "Allows easy menu integration" }, + "default-branch": true, "type": "symfony-bundle", "autoload": { "psr-4": { @@ -2504,7 +2505,7 @@ "description": "Admin/Backend theme bundle for Symfony based on Tabler.io", "support": { "issues": "https://github.com/kevinpapst/TablerBundle/issues", - "source": "https://github.com/kevinpapst/TablerBundle/tree/1.1.0" + "source": "https://github.com/kevinpapst/TablerBundle/tree/main" }, "funding": [ { @@ -2516,7 +2517,7 @@ "type": "github" } ], - "time": "2023-12-01T15:39:05+00:00" + "time": "2024-01-16T17:24:13+00:00" }, { "name": "league/csv", @@ -13353,7 +13354,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "kevinpapst/tabler-bundle": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/phpstan.neon b/phpstan.neon index b95e5cb6..beb67f78 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1036,11 +1036,6 @@ parameters: count: 3 path: src/Controller/QuickEntryController.php - - - message: "#^Cannot call method setTime\\(\\) on DateTime\\|null\\.$#" - count: 3 - path: src/Controller/QuickEntryController.php - - message: "#^Method App\\\\Controller\\\\Reporting\\\\AbstractUserReportController\\:\\:getStatisticDataRaw\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -3646,11 +3641,6 @@ parameters: count: 2 path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - - message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#" - count: 4 - path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - message: "#^Cannot call method getTitle\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" count: 1 @@ -5661,11 +5651,6 @@ parameters: count: 1 path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - - message: "#^Cannot call method getLanguage\\(\\) on App\\\\Entity\\\\User\\|null\\.$#" - count: 1 - path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php - - message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetBudgetUsedValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#" count: 2 diff --git a/src/API/ActionsController.php b/src/API/ActionsController.php index 03d1e56d..84c7fe9c 100644 --- a/src/API/ActionsController.php +++ b/src/API/ActionsController.php @@ -32,9 +32,9 @@ use Symfony\Contracts\Translation\TranslatorInterface; final class ActionsController extends BaseApiController { public function __construct( - private ViewHandlerInterface $viewHandler, - private EventDispatcherInterface $dispatcher, - private TranslatorInterface $translator + private readonly ViewHandlerInterface $viewHandler, + private readonly EventDispatcherInterface $dispatcher, + private readonly TranslatorInterface $translator ) { } diff --git a/src/Command/MailTestCommand.php b/src/Command/MailTestCommand.php new file mode 100644 index 00000000..7507d756 --- /dev/null +++ b/src/Command/MailTestCommand.php @@ -0,0 +1,48 @@ +addArgument('to', InputArgument::REQUIRED, 'The email address to send the email to'); + $this->addOption('from', null, InputOption::VALUE_OPTIONAL, 'The sender of the message', 'kimai@example.org'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $message = new Email(); + $message->to((string) $input->getArgument('to')); // @phpstan-ignore-line + $message->from((string) $input->getOption('from')); // @phpstan-ignore-line + $message->subject('Kimai test email'); + $message->text('This is an email for testing the text body.'); + + $this->dispatcher->dispatch(new EmailEvent($message)); + + return Command::SUCCESS; + } +} diff --git a/src/Constants.php b/src/Constants.php index 9394220c..1de3b6b0 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ class Constants /** * The current release version */ - public const VERSION = '2.9.0'; + public const VERSION = '2.10.0'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 20900; + public const VERSION_ID = 21000; /** * The software name */ diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index 49b4cd30..6cf81699 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -95,7 +95,12 @@ final class ProfileController extends AbstractController $this->flashSuccess('action.update.success'); - return $this->redirectToRoute('user_profile_edit', ['username' => $profile->getUserIdentifier()]); + $locale = $request->getLocale(); + if ($this->getUser()->getId() === $profile->getId()) { + $locale = $profile->getPreferenceValue('language', $locale, false); + } + + return $this->redirectToRoute('user_profile_edit', ['username' => $profile->getUserIdentifier(), '_locale' => $locale]); } return $this->render('user/profile.html.twig', [ diff --git a/src/Controller/QuickEntryController.php b/src/Controller/QuickEntryController.php index 455b3671..31ad4b41 100644 --- a/src/Controller/QuickEntryController.php +++ b/src/Controller/QuickEntryController.php @@ -10,7 +10,6 @@ namespace App\Controller; use App\Configuration\SystemConfiguration; -use App\Entity\Timesheet; use App\Form\QuickEntryForm; use App\Model\QuickEntryWeek; use App\Repository\Query\TimesheetQuery; @@ -71,7 +70,6 @@ final class QuickEntryController extends AbstractController $result = $this->repository->getTimesheetResult($query); $rows = []; - /** @var Timesheet $timesheet */ foreach ($result->getResults(true) as $timesheet) { $i = 0; $id = $timesheet->getProject()->getId() . '_' . $timesheet->getActivity()->getId(); @@ -124,7 +122,6 @@ final class QuickEntryController extends AbstractController $defaultBegin = $factory->createDateTime($this->configuration->getTimesheetDefaultBeginTime()); $defaultHour = (int) $defaultBegin->format('H'); $defaultMinute = (int) $defaultBegin->format('i'); - $defaultBegin->setTime($defaultHour, $defaultMinute, 0, 0); $formModel = new QuickEntryWeek($startWeek); @@ -136,8 +133,9 @@ final class QuickEntryController extends AbstractController $tmp = $this->timesheetService->createNewTimesheet($user); $tmp->setProject($row['project']); $tmp->setActivity($row['activity']); - $tmp->setBegin(clone $day['day']); - $tmp->getBegin()->setTime($defaultHour, $defaultMinute, 0, 0); + $newTime = \DateTime::createFromInterface($day['day']); + $newTime = $newTime->setTime($defaultHour, $defaultMinute); + $tmp->setBegin($newTime); $this->timesheetService->prepareNewTimesheet($tmp); $model->addTimesheet($tmp); } else { @@ -151,8 +149,9 @@ final class QuickEntryController extends AbstractController $empty->markAsPrototype(); foreach ($week as $dayId => $day) { $tmp = $this->timesheetService->createNewTimesheet($user); - $tmp->setBegin(clone $day['day']); - $tmp->getBegin()->setTime($defaultHour, $defaultMinute, 0, 0); + $newTime = \DateTime::createFromInterface($day['day']); + $newTime = $newTime->setTime($defaultHour, $defaultMinute, 0, 0); + $tmp->setBegin($newTime); $this->timesheetService->prepareNewTimesheet($tmp); $empty->addTimesheet($tmp); } @@ -165,8 +164,9 @@ final class QuickEntryController extends AbstractController $model = $formModel->addRow($user); foreach ($week as $dayId => $day) { $tmp = $this->timesheetService->createNewTimesheet($user); - $tmp->setBegin(clone $day['day']); - $tmp->getBegin()->setTime($defaultHour, $defaultMinute, 0, 0); + $newTime = \DateTime::createFromInterface($day['day']); + $newTime = $newTime->setTime($defaultHour, $defaultMinute, 0, 0); + $tmp->setBegin($newTime); $this->timesheetService->prepareNewTimesheet($tmp); $model->addTimesheet($tmp); } @@ -193,7 +193,7 @@ final class QuickEntryController extends AbstractController foreach ($tmpModel->getTimesheets() as $timesheet) { if ($timesheet->getId() !== null) { $duration = $timesheet->getDuration(false); - if ($duration === null || $timesheet->getEnd() === null) { + if ($duration === null || $timesheet->isRunning()) { $deleteTimesheets[] = $timesheet; } else { $saveTimesheets[] = $timesheet; diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 816220e2..a501de87 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -19,7 +19,6 @@ use App\Export\Spreadsheet\Writer\XlsxWriter; use App\Form\Toolbar\UserToolbarForm; use App\Form\Type\UserType; use App\Form\UserCreateType; -use App\Repository\Query\UserFormTypeQuery; use App\Repository\Query\UserQuery; use App\Repository\TimesheetRepository; use App\Repository\UserRepository; @@ -79,6 +78,7 @@ final class UserController extends AbstractController $table->addColumn('email', ['class' => 'd-none', 'orderBy' => false]); $table->addColumn('lastLogin', ['class' => 'd-none', 'orderBy' => false]); $table->addColumn('roles', ['class' => 'd-none', 'orderBy' => false]); + $table->addColumn('system_account', ['class' => 'd-none', 'orderBy' => 'systemAccount']); foreach ($event->getPreferences() as $userPreference) { $table->addColumn('mf_' . $userPreference->getName(), ['title' => $userPreference->getLabel(), 'class' => 'd-none', 'orderBy' => false, 'translation_domain' => 'messages', 'data' => $userPreference]); @@ -163,13 +163,7 @@ final class UserController extends AbstractController ] ]) ->add('user', UserType::class, [ - 'query_builder' => function (UserRepository $repo) use ($userToDelete) { - $query = new UserFormTypeQuery(); - $query->addUserToIgnore($userToDelete); - $query->setUser($this->getUser()); - - return $repo->getQueryBuilderForFormType($query); - }, + 'ignore_users' => [$userToDelete], 'required' => false, ]) ->setAction($this->generateUrl('admin_user_delete', ['id' => $userToDelete->getId()])) diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 637d90d2..d31f0bee 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -87,9 +87,9 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt * Reflects the date in the user timezone (not in UTC). * This value is automatically set through the begin column and ONLY used in statistic queries. */ - #[ORM\Column(name: 'date_tz', type: 'date', nullable: false)] + #[ORM\Column(name: 'date_tz', type: 'date_immutable', nullable: false)] #[Assert\NotNull] - private ?DateTime $date = null; + private ?\DateTimeImmutable $date = null; /** * Time records start date-time. * @@ -265,7 +265,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt $this->begin = $begin; $this->timezone = $begin->getTimezone()->getName(); // make sure that the original date is always kept in UTC - $this->date = new DateTime($begin->format('Y-m-d 00:00:00'), new DateTimeZone('UTC')); + $this->date = new \DateTimeImmutable($begin->format('Y-m-d 00:00:00'), new DateTimeZone('UTC')); return $this; } diff --git a/src/EventSubscriber/EmailSubscriber.php b/src/EventSubscriber/EmailSubscriber.php index c9ce85e1..d6b815d0 100644 --- a/src/EventSubscriber/EmailSubscriber.php +++ b/src/EventSubscriber/EmailSubscriber.php @@ -18,7 +18,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; */ final class EmailSubscriber implements EventSubscriberInterface { - public function __construct(private KimaiMailer $mailer) + public function __construct(private readonly KimaiMailer $mailer) { } diff --git a/src/EventSubscriber/NotificationsSubscriber.php b/src/EventSubscriber/NotificationsSubscriber.php new file mode 100644 index 00000000..ca90e5e0 --- /dev/null +++ b/src/EventSubscriber/NotificationsSubscriber.php @@ -0,0 +1,28 @@ + ['onNotificationEvent', 100], + ]; + } + + public function onNotificationEvent(NotificationEvent $event): void + { + $event->setShowBadgeTotal(false); + } +} diff --git a/src/Form/TimesheetEditForm.php b/src/Form/TimesheetEditForm.php index 6ee0e3db..a824a746 100644 --- a/src/Form/TimesheetEditForm.php +++ b/src/Form/TimesheetEditForm.php @@ -315,7 +315,7 @@ class TimesheetEditForm extends AbstractType function (FormEvent $event) { /** @var Timesheet|null $timesheet */ $timesheet = $event->getData(); - if (null === $timesheet || null === $timesheet->getEnd()) { + if (null === $timesheet || $timesheet->isRunning()) { $event->getForm()->get('duration')->setData(null); } } @@ -340,7 +340,7 @@ class TimesheetEditForm extends AbstractType // only apply the duration, if the end is not yet set // without that check, the end would be overwritten and the real end time would be lost - if (($forceApply && $duration > 0) || ($duration > 0 && null === $timesheet->getEnd())) { + if (($forceApply && $duration > 0) || ($duration > 0 && $timesheet->isRunning())) { $end = clone $timesheet->getBegin(); $end->modify('+ ' . $duration . 'seconds'); $timesheet->setEnd($end); diff --git a/src/Form/Type/DatePickerType.php b/src/Form/Type/DatePickerType.php index 5bbcb40b..d87192c6 100644 --- a/src/Form/Type/DatePickerType.php +++ b/src/Form/Type/DatePickerType.php @@ -39,13 +39,16 @@ class DatePickerType extends AbstractType return null; } - if ($reverseTransform instanceof \DateTimeInterface && $options['force_time']) { + if ($reverseTransform instanceof \DateTimeInterface && $options['force_time'] !== null) { if ($options['force_time'] === 'start') { $reverseTransform = \DateTime::createFromInterface($reverseTransform); - $reverseTransform->setTime(0, 0, 0); + $reverseTransform = $reverseTransform->setTime(0, 0, 0); } elseif ($options['force_time'] === 'end') { $reverseTransform = \DateTime::createFromInterface($reverseTransform); - $reverseTransform->setTime(23, 59, 59); + $reverseTransform = $reverseTransform->setTime(23, 59, 59); + } elseif (\is_string($options['force_time'])) { + $reverseTransform = \DateTime::createFromInterface($reverseTransform); + $reverseTransform = $reverseTransform->modify($options['force_time']); } } @@ -81,7 +84,7 @@ class DatePickerType extends AbstractType 'format' => $formFormat, 'model_timezone' => date_default_timezone_get(), 'view_timezone' => date_default_timezone_get(), - 'force_time' => null, + 'force_time' => null, // one of: string (start, end) or a string to as argument for DateTime->modify() or null 'min_day' => null, 'max_day' => null, ]); diff --git a/src/Form/Type/QuickEntryTimesheetType.php b/src/Form/Type/QuickEntryTimesheetType.php index 84f562a4..8359bc9d 100644 --- a/src/Form/Type/QuickEntryTimesheetType.php +++ b/src/Form/Type/QuickEntryTimesheetType.php @@ -56,7 +56,7 @@ final class QuickEntryTimesheetType extends AbstractType function (FormEvent $event) use ($durationOptions) { /** @var Timesheet|null $data */ $data = $event->getData(); - if (null === $data || null === $data->getEnd()) { + if (null === $data || $data->isRunning()) { $event->getForm()->get('duration')->setData(null); } diff --git a/src/Form/Type/UserType.php b/src/Form/Type/UserType.php index bad1e68a..0549550d 100644 --- a/src/Form/Type/UserType.php +++ b/src/Form/Type/UserType.php @@ -27,6 +27,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver; */ final class UserType extends AbstractType { + public function __construct(private readonly UserRepository $userRepository) + { + } + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ @@ -63,31 +67,66 @@ final class UserType extends AbstractType // e.g. when editing a team that has disabled users, these users would be removed silently // see https://github.com/kimai/kimai/pull/1841 'include_users' => [], + // includes the current user if it is a system-account, which is especially useful for forms pages, + // which have a user switcher and display the logged-in user by default + 'include_current_user_if_system_account' => false, 'documentation' => [ 'type' => 'integer', 'description' => 'User ID', ], ]); - $resolver->setDefault('query_builder', function (Options $options) { - return function (UserRepository $repo) use ($options) { - $query = new UserFormTypeQuery(); - $query->setUser($options['user']); + $resolver->setDefault('choices', function (Options $options) { + $query = new UserFormTypeQuery(); + $query->setUser($options['user']); - if ($options['include_disabled'] === true) { - $query->setVisibility(VisibilityInterface::SHOW_BOTH); + if ($options['include_disabled'] === true) { + $query->setVisibility(VisibilityInterface::SHOW_BOTH); + } + + $qb = $this->userRepository->getQueryBuilderForFormType($query); + $users = $qb->getQuery()->getResult(); + + $ignoreIds = []; + /** @var User $user */ + foreach ($options['ignore_users'] as $user) { + $ignoreIds[] = $user->getId(); + } + + $users = array_filter($users, function (User $user) use ($ignoreIds) { + if ($user->getId() === null) { + return false; } - foreach ($options['ignore_users'] as $userToIgnore) { - $query->addUserToIgnore($userToIgnore); - } + return !\in_array($user->getId(), $ignoreIds, true); + }); - if (!empty($options['include_users'])) { - $query->setUsersAlwaysIncluded($options['include_users']); - } + /** @var array $userById */ + $userById = []; + /** @var User $user */ + foreach ($users as $user) { + $userById[$user->getId()] = $user; + } - return $repo->getQueryBuilderForFormType($query); - }; + $includeUsers = $options['include_users']; + if ($options['include_current_user_if_system_account'] === true) { + if ($options['user'] instanceof User && $options['user']->isSystemAccount()) { + $includeUsers[] = $options['user']; + } + } + + /** @var User $user */ + foreach ($includeUsers as $user) { + if ($user->getId() !== null && !\array_key_exists($user->getId(), $userById)) { + $userById[$user->getId()] = $user; + } + } + + usort($userById, function (User $a, User $b) { + return $a->getDisplayName() <=> $b->getDisplayName(); + }); + + return array_values($userById); }); } diff --git a/src/Form/YearByUserForm.php b/src/Form/YearByUserForm.php index 1755a9c8..1a9ea203 100644 --- a/src/Form/YearByUserForm.php +++ b/src/Form/YearByUserForm.php @@ -27,7 +27,10 @@ final class YearByUserForm extends AbstractType ]); if ($options['include_user']) { - $builder->add('user', UserType::class, ['width' => false]); + $builder->add('user', UserType::class, [ + 'width' => false, + 'include_current_user_if_system_account' => true + ]); } } diff --git a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php index 426e3fa3..5549699d 100644 --- a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php @@ -133,11 +133,11 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator $max = null; foreach ($entries as $entry) { - if ($min === null || $min->getBegin()->getTimestamp() > $entry->getBegin()->getTimestamp()) { + if ($min === null || $min->getBegin() > $entry->getBegin()) { $min = $entry; } - if ($max === null || $max->getBegin()->getTimestamp() < $entry->getBegin()->getTimestamp()) { + if ($max === null || $max->getBegin() < $entry->getBegin()) { $max = $entry; } } diff --git a/src/Mail/KimaiMailer.php b/src/Mail/KimaiMailer.php index 83b892ce..0298acaa 100644 --- a/src/Mail/KimaiMailer.php +++ b/src/Mail/KimaiMailer.php @@ -17,13 +17,13 @@ use Symfony\Component\Mime\RawMessage; final class KimaiMailer implements MailerInterface { - public function __construct(private MailConfiguration $configuration, private MailerInterface $mailer) + public function __construct(private readonly MailConfiguration $configuration, private readonly MailerInterface $mailer) { } public function send(RawMessage $message, Envelope $envelope = null): void { - if ($message instanceof Email) { + if ($message instanceof Email && \count($message->getFrom()) === 0) { $message->from($this->configuration->getFromAddress()); } diff --git a/src/Reporting/MonthByUser/MonthByUserForm.php b/src/Reporting/MonthByUser/MonthByUserForm.php index ff7648fd..60092683 100644 --- a/src/Reporting/MonthByUser/MonthByUserForm.php +++ b/src/Reporting/MonthByUser/MonthByUserForm.php @@ -30,7 +30,10 @@ final class MonthByUserForm extends AbstractType ]); if ($options['include_user']) { - $builder->add('user', UserType::class, ['width' => false]); + $builder->add('user', UserType::class, [ + 'width' => false, + 'include_current_user_if_system_account' => true, + ]); } $builder->add('sumType', ReportSumType::class); } diff --git a/src/Reporting/WeekByUser/WeekByUserForm.php b/src/Reporting/WeekByUser/WeekByUserForm.php index 838fbe49..d5e2df72 100644 --- a/src/Reporting/WeekByUser/WeekByUserForm.php +++ b/src/Reporting/WeekByUser/WeekByUserForm.php @@ -30,7 +30,10 @@ final class WeekByUserForm extends AbstractType ]); if ($options['include_user']) { - $builder->add('user', UserType::class, ['width' => false]); + $builder->add('user', UserType::class, [ + 'width' => false, + 'include_current_user_if_system_account' => true, + ]); } $builder->add('sumType', ReportSumType::class); } diff --git a/src/Reporting/YearByUser/YearByUserForm.php b/src/Reporting/YearByUser/YearByUserForm.php index f4526166..96df3fde 100644 --- a/src/Reporting/YearByUser/YearByUserForm.php +++ b/src/Reporting/YearByUser/YearByUserForm.php @@ -30,7 +30,10 @@ final class YearByUserForm extends AbstractType ]); if ($options['include_user']) { - $builder->add('user', UserType::class, ['width' => false]); + $builder->add('user', UserType::class, [ + 'width' => false, + 'include_current_user_if_system_account' => true, + ]); } $builder->add('sumType', ReportSumType::class); } diff --git a/src/Repository/Query/UserFormTypeQuery.php b/src/Repository/Query/UserFormTypeQuery.php index 0f5411f9..60ebf274 100644 --- a/src/Repository/Query/UserFormTypeQuery.php +++ b/src/Repository/Query/UserFormTypeQuery.php @@ -9,64 +9,10 @@ namespace App\Repository\Query; -use App\Entity\User; - /** * Can be used to pre-fill form types with: UserRepository::getQueryBuilderForFormType() */ final class UserFormTypeQuery extends BaseFormTypeQuery { use VisibilityTrait; - - /** - * @var User[] - */ - private array $includeUsers = []; - /** - * @var User[] - */ - private array $ignoredUsers = []; - - /** - * Sets a list of users which must be included in the result always. - * - * @param array $users - */ - public function setUsersAlwaysIncluded(array $users): void - { - $this->includeUsers = $users; - } - - /** - * Get the list of users which should always be included in the result. - * - * @return User[] - */ - public function getUsersAlwaysIncluded(): array - { - return $this->includeUsers; - } - - /** - * Given user will be excluded from the result set. - * - * @param User $user - * @return $this - */ - public function addUserToIgnore(User $user): UserFormTypeQuery - { - $this->ignoredUsers[] = $user; - - return $this; - } - - /** - * Returns the list of users that should not be loaded. - * - * @return User[] - */ - public function getUsersToIgnore(): array - { - return $this->ignoredUsers; - } } diff --git a/src/Repository/Query/UserQuery.php b/src/Repository/Query/UserQuery.php index 95eb4fcd..481b9230 100644 --- a/src/Repository/Query/UserQuery.php +++ b/src/Repository/Query/UserQuery.php @@ -18,7 +18,7 @@ class UserQuery extends BaseQuery implements VisibilityInterface { use VisibilityTrait; - public const USER_ORDER_ALLOWED = ['alias', 'user', 'username', 'title', 'email']; + public const USER_ORDER_ALLOWED = ['username', 'alias', 'title', 'email', 'systemAccount']; private ?string $role = null; /** @@ -30,7 +30,7 @@ class UserQuery extends BaseQuery implements VisibilityInterface public function __construct() { $this->setDefaults([ - 'orderBy' => 'user', + 'orderBy' => 'username', 'searchTeams' => [], 'visibility' => VisibilityInterface::SHOW_VISIBLE, 'systemAccount' => null, diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 27e81f69..f4f1f3a2 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -180,35 +180,15 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us { $qb = $this->createQueryBuilder('u'); - $or = $qb->expr()->orX(); - if ($query->isShowVisible()) { - $or->add($qb->expr()->eq('u.enabled', ':enabled')); + $qb->andWhere($qb->expr()->eq('u.enabled', ':enabled')); $qb->setParameter('enabled', true, ParameterType::BOOLEAN); } - $includeAlways = $query->getUsersAlwaysIncluded(); - if (!empty($includeAlways)) { - $or->add($qb->expr()->in('u', ':users')); - $qb->setParameter('users', $includeAlways); - } - - if ($or->count() > 0) { - $qb->andWhere($or); - } - - if (\count($query->getUsersToIgnore()) > 0) { - $ids = array_map(function (User $user) { - return $user->getId(); - }, $query->getUsersToIgnore()); - - $qb->andWhere($qb->expr()->notIn('u.id', $ids)); - } - $qb->andWhere($qb->expr()->eq('u.systemAccount', ':system')); $qb->setParameter('system', false, Types::BOOLEAN); - - $qb->orderBy('u.username', 'ASC'); + $qb->addSelect("COALESCE(NULLIF(u.alias, ''), u.username) as HIDDEN userOrder"); + $qb->orderBy('userOrder', 'ASC'); $this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams()); @@ -307,10 +287,6 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us foreach ($query->getOrderGroups() as $orderBy => $order) { switch ($orderBy) { - case 'user': - $qb->addSelect('COALESCE(u.alias, u.username) as HIDDEN userOrder'); - $orderBy = 'userOrder'; - break; default: $orderBy = 'u.' . $orderBy; break; diff --git a/src/Timesheet/DateTimeFactory.php b/src/Timesheet/DateTimeFactory.php index f13e9e51..3aa6c606 100644 --- a/src/Timesheet/DateTimeFactory.php +++ b/src/Timesheet/DateTimeFactory.php @@ -135,6 +135,11 @@ final class DateTimeFactory return new DateTime($datetime, $this->getTimezone()); } + public function create(string $datetime = 'now'): \DateTimeImmutable + { + return new \DateTimeImmutable($datetime, $this->getTimezone()); + } + /** * @param string $format * @param null|string $datetime diff --git a/src/Timesheet/RateService.php b/src/Timesheet/RateService.php index 381e23c5..37cd5671 100644 --- a/src/Timesheet/RateService.php +++ b/src/Timesheet/RateService.php @@ -25,7 +25,7 @@ final class RateService implements RateServiceInterface public function calculate(Timesheet $record): Rate { - if (null === $record->getEnd()) { + if ($record->isRunning()) { return new Rate(0.00, 0.00); } diff --git a/src/Timesheet/TrackingMode/DefaultMode.php b/src/Timesheet/TrackingMode/DefaultMode.php index a4d27f20..5117b633 100644 --- a/src/Timesheet/TrackingMode/DefaultMode.php +++ b/src/Timesheet/TrackingMode/DefaultMode.php @@ -65,7 +65,7 @@ final class DefaultMode extends AbstractTrackingMode $this->rounding->roundBegin($timesheet); - if (null !== $timesheet->getEnd()) { + if (!$timesheet->isRunning()) { $this->rounding->roundEnd($timesheet); if (null !== $timesheet->getDuration()) { diff --git a/src/Validator/Constraints/TimesheetBasicValidator.php b/src/Validator/Constraints/TimesheetBasicValidator.php index 199ac84e..2e92feb8 100644 --- a/src/Validator/Constraints/TimesheetBasicValidator.php +++ b/src/Validator/Constraints/TimesheetBasicValidator.php @@ -140,13 +140,13 @@ final class TimesheetBasicValidator extends ConstraintValidator } if (null !== $timesheetEnd) { - if (null !== $projectEnd && $timesheetEnd->getTimestamp() > $projectEnd->getTimestamp()) { + if (null !== $projectEnd && $timesheetEnd > $projectEnd) { $context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::PROJECT_ALREADY_ENDED)) ->atPath($pathEnd) ->setTranslationDomain('validators') ->setCode(TimesheetBasic::PROJECT_ALREADY_ENDED) ->addViolation(); - } elseif (null !== $projectBegin && $timesheetEnd->getTimestamp() < $projectBegin->getTimestamp()) { + } elseif (null !== $projectBegin && $timesheetEnd < $projectBegin) { $context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::PROJECT_NOT_STARTED)) ->atPath($pathEnd) ->setTranslationDomain('validators') diff --git a/src/Validator/Constraints/TimesheetBudgetUsedValidator.php b/src/Validator/Constraints/TimesheetBudgetUsedValidator.php index e17d93b2..cc87246f 100644 --- a/src/Validator/Constraints/TimesheetBudgetUsedValidator.php +++ b/src/Validator/Constraints/TimesheetBudgetUsedValidator.php @@ -206,7 +206,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator private function addBudgetViolation(TimesheetBudgetUsed $constraint, Timesheet $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()->getLanguage()); + $helper = new LocaleFormatter($this->localeService, $timesheet->getUser()?->getLocale() ?? 'en'); $currency = $timesheet->getProject()->getCustomer()->getCurrency(); $free = $budget - $rate; diff --git a/src/Validator/Constraints/TimesheetDeactivatedValidator.php b/src/Validator/Constraints/TimesheetDeactivatedValidator.php index 574c3380..4c5ece9d 100644 --- a/src/Validator/Constraints/TimesheetDeactivatedValidator.php +++ b/src/Validator/Constraints/TimesheetDeactivatedValidator.php @@ -33,14 +33,9 @@ final class TimesheetDeactivatedValidator extends ConstraintValidator $this->validateActivityAndProject($value, $this->context); } - /** - * @param TimesheetEntity $timesheet - * @param ExecutionContextInterface $context - */ - protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context): void + private function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context): void { - $timesheetEnd = $timesheet->getEnd(); - $newOrStarted = null === $timesheetEnd || $timesheet->getId() === null; + $newOrStarted = $timesheet->isRunning() || $timesheet->getId() === null; if (!$newOrStarted) { return; diff --git a/src/Voter/UserVoter.php b/src/Voter/UserVoter.php index b12335be..1f335a0b 100644 --- a/src/Voter/UserVoter.php +++ b/src/Voter/UserVoter.php @@ -102,6 +102,10 @@ final class UserVoter extends Voter return $subject->getId() === $user->getId() || $user->isSuperAdmin(); } + if ($attribute === 'supervisor' && $subject->getId() === $user->getId()) { + return $user->isSuperAdmin(); + } + $permission = $attribute; // extend me for "team" support later on diff --git a/templates/base.html.twig b/templates/base.html.twig index ec494eac..7e9731e9 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -63,7 +63,7 @@ {{ event.content|raw }} {% if page_setup is defined and page_setup.help is not null %} diff --git a/templates/contract/status.html.twig b/templates/contract/status.html.twig index 3c592fda..1476db5f 100644 --- a/templates/contract/status.html.twig +++ b/templates/contract/status.html.twig @@ -1,6 +1,11 @@ {% extends 'page_setup.html.twig' %} {% import "macros/widgets.html.twig" as widgets %} +{% block form_addon %} + {% set event = actions(app.user, 'contract_links', 'index', {year: year, user: user}) %} + {{ widgets.actions(event.actions, {button_class: '', large: false}) }} +{% endblock %} + {% block main %} {% set withWorkHourConfiguration = user.hasWorkHourConfiguration() %} diff --git a/templates/export/layout.html.twig b/templates/export/layout.html.twig index 8e5245f7..a5cd42ae 100644 --- a/templates/export/layout.html.twig +++ b/templates/export/layout.html.twig @@ -3,7 +3,7 @@ - + {{ encore_entry_link_tags('app') }} {{ encore_entry_script_tags('app') }} diff --git a/templates/macros/datatables.html.twig b/templates/macros/datatables.html.twig index bd7153e9..31397c4b 100644 --- a/templates/macros/datatables.html.twig +++ b/templates/macros/datatables.html.twig @@ -25,7 +25,7 @@ {% endif %}
{% if dataTable.hasConfiguration() %} - + {{ icon('columns', true) }} {% endif %} @@ -40,7 +40,7 @@ {% set order = form_widget(form.order) %} {% set filterCount = dataTable.getQuery().countFilter() %} diff --git a/templates/partials/recent-activities.html.twig b/templates/partials/recent-activities.html.twig index 13d8a582..b04df4d0 100644 --- a/templates/partials/recent-activities.html.twig +++ b/templates/partials/recent-activities.html.twig @@ -1,6 +1,6 @@ {% if is_granted('start_own_timesheet') %} diff --git a/templates/partials/ticktack.html.twig b/templates/partials/ticktack.html.twig index 8abb2348..9bf0edf5 100644 --- a/templates/partials/ticktack.html.twig +++ b/templates/partials/ticktack.html.twig @@ -12,13 +12,13 @@ {% endif %} {% set entry = active_timesheets[0] %}
- + {{ 0|duration }} diff --git a/templates/partials/user-shortcuts.html.twig b/templates/partials/user-shortcuts.html.twig index 8c99e292..ba2215c6 100644 --- a/templates/partials/user-shortcuts.html.twig +++ b/templates/partials/user-shortcuts.html.twig @@ -1,7 +1,7 @@ {% set user_shortcuts = user_shortcuts(app.user) %} {% if user_shortcuts|length > 0 %}