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:
@@ -34,7 +34,7 @@
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^0.14",
|
||||
"kevinpapst/tabler-bundle": "^0.15",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
|
||||
729
composer.lock
generated
729
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -105,28 +105,21 @@ kimai:
|
||||
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
||||
LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet']
|
||||
REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting']
|
||||
# some single default definitions for roles
|
||||
SINGLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
|
||||
SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member']
|
||||
SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data']
|
||||
SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data']
|
||||
# link above sets to one complete set for each user role
|
||||
ROLE_USER: ['@TIMESHEET','@PROFILE','@SINGLE_USER']
|
||||
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAMLEAD','@PROJECTS_TEAMLEAD','@CUSTOMERS_TEAMLEAD','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@BILLABLE','@TAGS','@REPORTING','@SINGLE_TEAMLEAD']
|
||||
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_ADMIN','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@BILLABLE','@TAGS','@LOCKDOWN','@REPORTING','@SINGLE_ADMIN']
|
||||
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_ADMIN','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@BILLABLE','@TAGS','@LOCKDOWN','@REPORTING','@SINGLE_SUPER_ADMIN']
|
||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||
# permissions which are deactivated, as these features are hidden for now
|
||||
# brave users can try to activate them and be surprised what happens
|
||||
REGISTER_BETA: []
|
||||
# mapping a "role name" to an array of "set names"
|
||||
maps:
|
||||
ROLE_USER: ['ROLE_USER']
|
||||
ROLE_TEAMLEAD: ['ROLE_TEAMLEAD']
|
||||
ROLE_ADMIN: ['ROLE_ADMIN']
|
||||
ROLE_SUPER_ADMIN: ['ROLE_SUPER_ADMIN']
|
||||
# add or remove single permissions
|
||||
ROLE_USER: ['TIMESHEET','PROFILE']
|
||||
ROLE_TEAMLEAD: ['ACTIVITIES_TEAMLEAD','PROJECTS_TEAMLEAD','CUSTOMERS_TEAMLEAD','TIMESHEET_OTHER','INVOICE','TIMESHEET','PROFILE','EXPORT','BILLABLE','TAGS','REPORTING']
|
||||
ROLE_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING']
|
||||
ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING']
|
||||
# mapping a "role name" to an array of "permission names"
|
||||
roles:
|
||||
ROLE_USER: []
|
||||
ROLE_TEAMLEAD: []
|
||||
ROLE_ADMIN: []
|
||||
ROLE_SUPER_ADMIN: []
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data']
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
131
phpstan.neon
131
phpstan.neon
@@ -7,6 +7,12 @@ includes:
|
||||
- %rootDir%/../phpstan/conf/bleedingEdge.neon
|
||||
|
||||
parameters:
|
||||
featureToggles:
|
||||
skipCheckGenericClasses:
|
||||
- Symfony\Component\Form\AbstractType
|
||||
- Symfony\Component\Form\FormInterface
|
||||
- Symfony\Component\Form\FormTypeExtensionInterface
|
||||
- Symfony\Component\Form\FormTypeInterface
|
||||
level: 9
|
||||
strictRules:
|
||||
allRules: false
|
||||
@@ -55,16 +61,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/API/Authentication/TokenAuthenticator.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\API\\\\BaseApiController\\:\\:createSearchForm\\(\\) has parameter \\$data with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/API/BaseApiController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\API\\\\BaseApiController\\:\\:createSearchForm\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/API/BaseApiController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of static method App\\\\Timesheet\\\\DateTimeFactory\\:\\:createByUser\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -910,31 +906,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Configuration/SystemConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\AbstractController\\:\\:createFormForGetRequest\\(\\) has parameter \\$data with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/AbstractController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\AbstractController\\:\\:createFormForGetRequest\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/AbstractController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\AbstractController\\:\\:createFormWithName\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/AbstractController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\AbstractController\\:\\:createSearchForm\\(\\) has parameter \\$data with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/AbstractController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\AbstractController\\:\\:createSearchForm\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/AbstractController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\AbstractController\\:\\:getLastSearch\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -1335,21 +1306,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/Reporting/ReportUsersYearController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$begin of class App\\\\Model\\\\MonthlyStatistic constructor expects DateTime, DateTime\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/Reporting/ReportUsersYearController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$begin of method App\\\\Timesheet\\\\TimesheetStatisticService\\:\\:getMonthlyStats\\(\\) expects DateTime, DateTime\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/Reporting/ReportUsersYearController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$financialYear of method App\\\\Timesheet\\\\DateTimeFactory\\:\\:createEndOfFinancialYear\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/Reporting/ReportUsersYearController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$users of method App\\\\Timesheet\\\\TimesheetStatisticService\\:\\:getMonthlyStats\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
|
||||
count: 1
|
||||
@@ -1375,16 +1331,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/Reporting/UserWeekController.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method modify\\(\\) on DateTime\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Controller/Reporting/UserYearController.php
|
||||
|
||||
-
|
||||
message: "#^Cannot clone non\\-object variable \\$start of type DateTime\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Controller/Reporting/UserYearController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\Reporting\\\\UserYearController\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -1395,21 +1341,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/Reporting/UserYearController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$begin of class App\\\\Model\\\\MonthlyStatistic constructor expects DateTime, DateTime\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/Reporting/UserYearController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$begin of method App\\\\Controller\\\\Reporting\\\\AbstractUserReportController\\:\\:prepareReport\\(\\) expects DateTime, DateTime\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/Reporting/UserYearController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$financialYear of method App\\\\Timesheet\\\\DateTimeFactory\\:\\:createEndOfFinancialYear\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/Reporting/UserYearController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$user of class App\\\\Model\\\\MonthlyStatistic constructor expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -1675,16 +1606,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/DependencyInjection/AppExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:getFilteredPermissions\\(\\) has parameter \\$permissions with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/DependencyInjection/AppExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:getFilteredPermissions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/DependencyInjection/AppExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DependencyInjection\\\\Configuration\\:\\:getProjectNode\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -4240,26 +4161,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/Type/UserPreferencesCollectionType.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method modify\\(\\) on mixed\\.$#"
|
||||
count: 2
|
||||
path: src/Form/Type/WeekPickerType.php
|
||||
|
||||
-
|
||||
message: "#^Cannot clone non\\-object variable \\$date of type mixed\\.$#"
|
||||
count: 2
|
||||
path: src/Form/Type/WeekPickerType.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method modify\\(\\) on mixed\\.$#"
|
||||
count: 2
|
||||
path: src/Form/Type/YearPickerType.php
|
||||
|
||||
-
|
||||
message: "#^Cannot clone non\\-object variable \\$date of type mixed\\.$#"
|
||||
count: 2
|
||||
path: src/Form/Type/YearPickerType.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset mixed on mixed\\.$#"
|
||||
count: 1
|
||||
@@ -5155,21 +5056,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Project/ProjectStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Reporting\\\\AbstractUserList\\:\\:\\$date has no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Reporting/AbstractUserList.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Reporting\\\\AbstractUserList\\:\\:\\$decimal has no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Reporting/AbstractUserList.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Reporting\\\\AbstractUserList\\:\\:\\$sumType has no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Reporting/AbstractUserList.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Reporting\\\\CustomerMonthlyProjects\\\\CustomerMonthlyProjectsRepository\\:\\:getGroupedByCustomerProjectActivityUser\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -6590,11 +6476,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Utils/Pagination.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Utils\\\\PaginationTemplate\\:\\:getDefaultOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/PaginationTemplate.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$class of method App\\\\Utils\\\\PaginationTemplate\\:\\:linkLi\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to select weekdays.
|
||||
* @extends AbstractType<string>
|
||||
*/
|
||||
final class WeekDaysType extends AbstractType
|
||||
{
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -77,6 +77,11 @@ class BaseQuery
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasTeams(): bool
|
||||
{
|
||||
return \count($this->teams) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Team[]
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -225,9 +225,6 @@
|
||||
"phar-io/version": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"php-http/message-factory": {
|
||||
"version": "v1.0.2"
|
||||
},
|
||||
"phpdocumentor/reflection-common": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
|
||||
@@ -77,6 +77,14 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% set last = chartData|last %}
|
||||
{% if last is not empty and last.value > 0 %}
|
||||
{% set chartValueZero = {
|
||||
'label': (showMoneyBudget ? 0|money(currency) : 0|duration),
|
||||
'value': (showMoneyBudget ? 0|chart_money : 0|chart_duration),
|
||||
} %}
|
||||
{% set chartData = chartData|merge([chartValueZero]) %}
|
||||
{% endif %}
|
||||
{{ bar_chart(chartPrefix, chartLabels, [chartData], {'height': '300px', 'renderEvent': 'render.' ~ chartPrefix, 'type': 'line'}) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -69,6 +69,8 @@ class InvoiceItemDefaultHydratorTest extends TestCase
|
||||
'entry.user_alias',
|
||||
'entry.user_display',
|
||||
'entry.user_title',
|
||||
'entry.user_preference.foo',
|
||||
'entry.user_preference.mad',
|
||||
'entry.activity',
|
||||
'entry.activity_id',
|
||||
'entry.activity.meta.foo-activity',
|
||||
|
||||
@@ -276,6 +276,8 @@ class DebugRendererTest extends TestCase
|
||||
'entry.user_display',
|
||||
'entry.user_alias',
|
||||
'entry.user_title',
|
||||
'entry.user_preference.foo',
|
||||
'entry.user_preference.mad',
|
||||
'entry.activity',
|
||||
'entry.activity_id',
|
||||
'entry.activity.meta.foo-activity',
|
||||
|
||||
@@ -133,17 +133,21 @@ trait RendererTestTrait
|
||||
$activity2->method('getMetaFields')->willReturn(new ArrayCollection([$aMeta2]));
|
||||
$activity2->method('getVisibleMetaFields')->willReturn([$aMeta2]);
|
||||
|
||||
$userMethods = ['getId', 'getPreferenceValue', 'getUsername', 'getUserIdentifier'];
|
||||
$pref1 = new UserPreference('foo', 'bar');
|
||||
$pref2 = new UserPreference('mad', 123.45);
|
||||
$userMethods = ['getId', 'getPreferenceValue', 'getVisiblePreferences', 'getUsername', 'getUserIdentifier'];
|
||||
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||
$user1->method('getId')->willReturn(1);
|
||||
$user1->method('getPreferenceValue')->willReturn('50');
|
||||
$user1->method('getUsername')->willReturn('foo-bar');
|
||||
$user1->method('getUserIdentifier')->willReturn('foo-bar');
|
||||
$user1->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
|
||||
|
||||
$user2 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||
$user2 = $this->createMock(User::class);
|
||||
$user2->method('getId')->willReturn(2);
|
||||
$user2->method('getUsername')->willReturn('hello-world');
|
||||
$user2->method('getUserIdentifier')->willReturn('hello-world');
|
||||
$user2->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
@@ -204,6 +208,8 @@ trait RendererTestTrait
|
||||
|
||||
$userKevin = new User();
|
||||
$userKevin->setUserIdentifier('kevin');
|
||||
$userKevin->addPreference($pref1);
|
||||
$userKevin->addPreference($pref2);
|
||||
|
||||
$timesheet5 = new Timesheet();
|
||||
$timesheet5
|
||||
@@ -292,11 +298,14 @@ trait RendererTestTrait
|
||||
$activity->setProject($project);
|
||||
$activity->setMetaField((new ActivityMeta())->setName('foo-activity')->setValue('bar-activity')->setIsVisible(true));
|
||||
|
||||
$pref1 = new UserPreference('foo', 'bar');
|
||||
$pref2 = new UserPreference('mad', 123.45);
|
||||
$user1 = $this->createMock(User::class);
|
||||
$user1->method('getId')->willReturn(1);
|
||||
$user1->method('getPreferenceValue')->willReturn('50');
|
||||
$user1->method('getUsername')->willReturn('foo-bar');
|
||||
$user1->method('getUserIdentifier')->willReturn('foo-bar');
|
||||
$user1->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Saml\SamlToken;
|
||||
use App\Saml\Security\SamlAuthenticationSuccessHandler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
|
||||
@@ -27,6 +28,7 @@ class SamlAuthenticationSuccessHandlerTest extends TestCase
|
||||
$handler = new SamlAuthenticationSuccessHandler($httpUtils, ['always_use_default_target_path' => true]);
|
||||
$defaultTargetPath = $httpUtils->generateUri($this->getRequest('/sso/login'), $this->getOption($handler, 'default_target_path', '/'));
|
||||
$response = $handler->onAuthenticationSuccess($this->getRequest('/login', 'http://localhost/relayed'), $this->getSamlToken());
|
||||
$this->assertInstanceOf(RedirectResponse::class, $response);
|
||||
$this->assertTrue($response->isRedirect($defaultTargetPath));
|
||||
}
|
||||
|
||||
@@ -34,6 +36,7 @@ class SamlAuthenticationSuccessHandlerTest extends TestCase
|
||||
{
|
||||
$handler = new SamlAuthenticationSuccessHandler(new HttpUtils($this->getUrlGenerator()), ['always_use_default_target_path' => false]);
|
||||
$response = $handler->onAuthenticationSuccess($this->getRequest('/sso/login', 'http://localhost/relayed'), $this->getSamlToken());
|
||||
$this->assertInstanceOf(RedirectResponse::class, $response);
|
||||
$this->assertTrue($response->isRedirect('http://localhost/relayed'));
|
||||
}
|
||||
|
||||
@@ -43,6 +46,7 @@ class SamlAuthenticationSuccessHandlerTest extends TestCase
|
||||
$handler = new SamlAuthenticationSuccessHandler($httpUtils, ['always_use_default_target_path' => false]);
|
||||
$defaultTargetPath = $httpUtils->generateUri($this->getRequest('/sso/login'), $this->getOption($handler, 'default_target_path', '/'));
|
||||
$response = $handler->onAuthenticationSuccess($this->getRequest(), $this->getSamlToken());
|
||||
$this->assertInstanceOf(RedirectResponse::class, $response);
|
||||
$this->assertTrue($response->isRedirect($defaultTargetPath));
|
||||
}
|
||||
|
||||
@@ -52,6 +56,7 @@ class SamlAuthenticationSuccessHandlerTest extends TestCase
|
||||
$handler = new SamlAuthenticationSuccessHandler($httpUtils, ['always_use_default_target_path' => false]);
|
||||
$loginPath = $httpUtils->generateUri($this->getRequest('/sso/login'), $this->getOption($handler, 'login_path', '/login'));
|
||||
$response = $handler->onAuthenticationSuccess($this->getRequest($loginPath), $this->getSamlToken());
|
||||
$this->assertInstanceOf(RedirectResponse::class, $response);
|
||||
$this->assertTrue(!$response->isRedirect($loginPath));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user