Release 2.10 (#4549)
This commit is contained in:
@@ -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
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
48
src/Command/MailTestCommand.php
Normal file
48
src/Command/MailTestCommand.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Event\EmailEvent;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
#[AsCommand(name: 'kimai:mail:test', description: 'Send a test email')]
|
||||
final class MailTestCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()]))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
28
src/EventSubscriber/NotificationsSubscriber.php
Normal file
28
src/EventSubscriber/NotificationsSubscriber.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use KevinPapst\TablerBundle\Event\NotificationEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class NotificationsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
NotificationEvent::class => ['onNotificationEvent', 100],
|
||||
];
|
||||
}
|
||||
|
||||
public function onNotificationEvent(NotificationEvent $event): void
|
||||
{
|
||||
$event->setShowBadgeTotal(false);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int, User> $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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<User> $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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user