Release 2.6.0 (#4472)
- Added: calendar entry title combination for customer, project and activity - Added: show not_invoiced and not_exported data in detail screens - Added: force logout if user is disabled - Added: reduced amount of database queries on several screens - Fixed: open-close status on work-contract screen for users without configuration - Fixed: failsafe order/orderBy in query if manually manipulated to be null - Fixed: unify statistic calculation (not_invoiced and not_exported) across screens - Tech: bump packages - Tech: Symfony 6.4
This commit is contained in:
@@ -38,7 +38,7 @@ final class SessionAuthenticator extends AbstractAuthenticator
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
if (str_contains($request->getRequestUri(), '/api/')) {
|
||||
// API docs can only be access, when the user is logged in
|
||||
@@ -62,7 +62,7 @@ final class SessionAuthenticator extends AbstractAuthenticator
|
||||
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
return $this->authenticator->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ final class TokenAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
if (str_contains($request->getRequestUri(), '/api/')) {
|
||||
return !str_contains($request->getRequestUri(), '/api/doc');
|
||||
@@ -95,7 +95,7 @@ final class TokenAuthenticator extends AbstractAuthenticator
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
$data = [
|
||||
'message' => $exception instanceof CustomUserMessageAuthenticationException ? $exception->getMessage() : 'Invalid credentials'
|
||||
|
||||
@@ -62,7 +62,7 @@ final class RecentActivitiesSource implements DragAndDropSource
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
public function getBlockInclude(): ?string
|
||||
public function getBlockInclude(): string
|
||||
{
|
||||
return 'calendar/drag-drop.html.twig';
|
||||
}
|
||||
|
||||
@@ -44,12 +44,14 @@ final class TimesheetEntry implements DragAndDropEntry
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
if ($this->timesheet->getActivity() !== null && $this->timesheet->getActivity()->getName() !== null) {
|
||||
return $this->timesheet->getActivity()->getName();
|
||||
$activity = $this->timesheet->getActivity();
|
||||
if ($activity !== null && $activity->getName() !== null) {
|
||||
return $activity->getName();
|
||||
}
|
||||
|
||||
if (null !== $this->timesheet->getProject() && $this->timesheet->getProject()->getName() !== null) {
|
||||
return $this->timesheet->getProject()->getName();
|
||||
$project = $this->timesheet->getProject();
|
||||
if ($project !== null && $project->getName() !== null) {
|
||||
return $project->getName();
|
||||
}
|
||||
|
||||
return $this->timesheet->getDescription() ?? '';
|
||||
@@ -60,7 +62,7 @@ final class TimesheetEntry implements DragAndDropEntry
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function getBlockName(): ?string
|
||||
public function getBlockName(): string
|
||||
{
|
||||
return 'dd_timesheet';
|
||||
}
|
||||
|
||||
@@ -482,10 +482,10 @@ final class SystemConfiguration
|
||||
return (bool) $this->find('theme.avatar_url');
|
||||
}
|
||||
|
||||
public function getThemeColorChoices(): ?string
|
||||
public function getThemeColorChoices(): string
|
||||
{
|
||||
$config = $this->find('theme.color_choices');
|
||||
if (!empty($config)) {
|
||||
if (\is_string($config) && $config !== '') {
|
||||
return $config;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.5.0';
|
||||
public const VERSION = '2.6.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20500;
|
||||
public const VERSION_ID = 20600;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,8 @@ use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
@@ -135,6 +137,22 @@ final class ActivityController extends AbstractController
|
||||
$defaultTeam = null;
|
||||
$now = $this->getDateTimeFactory()->createDateTime();
|
||||
|
||||
$exportUrl = null;
|
||||
$invoiceUrl = null;
|
||||
$params = ['customers[]' => '', 'projects[]' => '', 'activities[]' => $activity->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true];
|
||||
if ($activity->getProject() !== null) {
|
||||
$params['projects[]'] = $activity->getProject()->getId();
|
||||
if ($activity->getProject()->getCustomer() !== null) {
|
||||
$params['customers[]'] = $activity->getProject()->getCustomer()->getId();
|
||||
}
|
||||
}
|
||||
if ($this->isGranted('create_export')) {
|
||||
$exportUrl = $this->generateUrl('export', array_merge($params, ['preview' => true]));
|
||||
}
|
||||
if ($this->isGranted('view_invoice')) {
|
||||
$invoiceUrl = $this->generateUrl('invoice', $params);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $activity)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
||||
@@ -147,7 +165,9 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $activity) || $this->isGranted('details', $activity) || $this->isGranted('view_team')) {
|
||||
$teams = $activity->getTeams();
|
||||
$query = new TeamQuery();
|
||||
$query->addActivity($activity);
|
||||
$teams = $teamRepository->getTeamsForQuery($query);
|
||||
}
|
||||
|
||||
// additional boxes by plugins
|
||||
@@ -168,7 +188,9 @@ final class ActivityController extends AbstractController
|
||||
'team' => $defaultTeam,
|
||||
'teams' => $teams,
|
||||
'now' => $now,
|
||||
'boxes' => $boxes
|
||||
'boxes' => $boxes,
|
||||
'export_url' => $exportUrl,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace App\Controller\Auth;
|
||||
use App\Configuration\SamlConfigurationInterface;
|
||||
use App\Saml\SamlAuthFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
#[Route(path: '/saml')]
|
||||
final class SamlController extends AbstractController
|
||||
@@ -32,7 +32,7 @@ final class SamlController extends AbstractController
|
||||
}
|
||||
|
||||
$session = $request->getSession();
|
||||
$authErrorKey = Security::AUTHENTICATION_ERROR;
|
||||
$authErrorKey = SecurityRequestAttributes::AUTHENTICATION_ERROR;
|
||||
|
||||
$error = null;
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
@@ -307,6 +309,15 @@ final class CustomerController extends AbstractController
|
||||
$rates = [];
|
||||
$now = $this->getDateTimeFactory()->createDateTime();
|
||||
|
||||
$exportUrl = null;
|
||||
$invoiceUrl = null;
|
||||
if ($this->isGranted('create_export')) {
|
||||
$exportUrl = $this->generateUrl('export', ['customers[]' => $customer->getId(), 'projects[]' => '', 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'preview' => true, 'billable' => true]);
|
||||
}
|
||||
if ($this->isGranted('view_invoice')) {
|
||||
$invoiceUrl = $this->generateUrl('invoice', ['customers[]' => $customer->getId(), 'projects[]' => '', 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $customer)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||
@@ -328,7 +339,9 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $customer) || $this->isGranted('details', $customer) || $this->isGranted('view_team')) {
|
||||
$teams = $customer->getTeams();
|
||||
$query = new TeamQuery();
|
||||
$query->addCustomer($customer);
|
||||
$teams = $teamRepository->getTeamsForQuery($query);
|
||||
}
|
||||
|
||||
// additional boxes by plugins
|
||||
@@ -353,7 +366,9 @@ final class CustomerController extends AbstractController
|
||||
'customer_now' => new \DateTime('now', $timezone),
|
||||
'rates' => $rates,
|
||||
'now' => $now,
|
||||
'boxes' => $boxes
|
||||
'boxes' => $boxes,
|
||||
'export_url' => $exportUrl,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ final class ProfileController extends AbstractController
|
||||
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher, UserRepository $userRepository): Response
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($profile);
|
||||
$event = new PrepareUserEvent($profile, false);
|
||||
$dispatcher->dispatch($event);
|
||||
|
||||
$form = $this->createPreferencesForm($profile);
|
||||
|
||||
@@ -36,6 +36,8 @@ use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\Context;
|
||||
use App\Utils\DataTable;
|
||||
@@ -334,6 +336,15 @@ final class ProjectController extends AbstractController
|
||||
$rates = [];
|
||||
$now = $this->getDateTimeFactory()->createDateTime();
|
||||
|
||||
$exportUrl = null;
|
||||
$invoiceUrl = null;
|
||||
if ($this->isGranted('create_export') && $project->getCustomer() !== null) {
|
||||
$exportUrl = $this->generateUrl('export', ['customers[]' => $project->getCustomer()->getId(), 'projects[]' => $project->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'preview' => true, 'billable' => true]);
|
||||
}
|
||||
if ($this->isGranted('view_invoice') && $project->getCustomer() !== null) {
|
||||
$invoiceUrl = $this->generateUrl('invoice', ['customers[]' => $project->getCustomer()->getId(), 'projects[]' => $project->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $project)) {
|
||||
if ($this->isGranted('create_team')) {
|
||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||
@@ -351,7 +362,9 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $project) || $this->isGranted('details', $project) || $this->isGranted('view_team')) {
|
||||
$teams = $project->getTeams();
|
||||
$query = new TeamQuery();
|
||||
$query->addProject($project);
|
||||
$teams = $teamRepository->getTeamsForQuery($query);
|
||||
}
|
||||
|
||||
// additional boxes by plugins
|
||||
@@ -375,7 +388,9 @@ final class ProjectController extends AbstractController
|
||||
'teams' => $teams,
|
||||
'rates' => $rates,
|
||||
'now' => $now,
|
||||
'boxes' => $boxes
|
||||
'boxes' => $boxes,
|
||||
'export_url' => $exportUrl,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,14 +30,15 @@ final class ProjectDateRangeController extends AbstractController
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectDaterangeQuery($dateFactory->getStartOfMonth(), $user);
|
||||
$defaultStart = $dateFactory->getStartOfMonth();
|
||||
$query = new ProjectDaterangeQuery($defaultStart, $user);
|
||||
$form = $this->createFormForGetRequest(ProjectDateRangeForm::class, $query, [
|
||||
'timezone' => $user->getTimezone()
|
||||
]);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$dateRange = new DateRange(true);
|
||||
$dateRange->setBegin($query->getMonth());
|
||||
$dateRange->setBegin($query->getMonth() ?? $defaultStart);
|
||||
$dateRange->setEnd($dateFactory->getEndOfMonth($dateRange->getBegin()));
|
||||
|
||||
$projects = $service->findProjectsForDateRange($query, $dateRange);
|
||||
|
||||
@@ -64,7 +64,7 @@ final class PasswordResetController extends AbstractController
|
||||
$username = $request->request->get('username');
|
||||
$user = $this->userService->findUserByUsernameOrEmail($username);
|
||||
|
||||
if (null !== $user && !$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
if (!$user->isInternalUser()) {
|
||||
throw $this->createAccessDeniedException(
|
||||
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUserIdentifier(), $user->getAuth())
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\UserExporter;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
@@ -112,7 +113,7 @@ final class UserController extends AbstractController
|
||||
|
||||
#[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('create_user')]
|
||||
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository): Response
|
||||
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository, EventDispatcherInterface $dispatcher): Response
|
||||
{
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
$editForm = $this->getCreateUserForm($user);
|
||||
@@ -126,6 +127,14 @@ final class UserController extends AbstractController
|
||||
$userRepository->saveUser($user);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
try {
|
||||
$event = new PrepareUserEvent($user, false);
|
||||
$dispatcher->dispatch($event);
|
||||
$userRepository->saveUser($user);
|
||||
} catch (\Exception $ex) {
|
||||
// it should be no problem, if creating default user preferences fails
|
||||
}
|
||||
|
||||
return $this->redirectToRouteAfterCreate('user_profile_edit', ['username' => $user->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,9 +76,9 @@ final class WizardController extends AbstractController
|
||||
$userService->updateUser($user);
|
||||
|
||||
if ($data['reload'] === '1') {
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $user->getLanguage()]);
|
||||
} else {
|
||||
return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $data['language']]);
|
||||
return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $user->getLanguage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,15 +36,12 @@ trait MetaTableTypeTrait
|
||||
private ?string $name = null;
|
||||
/**
|
||||
* Value of the meta (custom) field
|
||||
*
|
||||
* ATTENTION:
|
||||
* This field can be used to temporary hold data in another format (e.g. array) during form transformation,
|
||||
*/
|
||||
#[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)]
|
||||
#[Assert\Length(max: 65535)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private mixed $value = null;
|
||||
private ?string $value = null;
|
||||
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
#[Assert\NotNull]
|
||||
private bool $visible = false;
|
||||
@@ -61,6 +58,14 @@ trait MetaTableTypeTrait
|
||||
*/
|
||||
private array $options = [];
|
||||
private int $order = 0;
|
||||
/**
|
||||
* Used for data conversion during form transformation.
|
||||
*
|
||||
* ATTENTION: This field can be used to temporary hold data in another format (e.g. array) during form transformation.
|
||||
* TODO unclear when "array" should happen. the above statement is old and maybe we can remove the "mixed” type
|
||||
*/
|
||||
private mixed $data = null;
|
||||
private bool $updated = false;
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
@@ -80,41 +85,47 @@ trait MetaTableTypeTrait
|
||||
|
||||
public function getValue(): mixed
|
||||
{
|
||||
if ($this->value === null) {
|
||||
$value = $this->updated ? $this->data : $this->value;
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($this->type) {
|
||||
YesNoType::class, CheckboxType::class => (\is_string($this->value) || \is_int($this->value)) ? (bool) $this->value : $this->value,
|
||||
IntegerType::class => (\is_string($this->value) || \is_int($this->value)) ? (int) $this->value : $this->value,
|
||||
NumberType::class => (\is_string($this->value) || \is_float($this->value)) ? (float) $this->value : $this->value,
|
||||
default => $this->value,
|
||||
YesNoType::class, CheckboxType::class => (\is_string($value) || \is_int($value)) ? (bool) $value : $value,
|
||||
IntegerType::class => (\is_string($value) || \is_int($value)) ? (int) $value : $value,
|
||||
NumberType::class => (\is_string($value) || \is_float($value)) ? (float) $value : $value,
|
||||
default => $value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Value will not be serialized before its stored, so it should be a primitive type.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return MetaTableTypeInterface
|
||||
* Value will not be serialized before its stored, so it should be a primitive/scalar type.
|
||||
*/
|
||||
public function setValue(mixed $value): MetaTableTypeInterface
|
||||
{
|
||||
$this->data = $value;
|
||||
$this->updated = true;
|
||||
|
||||
// unchecked checkboxes / false bool would save an empty string in the database
|
||||
// those cannot be searched in the database
|
||||
if (null !== $value) {
|
||||
switch ($this->type) {
|
||||
case YesNoType::class:
|
||||
case CheckboxType::class:
|
||||
if (\is_string($value) || \is_bool($value)) {
|
||||
$value = (int) $value;
|
||||
} else {
|
||||
if (!\is_int($value) && !\is_bool($value) && !\is_string($value)) {
|
||||
throw new \InvalidArgumentException('Failed converting meta-field bool value');
|
||||
} else {
|
||||
$value = (string) $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
if ($value === null) {
|
||||
$this->value = $value;
|
||||
} elseif (\is_scalar($value)) {
|
||||
$this->value = (string) $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -1023,6 +1023,10 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->enabled !== $user->isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,24 @@ use App\Entity\User;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if a user profile is loaded and want to fill the dynamic user preferences
|
||||
* To be used when a user profile is loaded and should be filled with dynamic user preferences.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class PrepareUserEvent extends Event
|
||||
{
|
||||
public function __construct(private User $user)
|
||||
public function __construct(private User $user, private bool $booting = true)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this event is dispatched for the currently logged in user during kernel boot.
|
||||
*/
|
||||
public function isBooting(): bool
|
||||
{
|
||||
return $this->booting;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
|
||||
@@ -14,18 +14,26 @@ use App\Entity\UserPreference;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if further user preferences should be added dynamically.
|
||||
* Add further user-preference definitions dynamically.
|
||||
* This is used on every page load, do not query the database when this is dispatched.
|
||||
*/
|
||||
final class UserPreferenceEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @param User $user
|
||||
* @param UserPreference[] $preferences
|
||||
* @param array<UserPreference> $preferences
|
||||
*/
|
||||
public function __construct(private User $user, private array $preferences)
|
||||
public function __construct(private readonly User $user, private array $preferences, private readonly bool $booting = true)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this event is dispatched for the currently logged in user during kernel boot.
|
||||
*/
|
||||
public function isBooting(): bool
|
||||
{
|
||||
return $this->booting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not set the preferences directly to the user object, but ONLY via addPreference()
|
||||
*
|
||||
|
||||
@@ -137,7 +137,7 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
$user = $event->getUser();
|
||||
|
||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user));
|
||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user), $event->isBooting());
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
foreach ($event->getPreferences() as $preference) {
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\DatePickerType;
|
||||
use App\Form\Type\DescriptionType;
|
||||
use App\Form\Type\DurationType;
|
||||
@@ -23,6 +25,7 @@ use App\Form\Type\TimesheetBillableType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Timesheet\Calculator\BillableCalculator;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
@@ -49,7 +52,6 @@ class TimesheetEditForm extends AbstractType
|
||||
$project = null;
|
||||
$customer = null;
|
||||
$currency = false;
|
||||
$customerCount = $this->customers->countCustomer(true);
|
||||
$timezone = $options['timezone'];
|
||||
$isNew = true;
|
||||
|
||||
@@ -100,15 +102,33 @@ class TimesheetEditForm extends AbstractType
|
||||
$this->addDuration($builder, $options, (!$options['allow_begin_datetime'] || !$options['allow_end_datetime']), $isNew);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($options['user']); // @phpstan-ignore-line
|
||||
$qb = $this->customers->getQueryBuilderForFormType($query);
|
||||
/** @var array<Customer> $customers */
|
||||
$customers = $qb->getQuery()->getResult();
|
||||
$customerCount = \count($customers);
|
||||
|
||||
if ($this->showCustomer($options, $isNew, $customerCount)) {
|
||||
$this->addCustomer($builder, $customer);
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'choices' => $customers,
|
||||
'data' => $customer,
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
'mapped' => false,
|
||||
'project_enabled' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$allowCreate = (bool) $this->systemConfiguration->find('activity.allow_inline_create');
|
||||
|
||||
// TODO pre-select if only one exists
|
||||
$this->addProject($builder, $isNew, $project, $customer);
|
||||
|
||||
// TODO make creation possible
|
||||
//$allowCreate = (bool) $this->systemConfiguration->find('activity.allow_inline_create');
|
||||
$this->addActivity($builder, $activity, $project, [
|
||||
'allow_create' => $allowCreate && $options['create_activity'],
|
||||
'allow_create' => false,
|
||||
// 'allow_create' => $allowCreate && $options['create_activity'],
|
||||
]);
|
||||
|
||||
$descriptionOptions = ['required' => false];
|
||||
@@ -394,7 +414,7 @@ class TimesheetEditForm extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$maxMinutes = $this->systemConfiguration->getTimesheetLongRunningDuration();
|
||||
$maxHours = 8;
|
||||
$maxHours = 10;
|
||||
if ($maxMinutes > 0) {
|
||||
$maxHours = (int) ($maxMinutes / 60);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ final class CalendarTitlePatternType extends AbstractType
|
||||
public const PATTERN_PROJECT_DESCRIPTION = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_DESCRIPTION;
|
||||
public const PATTERN_CUSTOMER_DESCRIPTION = self::PATTERN_CUSTOMER . self::SPACER . self::PATTERN_DESCRIPTION;
|
||||
public const PATTERN_PROJECT_CUSTOMER = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_CUSTOMER;
|
||||
public const PATTERN_CUSTOMER_PROJECT = self::PATTERN_CUSTOMER . self::SPACER . self::PATTERN_PROJECT;
|
||||
public const PATTERN_PROJECT_ACTIVITY = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_ACTIVITY;
|
||||
|
||||
public function __construct(private TranslatorInterface $translator)
|
||||
{
|
||||
@@ -54,6 +56,8 @@ final class CalendarTitlePatternType extends AbstractType
|
||||
$project . self::SPACER . $description => CalendarTitlePatternType::PATTERN_PROJECT_DESCRIPTION,
|
||||
$customer . self::SPACER . $description => CalendarTitlePatternType::PATTERN_CUSTOMER_DESCRIPTION,
|
||||
$project . self::SPACER . $customer => CalendarTitlePatternType::PATTERN_PROJECT_CUSTOMER,
|
||||
$customer . self::SPACER . $project => CalendarTitlePatternType::PATTERN_CUSTOMER_PROJECT,
|
||||
$project . self::SPACER . $activity => CalendarTitlePatternType::PATTERN_PROJECT_ACTIVITY,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@ final class ColorChoiceType extends AbstractType implements DataTransformerInter
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $config
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function convertStringToColorArray(string $config): array
|
||||
{
|
||||
$config = explode(',', $config);
|
||||
|
||||
@@ -16,6 +16,9 @@ use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* @template-implements UserProviderInterface<User>
|
||||
*/
|
||||
final class LdapUserProvider implements UserProviderInterface
|
||||
{
|
||||
public function __construct(private LdapManager $ldapManager, private ?LoggerInterface $logger = null)
|
||||
|
||||
@@ -727,13 +727,11 @@ class ProjectStatisticService
|
||||
$endMonth = (clone $startOfWeek)->modify('last day of this month');
|
||||
|
||||
$projectViews = [];
|
||||
foreach ($projects as $project) {
|
||||
$projectViews[$project->getId()] = new ProjectViewModel($project);
|
||||
}
|
||||
|
||||
$budgetStats = $this->getBudgetStatisticModelForProjects($projects, $today);
|
||||
foreach ($budgetStats as $model) {
|
||||
$projectViews[$model->getProject()->getId()]->setBudgetStatisticModel($model);
|
||||
$project = $model->getProject();
|
||||
$projectViews[$project->getId()] = new ProjectViewModel($model);
|
||||
}
|
||||
|
||||
$projectIds = array_keys($projectViews);
|
||||
@@ -741,7 +739,6 @@ class ProjectStatisticService
|
||||
$tplQb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$tplQb
|
||||
->select('IDENTITY(t.project) AS id')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) AS rate')
|
||||
->andWhere($tplQb->expr()->in('t.project', ':project'))
|
||||
@@ -749,14 +746,11 @@ class ProjectStatisticService
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
// find the most recent timesheet for each project
|
||||
$qb = clone $tplQb;
|
||||
$qb->addSelect('MAX(t.date) as lastRecord');
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationTotal($row['duration']);
|
||||
$projectViews[$row['id']]->setRateTotal($row['rate']);
|
||||
$projectViews[$row['id']]->setTimesheetCounter($row['amount']);
|
||||
if ($row['lastRecord'] !== null) {
|
||||
// might be the wrong timezone
|
||||
$projectViews[$row['id']]->setLastRecord($factory->createDateTime($row['lastRecord']));
|
||||
@@ -801,34 +795,6 @@ class ProjectStatisticService
|
||||
$projectViews[$row['id']]->setDurationMonth($row['duration']);
|
||||
}
|
||||
|
||||
$qb = clone $tplQb;
|
||||
$qb
|
||||
->addSelect('t.exported')
|
||||
->addSelect('t.billable')
|
||||
->addGroupBy('t.exported')
|
||||
->addGroupBy('t.billable')
|
||||
;
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
/** @var ProjectViewModel $view */
|
||||
$view = $projectViews[$row['id']];
|
||||
if ($row['billable'] === 1 && $row['exported'] === 1) {
|
||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
||||
} elseif ($row['billable'] === 1 && $row['exported'] === 0) {
|
||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
||||
$view->setNotBilledDuration($view->getNotBilledDuration() + $row['duration']);
|
||||
$view->setNotBilledRate($view->getNotBilledRate() + $row['rate']);
|
||||
} elseif ($row['billable'] === 0 && $row['exported'] === 0) {
|
||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
||||
}
|
||||
// the last possible case $row['billable'] === 0 && $row['exported'] === 1 is extremely unlikely and not used
|
||||
}
|
||||
|
||||
return array_values($projectViews);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ final class ProjectDateRangeForm extends AbstractType
|
||||
]);
|
||||
|
||||
$builder->add('month', MonthPickerType::class, [
|
||||
'required' => true,
|
||||
'label' => false,
|
||||
'view_timezone' => $options['timezone'],
|
||||
'model_timezone' => $options['timezone'],
|
||||
|
||||
@@ -14,16 +14,14 @@ use App\Entity\User;
|
||||
|
||||
final class ProjectDateRangeQuery
|
||||
{
|
||||
private \DateTime $month;
|
||||
private ?User $user;
|
||||
private ?\DateTime $month;
|
||||
private ?Customer $customer = null;
|
||||
private bool $includeNoWork = false;
|
||||
private ?string $budgetType = null;
|
||||
|
||||
public function __construct(\DateTime $month, User $user)
|
||||
public function __construct(\DateTime $month, private User $user)
|
||||
{
|
||||
$this->month = clone $month;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function isBudgetIndependent(): bool
|
||||
@@ -46,7 +44,7 @@ final class ProjectDateRangeQuery
|
||||
$this->includeNoWork = $includeNoWork;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
@@ -15,15 +15,13 @@ use DateTime;
|
||||
final class ProjectInactiveQuery
|
||||
{
|
||||
private DateTime $lastChange;
|
||||
private User $user;
|
||||
|
||||
public function __construct(DateTime $lastChange, User $user)
|
||||
public function __construct(DateTime $lastChange, private User $user)
|
||||
{
|
||||
$this->lastChange = clone $lastChange;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
@@ -11,42 +11,24 @@ namespace App\Reporting\ProjectView;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Model\BudgetStatisticModelInterface;
|
||||
use App\Model\ProjectBudgetStatisticModel;
|
||||
use App\Model\Statistic\BudgetStatistic;
|
||||
use DateTime;
|
||||
|
||||
final class ProjectViewModel
|
||||
{
|
||||
private int $timesheetCounter = 0;
|
||||
private int $durationDay = 0;
|
||||
private int $durationWeek = 0;
|
||||
private int $durationMonth = 0;
|
||||
private int $durationTotal = 0;
|
||||
private float $rateTotal = 0.00;
|
||||
private int $notExportedDuration = 0;
|
||||
private float $notExportedRate = 0.00;
|
||||
private int $notBilledDuration = 0;
|
||||
private float $notBilledRate = 0.00;
|
||||
private int $billableDuration = 0;
|
||||
private float $billableRate = 0.00;
|
||||
private ?DateTime $lastRecord = null;
|
||||
private ?BudgetStatisticModelInterface $budgetStatisticModel = null;
|
||||
|
||||
public function __construct(private Project $project)
|
||||
public function __construct(private ProjectBudgetStatisticModel $budgetStatisticModel)
|
||||
{
|
||||
}
|
||||
|
||||
public function getProject(): Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function getTimesheetCounter(): int
|
||||
{
|
||||
return $this->timesheetCounter;
|
||||
}
|
||||
|
||||
public function setTimesheetCounter(int $timesheetCounter): void
|
||||
{
|
||||
$this->timesheetCounter = $timesheetCounter;
|
||||
return $this->budgetStatisticModel->getProject();
|
||||
}
|
||||
|
||||
public function getDurationDay(): int
|
||||
@@ -79,84 +61,47 @@ final class ProjectViewModel
|
||||
$this->durationMonth = $durationMonth;
|
||||
}
|
||||
|
||||
public function getDurationTotal(): int
|
||||
private function getTotals(): BudgetStatistic
|
||||
{
|
||||
return $this->durationTotal;
|
||||
if ($this->budgetStatisticModel->getStatisticTotal() === null) {
|
||||
throw new \InvalidArgumentException('Totals must not be null');
|
||||
}
|
||||
|
||||
return $this->budgetStatisticModel->getStatisticTotal();
|
||||
}
|
||||
|
||||
public function setDurationTotal(int $durationTotal): void
|
||||
public function getDurationTotal(): int
|
||||
{
|
||||
$this->durationTotal = $durationTotal;
|
||||
return $this->getTotals()->getDuration();
|
||||
}
|
||||
|
||||
public function getNotExportedDuration(): int
|
||||
{
|
||||
return $this->notExportedDuration;
|
||||
}
|
||||
$totals = $this->getTotals();
|
||||
|
||||
public function setNotExportedDuration(int $notExportedDuration): void
|
||||
{
|
||||
$this->notExportedDuration = $notExportedDuration;
|
||||
return $totals->getDurationBillable() - $totals->getDurationBillableExported();
|
||||
}
|
||||
|
||||
public function getNotExportedRate(): float
|
||||
{
|
||||
return $this->notExportedRate;
|
||||
}
|
||||
$totals = $this->getTotals();
|
||||
|
||||
public function setNotExportedRate(float $notExportedRate): void
|
||||
{
|
||||
$this->notExportedRate = $notExportedRate;
|
||||
}
|
||||
|
||||
public function getNotBilledDuration(): int
|
||||
{
|
||||
return $this->notBilledDuration;
|
||||
}
|
||||
|
||||
public function setNotBilledDuration(int $notBilledDuration): void
|
||||
{
|
||||
$this->notBilledDuration = $notBilledDuration;
|
||||
}
|
||||
|
||||
public function getNotBilledRate(): float
|
||||
{
|
||||
return $this->notBilledRate;
|
||||
}
|
||||
|
||||
public function setNotBilledRate(float $notBilledRate): void
|
||||
{
|
||||
$this->notBilledRate = $notBilledRate;
|
||||
return $totals->getRateBillable() - $totals->getRateBillableExported();
|
||||
}
|
||||
|
||||
public function getBillableDuration(): int
|
||||
{
|
||||
return $this->billableDuration;
|
||||
}
|
||||
|
||||
public function setBillableDuration(int $billableDuration): void
|
||||
{
|
||||
$this->billableDuration = $billableDuration;
|
||||
return $this->getTotals()->getDurationBillable();
|
||||
}
|
||||
|
||||
public function getBillableRate(): float
|
||||
{
|
||||
return $this->billableRate;
|
||||
}
|
||||
|
||||
public function setBillableRate(float $billableRate): void
|
||||
{
|
||||
$this->billableRate = $billableRate;
|
||||
return $this->getTotals()->getRateBillable();
|
||||
}
|
||||
|
||||
public function getRateTotal(): float
|
||||
{
|
||||
return $this->rateTotal;
|
||||
}
|
||||
|
||||
public function setRateTotal(float $rateTotal): void
|
||||
{
|
||||
$this->rateTotal = $rateTotal;
|
||||
return $this->getTotals()->getRate();
|
||||
}
|
||||
|
||||
public function getLastRecord(): ?DateTime
|
||||
@@ -173,9 +118,4 @@ final class ProjectViewModel
|
||||
{
|
||||
return $this->budgetStatisticModel;
|
||||
}
|
||||
|
||||
public function setBudgetStatisticModel(BudgetStatisticModelInterface $budgetStatisticModel): void
|
||||
{
|
||||
$this->budgetStatisticModel = $budgetStatisticModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ final class ProjectViewQuery
|
||||
{
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
@@ -142,8 +142,12 @@ class BaseQuery
|
||||
return $this->orderBy;
|
||||
}
|
||||
|
||||
public function setOrderBy(string $orderBy): self
|
||||
public function setOrderBy(?string $orderBy): self
|
||||
{
|
||||
if ($orderBy === null) {
|
||||
$orderBy = (string) $this->defaults['orderBy']; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
$this->orderBy = $orderBy;
|
||||
|
||||
return $this;
|
||||
@@ -154,8 +158,12 @@ class BaseQuery
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function setOrder(string $order): self
|
||||
public function setOrder(?string $order): self
|
||||
{
|
||||
if ($order === null) {
|
||||
$order = (string) $this->defaults['order']; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
if (\in_array($order, [self::ORDER_ASC, self::ORDER_DESC])) {
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
|
||||
class TeamQuery extends BaseQuery
|
||||
@@ -19,12 +22,27 @@ class TeamQuery extends BaseQuery
|
||||
* @var User[]
|
||||
*/
|
||||
private array $users = [];
|
||||
/**
|
||||
* @var array<Customer>
|
||||
*/
|
||||
private array $customers = [];
|
||||
/**
|
||||
* @var array<Project>
|
||||
*/
|
||||
private array $projects = [];
|
||||
/**
|
||||
* @var array<Activity>
|
||||
*/
|
||||
private array $activities = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDefaults([
|
||||
'orderBy' => 'name',
|
||||
'users' => [],
|
||||
'customers' => [],
|
||||
'projects' => [],
|
||||
'activities' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -33,20 +51,16 @@ class TeamQuery extends BaseQuery
|
||||
return !empty($this->users);
|
||||
}
|
||||
|
||||
public function addUser(User $user): self
|
||||
public function addUser(User $user): void
|
||||
{
|
||||
$this->users[$user->getId()] = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUser(User $user): self
|
||||
public function removeUser(User $user): void
|
||||
{
|
||||
if (isset($this->users[$user->getId()])) {
|
||||
unset($this->users[$user->getId()]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,4 +70,82 @@ class TeamQuery extends BaseQuery
|
||||
{
|
||||
return array_values($this->users);
|
||||
}
|
||||
|
||||
public function hasCustomers(): bool
|
||||
{
|
||||
return \count($this->customers) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer[]
|
||||
*/
|
||||
public function getCustomers(): array
|
||||
{
|
||||
return $this->customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Customer> $customers
|
||||
*/
|
||||
public function setCustomers(array $customers): void
|
||||
{
|
||||
$this->customers = $customers;
|
||||
}
|
||||
|
||||
public function addCustomer(Customer $customer): void
|
||||
{
|
||||
$this->customers[] = $customer;
|
||||
}
|
||||
|
||||
public function hasProjects(): bool
|
||||
{
|
||||
return \count($this->projects) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project[]
|
||||
*/
|
||||
public function getProjects(): array
|
||||
{
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Project> $projects
|
||||
*/
|
||||
public function setProjects(array $projects): void
|
||||
{
|
||||
$this->projects = $projects;
|
||||
}
|
||||
|
||||
public function addProject(Project $project): void
|
||||
{
|
||||
$this->projects[] = $project;
|
||||
}
|
||||
|
||||
public function hasActivities(): bool
|
||||
{
|
||||
return \count($this->activities) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Activity[]
|
||||
*/
|
||||
public function getActivities(): array
|
||||
{
|
||||
return $this->activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Activity> $activities
|
||||
*/
|
||||
public function setActivities(array $activities): void
|
||||
{
|
||||
$this->activities = $activities;
|
||||
}
|
||||
|
||||
public function addActivity(Activity $activity): void
|
||||
{
|
||||
$this->activities[] = $activity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,12 +148,9 @@ class TeamRepository extends EntityRepository
|
||||
|
||||
private function getQueryBuilderForQuery(TeamQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb = $this->createQueryBuilder('t');
|
||||
|
||||
$qb
|
||||
->select('t')
|
||||
->from(Team::class, 't')
|
||||
;
|
||||
$qb->select('t');
|
||||
|
||||
$orderBy = $query->getOrderBy();
|
||||
switch ($orderBy) {
|
||||
@@ -162,6 +159,30 @@ class TeamRepository extends EntityRepository
|
||||
break;
|
||||
}
|
||||
|
||||
if ($query->hasCustomers()) {
|
||||
$qb->leftJoin('t.customers', 'qCustomers');
|
||||
$qb->orWhere(
|
||||
$qb->expr()->in('qCustomers', ':customers')
|
||||
);
|
||||
$qb->setParameter('customers', $query->getCustomers());
|
||||
}
|
||||
|
||||
if ($query->hasProjects()) {
|
||||
$qb->leftJoin('t.projects', 'qProjects');
|
||||
$qb->orWhere(
|
||||
$qb->expr()->in('qProjects', ':projects')
|
||||
);
|
||||
$qb->setParameter('projects', $query->getProjects());
|
||||
}
|
||||
|
||||
if ($query->hasActivities()) {
|
||||
$qb->leftJoin('t.activities', 'qActivities');
|
||||
$qb->orWhere(
|
||||
$qb->expr()->in('qActivities', ':activities')
|
||||
);
|
||||
$qb->setParameter('activities', $query->getActivities());
|
||||
}
|
||||
|
||||
if ($query->hasUsers()) {
|
||||
$qb->leftJoin('t.members', 'qMembers');
|
||||
$qb->orWhere(
|
||||
|
||||
@@ -35,6 +35,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
/**
|
||||
* @extends \Doctrine\ORM\EntityRepository<User>
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
* @template-implements UserProviderInterface<User>
|
||||
*/
|
||||
class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
|
||||
@@ -18,6 +18,9 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
final class SamlProvider
|
||||
{
|
||||
/**
|
||||
* @param UserProviderInterface<User> $userProvider
|
||||
*/
|
||||
public function __construct(
|
||||
private UserRepository $repository,
|
||||
private UserProviderInterface $userProvider,
|
||||
@@ -30,8 +33,10 @@ final class SamlProvider
|
||||
$user = null;
|
||||
|
||||
try {
|
||||
/** @var User $user */
|
||||
$user = $this->userProvider->loadUserByIdentifier($token->getUserIdentifier());
|
||||
if ($token->getUserIdentifier() !== null) {
|
||||
/** @var User $user */
|
||||
$user = $this->userProvider->loadUserByIdentifier($token->getUserIdentifier());
|
||||
}
|
||||
} catch (UserNotFoundException $e) {
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,14 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* @template-implements PasswordUpgraderInterface<User>
|
||||
* @template-implements UserProviderInterface<User>
|
||||
*/
|
||||
final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||
{
|
||||
private ?ChainUserProvider $provider = null;
|
||||
|
||||
/**
|
||||
* @param iterable|UserProviderInterface[] $providers
|
||||
* @param iterable<UserProviderInterface<User>> $providers
|
||||
*/
|
||||
public function __construct(private iterable $providers, private SystemConfiguration $configuration)
|
||||
{
|
||||
@@ -60,12 +61,12 @@ final class KimaiUserProvider implements UserProviderInterface, PasswordUpgrader
|
||||
|
||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||
{
|
||||
return $this->getInternalProvider()->loadUserByIdentifier($identifier);
|
||||
return $this->getInternalProvider()->loadUserByIdentifier($identifier); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user): UserInterface
|
||||
{
|
||||
return $this->getInternalProvider()->refreshUser($user);
|
||||
return $this->getInternalProvider()->refreshUser($user); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function supportsClass(string $class): bool
|
||||
|
||||
@@ -16,7 +16,6 @@ use App\Event\PageActionsEvent;
|
||||
use App\Event\ThemeEvent;
|
||||
use App\Event\ThemeJavascriptTranslationsEvent;
|
||||
use App\Utils\Color;
|
||||
use App\Utils\FormFormatConverter;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
@@ -110,11 +109,8 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
return (new Color())->getRandom($identifier);
|
||||
}
|
||||
|
||||
public function getTimePresets(string $timezone, string $format): array
|
||||
public function getTimePresets(string $timezone): array
|
||||
{
|
||||
$converter = new FormFormatConverter();
|
||||
$format = $converter->convert($format);
|
||||
|
||||
$intervalMinutes = $this->configuration->getTimesheetIncrementMinutes();
|
||||
|
||||
if ($intervalMinutes < 5) {
|
||||
@@ -123,17 +119,15 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
||||
|
||||
$maxMinutes = 24 * 60 - $intervalMinutes;
|
||||
|
||||
$date = new \DateTime('now', new \DateTimeZone($timezone));
|
||||
$date->setTime(0, 0, 0);
|
||||
$date = new \DateTimeImmutable('now', new \DateTimeZone($timezone));
|
||||
$date = $date->setTime(0, 0, 0);
|
||||
|
||||
$presets = [
|
||||
$date->format($format)
|
||||
];
|
||||
$presets = [$date];
|
||||
|
||||
for ($minutes = $intervalMinutes; $minutes <= $maxMinutes; $minutes += $intervalMinutes) {
|
||||
$date->modify('+' . $intervalMinutes . ' minutes');
|
||||
$date = $date->modify('+' . $intervalMinutes . ' minutes');
|
||||
|
||||
$presets[] = $date->format($format);
|
||||
$presets[] = $date;
|
||||
}
|
||||
|
||||
return $presets;
|
||||
|
||||
@@ -127,7 +127,7 @@ class UserService
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function findUserByUsernameOrEmail(string $usernameOrEmail): ?User
|
||||
public function findUserByUsernameOrEmail(string $usernameOrEmail): User
|
||||
{
|
||||
return $this->repository->loadUserByIdentifier($usernameOrEmail);
|
||||
}
|
||||
|
||||
@@ -57,14 +57,14 @@ final class SearchTerm
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
public function getSearchTerm(): ?string
|
||||
public function getSearchTerm(): string
|
||||
{
|
||||
return $this->term;
|
||||
}
|
||||
|
||||
public function hasSearchTerm(): bool
|
||||
{
|
||||
return !empty($this->term);
|
||||
return $this->term !== '';
|
||||
}
|
||||
|
||||
public function getOriginalSearch(): string
|
||||
|
||||
@@ -22,7 +22,7 @@ final class Customer extends Constraint
|
||||
|
||||
public string $message = 'This customer has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ final class DateTimeFormat extends Constraint
|
||||
public ?string $separator = null;
|
||||
public ?string $message = 'This datetime format is invalid.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::PROPERTY_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ final class Project extends Constraint
|
||||
|
||||
public string $message = 'This project has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ final class Team extends Constraint
|
||||
|
||||
public string $message = 'The team has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ final class TimeFormat extends Constraint
|
||||
|
||||
public string $message = 'This time format is invalid.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::PROPERTY_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ final class Timesheet extends Constraint
|
||||
{
|
||||
public string $message = 'This timesheet has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ final class TimesheetBasic extends TimesheetConstraint
|
||||
|
||||
public string $message = 'This timesheet has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ final class TimesheetDeactivated extends TimesheetConstraint
|
||||
|
||||
public string $message = 'This timesheet has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ final class TimesheetExported extends TimesheetConstraint
|
||||
*/
|
||||
public null|\DateTime|string $now;
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ final class TimesheetFutureTimes extends TimesheetConstraint
|
||||
|
||||
public string $message = 'The date cannot be in the future.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ final class TimesheetLockdown extends TimesheetConstraint
|
||||
*/
|
||||
public \DateTime|string|null $now;
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ final class TimesheetLongRunning extends TimesheetConstraint
|
||||
public string $message = 'Maximum duration of {{ value }} hours exceeded.';
|
||||
public string $maximumMessage = 'Maximum duration exceeded.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ final class TimesheetMultiUpdate extends Constraint
|
||||
|
||||
public string $message = 'This form has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ final class TimesheetMultiUser extends Constraint
|
||||
|
||||
public string $message = 'This form has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ final class TimesheetOverlapping extends TimesheetConstraint
|
||||
|
||||
public string $message = 'You already have an entry for this time.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ final class TimesheetRestart extends TimesheetConstraint
|
||||
|
||||
public string $message = 'You are not allowed to start this timesheet record.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ final class TimesheetZeroDuration extends TimesheetConstraint
|
||||
|
||||
public string $message = 'Duration cannot be zero.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ final class User extends Constraint
|
||||
|
||||
public string $message = 'The user has invalid settings.';
|
||||
|
||||
public function getTargets(): string|array
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user