Release 2.0.18 (#4003)

* prevent too long values for user preferences
* add chart value 0 to y-axis if last value > 0, to always display the zero line (project details)
* add user-preferences to invoice hydrator
* DateTime vs DateTimeInterface
* simplify permission config
* fix query for teamleads limiting to only selected teams not possible
This commit is contained in:
Kevin Papst
2023-05-08 17:11:55 +02:00
committed by GitHub
parent d8f72b8b88
commit 1099e76244
74 changed files with 627 additions and 621 deletions

View File

@@ -17,6 +17,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* @method null|User getUser()
@@ -27,7 +28,15 @@ abstract class BaseApiController extends AbstractController
public const DATE_FORMAT = DateTimeType::HTML5_FORMAT;
public const DATE_FORMAT_PHP = 'Y-m-d\TH:i:s';
protected function createSearchForm(string $type = FormType::class, $data = null, array $options = []): FormInterface
/**
* @template TFormType of FormTypeInterface<TData>
* @template TData of mixed
* @param class-string<TFormType> $type
* @param TData|null $data
* @param array<mixed> $options
* @return FormInterface<TData|null>
*/
protected function createSearchForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
{
return $this->container
->get('form.factory')

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.0.17';
public const VERSION = '2.0.18';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20017;
public const VERSION_ID = 20018;
/**
* The software name
*/

View File

@@ -20,6 +20,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstract
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
@@ -47,19 +48,41 @@ abstract class AbstractController extends BaseAbstractController implements Serv
return $this->container->get('translator');
}
protected function createSearchForm(string $type = FormType::class, $data = null, array $options = []): FormInterface
/**
* @template TFormType of FormTypeInterface<TData>
* @template TData of mixed
* @param class-string<TFormType> $type
* @param TData $data
* @param array<mixed> $options
* @return FormInterface<TData>
*/
protected function createSearchForm(string $type, mixed $data, array $options = []): FormInterface
{
return $this->createFormForGetRequest($type, $data, $options);
}
protected function createFormForGetRequest(string $type = FormType::class, $data = null, array $options = []): FormInterface
/**
* @template TFormType of FormTypeInterface<TData>
* @template TData of mixed
* @param class-string<TFormType> $type
* @param TData $data
* @param array<mixed> $options
* @return FormInterface<TData>
*/
protected function createFormForGetRequest(string $type, mixed $data, array $options = []): FormInterface
{
return $this->container
->get('form.factory')
->createNamed('', $type, $data, array_merge(['method' => 'GET'], $options));
return $this->container->get('form.factory')->createNamed('', $type, $data, array_merge(['method' => 'GET'], $options)); // @phpstan-ignore-line
}
protected function createFormWithName(string $name, string $type, mixed $data = null, array $options = []): FormInterface
/**
* @template TFormType of FormTypeInterface<TData>
* @template TData of mixed
* @param class-string<TFormType> $type
* @param TData|null $data
* @param array<mixed> $options
* @return FormInterface<TData|null>
*/
protected function createFormWithName(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
{
return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
}
@@ -166,6 +189,9 @@ abstract class AbstractController extends BaseAbstractController implements Serv
/**
* Handles exception flash messages for failed update/create actions.
* @param \Exception $exception
* @param FormInterface $form
* @return void
*/
protected function handleFormUpdateException(\Exception $exception, FormInterface $form): void
{

View File

@@ -414,6 +414,10 @@ final class ActivityController extends AbstractController
return $writer->getFileResponse($spreadsheet);
}
/**
* @param ActivityQuery $query
* @return FormInterface<ActivityQuery>
*/
private function getToolbarForm(ActivityQuery $query): FormInterface
{
return $this->createSearchForm(ActivityToolbarForm::class, $query, [
@@ -423,6 +427,10 @@ final class ActivityController extends AbstractController
]);
}
/**
* @param Activity $activity
* @return FormInterface<ActivityEditForm>
*/
private function createEditForm(Activity $activity): FormInterface
{
$currency = $this->configuration->getCustomerDefaultCurrency();

View File

@@ -505,6 +505,10 @@ final class CustomerController extends AbstractController
]);
}
/**
* @param CustomerQuery $query
* @return FormInterface<CustomerQuery>
*/
private function getToolbarForm(CustomerQuery $query): FormInterface
{
return $this->createSearchForm(CustomerToolbarForm::class, $query, [
@@ -514,6 +518,10 @@ final class CustomerController extends AbstractController
]);
}
/**
* @param CustomerComment $comment
* @return FormInterface<CustomerComment>
*/
private function getCommentForm(CustomerComment $comment): FormInterface
{
if (null === $comment->getId()) {
@@ -526,6 +534,10 @@ final class CustomerController extends AbstractController
]);
}
/**
* @param Customer $customer
* @return FormInterface<Customer>
*/
private function createEditForm(Customer $customer): FormInterface
{
$event = new CustomerMetaDefinitionEvent($customer);

View File

@@ -157,6 +157,11 @@ final class ExportController extends AbstractController
return $this->export->getExportItems($query);
}
/**
* @param ExportQuery $query
* @param string $method
* @return FormInterface<ExportQuery>
*/
private function getToolbarForm(ExportQuery $query, string $method): FormInterface
{
return $this->createSearchForm(ExportToolbarForm::class, $query, [

View File

@@ -17,7 +17,7 @@ use App\Model\Statistic\StatisticDate;
use App\Repository\ActivityRepository;
use App\Repository\ProjectRepository;
use App\Timesheet\TimesheetStatisticService;
use DateTime;
use DateTimeInterface;
abstract class AbstractUserReportController extends AbstractController
{
@@ -35,17 +35,17 @@ abstract class AbstractUserReportController extends AbstractController
return true;
}
protected function getStatisticDataRaw(DateTime $begin, DateTime $end, User $user): array
protected function getStatisticDataRaw(DateTimeInterface $begin, DateTimeInterface $end, User $user): array
{
return $this->statisticService->getDailyStatisticsGrouped($begin, $end, [$user]);
}
protected function createStatisticModel(DateTime $begin, DateTime $end, User $user): DateStatisticInterface
protected function createStatisticModel(DateTimeInterface $begin, DateTimeInterface $end, User $user): DateStatisticInterface
{
return new DailyStatistic($begin, $end, $user);
}
protected function prepareReport(DateTime $begin, DateTime $end, User $user): array
protected function prepareReport(DateTimeInterface $begin, DateTimeInterface $end, User $user): array
{
$data = $this->getStatisticDataRaw($begin, $end, $user);

View File

@@ -105,7 +105,9 @@ final class ReportUsersYearController extends AbstractController
$values->setDate(clone $defaultDate);
}
/** @var \DateTime $start */
$start = $values->getDate();
// there is a potential edge case bug for financial years:
// the last month will be skipped, if the financial year started on a different day than the first
$end = $dateTimeFactory->createEndOfFinancialYear($start);

View File

@@ -16,6 +16,7 @@ use App\Model\MonthlyStatistic;
use App\Reporting\YearByUser\YearByUser;
use App\Reporting\YearByUser\YearByUserForm;
use DateTime;
use DateTimeInterface;
use Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -75,6 +76,7 @@ final class UserYearController extends AbstractUserReportController
$values->setDate(clone $defaultDate);
}
/** @var \DateTimeInterface $start */
$start = $values->getDate();
// there is a potential edge case bug for financial years:
// the last month will be skipped, if the financial year started on a different day than the first
@@ -82,10 +84,10 @@ final class UserYearController extends AbstractUserReportController
$selectedUser = $values->getUser();
$previous = clone $start;
$previous = DateTime::createFromInterface($start);
$previous->modify('-1 year');
$next = clone $start;
$next = DateTime::createFromInterface($start);
$next->modify('+1 year');
$data = $this->prepareReport($start, $end, $selectedUser);
@@ -105,12 +107,12 @@ final class UserYearController extends AbstractUserReportController
];
}
protected function getStatisticDataRaw(DateTime $begin, DateTime $end, User $user): array
protected function getStatisticDataRaw(DateTimeInterface $begin, DateTimeInterface $end, User $user): array
{
return $this->statisticService->getMonthlyStatisticsGrouped($begin, $end, [$user]);
}
protected function createStatisticModel(DateTime $begin, DateTime $end, User $user): DateStatisticInterface
protected function createStatisticModel(DateTimeInterface $begin, DateTimeInterface $end, User $user): DateStatisticInterface
{
return new MonthlyStatistic($begin, $end, $user);
}

View File

@@ -152,9 +152,6 @@ final class AppExtension extends Extension
// permissions as well, which are off by default for all roles
foreach ($config['sets'] as $set => $permNames) {
foreach ($permNames as $name) {
if (str_starts_with($name, '@') || str_starts_with($name, '!')) {
continue;
}
$names[$name] = true;
}
}
@@ -169,9 +166,7 @@ final class AppExtension extends Extension
$exception->setPath('kimai.permissions.maps.' . $role);
throw $exception;
}
$roles[$role] = array_merge($roles[$role] ?? [], $this->getFilteredPermissions(
$this->extractSinglePermissionsFromSet($config, $set)
));
$roles[$role] = array_merge($roles[$role] ?? [], $this->extractSinglePermissionsFromSet($config, $set));
}
}
@@ -182,7 +177,7 @@ final class AppExtension extends Extension
$roles[$name][$name2] = true;
}
}
$config['roles'][$name] = $this->getFilteredPermissions($roles[$name]);
$config['roles'][$name] = $roles[$name];
}
// make sure to apply all other permissions that might have been registered through plugins
@@ -205,21 +200,6 @@ final class AppExtension extends Extension
$container->setParameter('kimai.permission_roles', array_map('strtoupper', array_values(array_unique($roles))));
}
private function getFilteredPermissions(array $permissions): array
{
$deleteFromArray = array_filter($permissions, function ($permission): bool {
return $permission[0] === '!';
}, ARRAY_FILTER_USE_KEY);
return array_filter($permissions, function ($permission) use ($deleteFromArray): bool {
if ($permission[0] === '!') {
return false;
}
return !\array_key_exists('!' . $permission, $deleteFromArray);
}, ARRAY_FILTER_USE_KEY);
}
private function extractSinglePermissionsFromSet(array $permissions, string $name): array
{
if (!isset($permissions['sets'][$name])) {
@@ -229,14 +209,7 @@ final class AppExtension extends Extension
$result = [];
foreach ($permissions['sets'][$name] as $permissionName) {
if ($permissionName[0] === '@') {
$result = array_merge(
$result,
$this->extractSinglePermissionsFromSet($permissions, substr($permissionName, 1))
);
} else {
$result[$permissionName] = true;
}
$result[$permissionName] = true;
}
return $result;

View File

@@ -47,6 +47,7 @@ class UserPreference
#[Serializer\Groups(['Default'])]
private string $name;
#[ORM\Column(name: 'value', type: 'string', length: 255, nullable: true)]
#[Assert\Length(max: 250)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?string $value;

View File

@@ -24,6 +24,9 @@ use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<Customer>
*/
class CustomerEditForm extends AbstractType
{
use EntityFormTrait;

View File

@@ -17,6 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the activities.
* @extends AbstractType<ActivityQuery>
*/
final class ActivityToolbarForm extends AbstractType
{

View File

@@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the customer.
* @extends AbstractType<CustomerQuery>
*/
final class CustomerToolbarForm extends AbstractType
{

View File

@@ -17,6 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering timesheet entries for exports.
* @extends AbstractType<ExportQuery>
*/
final class ExportToolbarForm extends AbstractType
{

View File

@@ -17,6 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering timesheet entries for invoices.
* @extends AbstractType<InvoiceArchiveQuery>
*/
final class InvoiceArchiveForm extends AbstractType
{

View File

@@ -18,6 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering timesheet entries for invoices.
* @extends AbstractType<InvoiceQuery>
*/
final class InvoiceToolbarForm extends AbstractType
{

View File

@@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the projects.
* @extends AbstractType<ProjectQuery>
*/
final class ProjectToolbarForm extends AbstractType
{

View File

@@ -14,6 +14,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<TagQuery>
*/
final class TagToolbarForm extends AbstractType
{
use ToolbarFormTrait;

View File

@@ -14,6 +14,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<TeamQuery>
*/
final class TeamToolbarForm extends AbstractType
{
use ToolbarFormTrait;

View File

@@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the timesheet.
* @extends AbstractType<TimesheetQuery>
*/
final class TimesheetExportToolbarForm extends AbstractType
{

View File

@@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the timesheet.
* @extends AbstractType<TimesheetQuery>
*/
final class TimesheetToolbarForm extends AbstractType
{

View File

@@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the user.
* @extends AbstractType<UserQuery>
*/
final class UserToolbarForm extends AbstractType
{

View File

@@ -14,6 +14,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<TeamMember>
*/
final class TeamMemberType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -18,6 +18,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<Team>
*/
final class TeamType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void

View File

@@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select if a timesheet is billable.
* @extends AbstractType<string>
*/
final class TimesheetBillableType extends AbstractType
{

View File

@@ -13,6 +13,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TimezoneType as BaseTimezoneType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<string>
*/
final class TimezoneType extends AbstractType
{
public function getBlockPrefix(): string

View File

@@ -16,6 +16,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Custom form field type to select the user language.
* @extends AbstractType<string>
*/
final class UserLanguageType extends AbstractType
{

View File

@@ -13,15 +13,19 @@ use App\Entity\UserPreference;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Custom form field type to edit a user preference.
* @extends AbstractType<UserPreference>
*/
final class UserPreferenceType extends AbstractType
{
@@ -46,6 +50,10 @@ final class UserPreferenceType extends AbstractType
unset($options['required']);
}
if (\in_array($preference->getType(), [TextType::class, TextareaType::class])) {
$constraints[] = new Length(['max' => 255]);
}
if (\in_array($preference->getType(), [CheckboxType::class, YesNoType::class])) {
$required = false;
}

View File

@@ -9,6 +9,7 @@
namespace App\Form\Type;
use App\Entity\User;
use App\Entity\UserPreference;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Form\AbstractType;
@@ -20,6 +21,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to edit user preferences.
* @extends AbstractType<User>
*/
final class UserPreferencesCollectionType extends AbstractType
{

View File

@@ -18,6 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a user role.
* @extends AbstractType<User>
*/
final class UserRoleType extends AbstractType
{

View File

@@ -21,6 +21,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a user.
* @extends AbstractType<User>
*/
final class UserType extends AbstractType
{

View File

@@ -16,6 +16,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a visibility.
* @extends AbstractType<int>
*/
final class VisibilityType extends AbstractType
{

View File

@@ -17,6 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select weekdays.
* @extends AbstractType<string>
*/
final class WeekDaysType extends AbstractType
{

View File

@@ -17,8 +17,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a week via picker and select previous and next week.
*
* Always falls back to the current week if none or an invalid date is given.
* @extends AbstractType<\DateTimeInterface|null>
*/
final class WeekPickerType extends AbstractType
{
@@ -28,19 +28,22 @@ final class WeekPickerType extends AbstractType
'widget' => 'single_text',
'html5' => false,
'format' => DateType::HTML5_FORMAT,
'start_date' => new \DateTime(),
'start_date' => new \DateTimeImmutable(),
]);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
/** @var \DateTime|null $date */
/** @var \DateTimeInterface|null $date */
$date = $form->getData();
if (null === $date) {
/** @var \DateTimeImmutable $date */
$date = $options['start_date'];
}
$date = \DateTime::createFromInterface($date);
$view->vars['week'] = $date;
$view->vars['previousWeek'] = (clone $date)->modify('-1 week');
$view->vars['nextWeek'] = (clone $date)->modify('+1 week');

View File

@@ -17,8 +17,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a year via picker and select previous and next year.
*
* Always falls back to the current year if none or an invalid date is given.
* @extends AbstractType<\DateTimeInterface|null>
*/
final class YearPickerType extends AbstractType
{
@@ -28,20 +28,23 @@ final class YearPickerType extends AbstractType
'widget' => 'single_text',
'html5' => false,
'format' => DateType::HTML5_FORMAT,
'start_date' => new \DateTime(),
'start_date' => new \DateTimeImmutable(),
'show_range' => false,
]);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
/** @var \DateTime|null $date */
/** @var \DateTimeInterface|null $date */
$date = $form->getData();
if (null === $date) {
if ($date === null) {
/** @var \DateTimeImmutable $date */
$date = $options['start_date'];
}
$date = \DateTime::createFromInterface($date);
$view->vars['year'] = $date;
$view->vars['show_range'] = $options['show_range'];
$view->vars['previousYear'] = (clone $date)->modify('-1 year');

View File

@@ -15,6 +15,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select between Yes and No.
* @extends AbstractType<bool>
*/
final class YesNoType extends AbstractType
{

View File

@@ -18,6 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to set the users API token.
* @extends AbstractType<User>
*/
final class UserApiTokenType extends AbstractType
{

View File

@@ -23,6 +23,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to edit the profile of a User.
* @extends AbstractType<User>
*/
class UserEditType extends AbstractType
{

View File

@@ -17,7 +17,8 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to set the users password.
* Defines the form used to set the user password.
* @extends AbstractType<User>
*/
final class UserPasswordType extends AbstractType
{

View File

@@ -17,6 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to edit the user preferences.
* @extends AbstractType<User>
*/
final class UserPreferencesForm extends AbstractType
{

View File

@@ -17,6 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to set roles for a User.
* @extends AbstractType<User>
*/
final class UserRolesType extends AbstractType
{

View File

@@ -17,6 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to assign a User to teams.
* @extends AbstractType<User>
*/
final class UserTeamsType extends AbstractType
{

View File

@@ -20,6 +20,9 @@ use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @extends AbstractType<TotpActivation>
*/
final class UserTwoFactorType extends AbstractType
{
public function __construct(private TotpAuthenticatorInterface $totpAuthenticator)

View File

@@ -93,6 +93,10 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
'entry.user_alias' => $user->getAlias() ?? '',
'entry.user_display' => $user->getDisplayName() ?? '',
]);
foreach ($user->getVisiblePreferences() as $pref) {
$values['entry.user_preference.' . $pref->getName()] = $pref->getValue();
}
}
if (null !== $activity) {

View File

@@ -19,12 +19,12 @@ final class MonthlyStatistic implements DateStatisticInterface
/**
* @var array<string|int, array<int<1, 12>, StatisticDate>>
*/
private $years = [];
private $begin;
private $end;
private $user;
private array $years = [];
private DateTimeInterface $begin;
private DateTimeInterface $end;
private User $user;
public function __construct(DateTime $begin, DateTime $end, User $user)
public function __construct(DateTimeInterface $begin, DateTimeInterface $end, User $user)
{
$this->begin = clone $begin;
$this->end = clone $end;
@@ -43,7 +43,7 @@ final class MonthlyStatistic implements DateStatisticInterface
}
$years = [];
$begin = clone $this->begin;
$begin = DateTime::createFromInterface($this->begin);
$begin->setTime(0, 0, 0);
$tmp = clone $begin;

View File

@@ -13,20 +13,17 @@ use App\Entity\Team;
abstract class AbstractUserList
{
private $date;
private $decimal = false;
private $sumType = 'duration';
/**
* @var Team|null
*/
private $team = null;
private ?\DateTimeInterface $date = null;
private bool $decimal = false;
private string $sumType = 'duration';
private ?Team $team = null;
public function getDate(): ?\DateTime
public function getDate(): ?\DateTimeInterface
{
return $this->date;
}
public function setDate(\DateTime $date): void
public function setDate(\DateTimeInterface $date): void
{
$this->date = $date;
}

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<CustomerMonthlyProjects>
*/
final class CustomerMonthlyProjectsForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<MonthByUser>
*/
final class MonthByUserForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<MonthlyUserList>
*/
final class MonthlyUserListForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -17,6 +17,9 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<ProjectDateRangeQuery>
*/
final class ProjectDateRangeForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<ProjectDetailsQuery>
*/
final class ProjectDetailsForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -14,6 +14,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<ProjectInactiveQuery>
*/
final class ProjectInactiveForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<ProjectViewQuery>
*/
final class ProjectViewForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<WeekByUser>
*/
final class WeekByUserForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<WeeklyUserList>
*/
final class WeeklyUserListForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<YearByUser>
*/
final class YearByUserForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -16,6 +16,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<YearlyUserList>
*/
final class YearlyUserListForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void

View File

@@ -77,6 +77,11 @@ class BaseQuery
return $this;
}
public function hasTeams(): bool
{
return \count($this->teams) > 0;
}
/**
* @return Team[]
*/

View File

@@ -515,9 +515,11 @@ class TimesheetRepository extends EntityRepository
// if teams are used and the user is not a teamlead, the list of users would be empty and then leading to NOT limit the select by user IDs
$user[] = $currentUser;
foreach ($currentUser->getTeams() as $team) {
if ($currentUser->isTeamleadOf($team)) {
$query->addTeam($team);
if (!$query->hasTeams()) {
foreach ($currentUser->getTeams() as $team) {
if ($currentUser->isTeamleadOf($team)) {
$query->addTeam($team);
}
}
}
}

View File

@@ -14,6 +14,7 @@ use App\Model\DailyStatistic;
use App\Model\MonthlyStatistic;
use App\Repository\TimesheetRepository;
use DateTime;
use DateTimeInterface;
final class TimesheetStatisticService
{
@@ -22,12 +23,12 @@ final class TimesheetStatisticService
}
/**
* @param DateTime $begin
* @param DateTime $end
* @param DateTimeInterface $begin
* @param DateTimeInterface $end
* @param User[] $users
* @return DailyStatistic[]
*/
public function getDailyStatistics(DateTime $begin, DateTime $end, array $users): array
public function getDailyStatistics(DateTimeInterface $begin, DateTimeInterface $end, array $users): array
{
/** @var DailyStatistic[] $stats */
$stats = [];
@@ -86,12 +87,12 @@ final class TimesheetStatisticService
/**
* @internal only for core development
* @param DateTime $begin
* @param DateTime $end
* @param DateTimeInterface $begin
* @param DateTimeInterface $end
* @param User[] $users
* @return array
*/
public function getDailyStatisticsGrouped(DateTime $begin, DateTime $end, array $users): array
public function getDailyStatisticsGrouped(DateTimeInterface $begin, DateTimeInterface $end, array $users): array
{
$stats = [];
$usersById = [];
@@ -163,12 +164,12 @@ final class TimesheetStatisticService
/**
* @internal only for core development
* @param DateTime $begin
* @param DateTime $end
* @param DateTimeInterface $begin
* @param DateTimeInterface $end
* @param User[] $users
* @return array
*/
public function getMonthlyStatisticsGrouped(DateTime $begin, DateTime $end, array $users): array
public function getMonthlyStatisticsGrouped(DateTimeInterface $begin, DateTimeInterface $end, array $users): array
{
$stats = [];
$usersById = [];

View File

@@ -19,6 +19,8 @@ use Twig\TwigTest;
final class Extensions extends AbstractExtension
{
public const REPORT_DATE = 'Y-m-d';
/**
* {@inheritdoc}
*/
@@ -56,9 +58,9 @@ final class Extensions extends AbstractExtension
];
}
public function formatReportDate(\DateTime $dateTime): string
public function formatReportDate(\DateTimeInterface $dateTime): string
{
return $dateTime->format('Y-m-d');
return $dateTime->format(self::REPORT_DATE);
}
public function getIsoDayByName(string $weekDay): int

View File

@@ -9,6 +9,7 @@
namespace App\Utils;
use App\Form\MultiUpdate\MultiUpdateTable;
use App\Repository\Query\BaseQuery;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
@@ -18,6 +19,9 @@ final class DataTable implements \Countable, \IteratorAggregate
{
private ?Pagination $pagination = null;
private ?FormInterface $searchForm = null;
/**
* @var FormInterface<MultiUpdateTable>|null
*/
private ?FormInterface $batchForm = null;
private array $columns = [];
private array $reloadEvents = [];
@@ -79,6 +83,10 @@ final class DataTable implements \Countable, \IteratorAggregate
return $this->batchForm?->createView();
}
/**
* @param FormInterface<MultiUpdateTable>|null $batchForm
* @return void
*/
public function setBatchForm(?FormInterface $batchForm): void
{
$this->batchForm = $batchForm;

View File

@@ -13,6 +13,9 @@ use Pagerfanta\View\Template\TwitterBootstrap5Template;
final class PaginationTemplate extends TwitterBootstrap5Template
{
/**
* @return array<string, string>
*/
protected function getDefaultOptions(): array
{
return array_merge(