Release 2.6.0 (#4472)
- Added: calendar entry title combination for customer, project and activity - Added: show not_invoiced and not_exported data in detail screens - Added: force logout if user is disabled - Added: reduced amount of database queries on several screens - Fixed: open-close status on work-contract screen for users without configuration - Fixed: failsafe order/orderBy in query if manually manipulated to be null - Fixed: unify statistic calculation (not_invoiced and not_exported) across screens - Tech: bump packages - Tech: Symfony 6.4
This commit is contained in:
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -8,4 +8,4 @@ contact_links:
|
|||||||
about: Want to discuss something with a community? Do it in discussions!
|
about: Want to discuss something with a community? Do it in discussions!
|
||||||
- name: Get Professional support
|
- name: Get Professional support
|
||||||
url: https://www.kimai.org/en/support.html
|
url: https://www.kimai.org/en/support.html
|
||||||
about: As a customer, you will always receive fast and helpful replies from the developer.
|
about: As a customer, you will always receive fast replies from the developer.
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/docker.yml
vendored
8
.github/ISSUE_TEMPLATE/docker.yml
vendored
@@ -28,11 +28,9 @@ body:
|
|||||||
- Docker compose version: [e.g. 1.21.0]
|
- Docker compose version: [e.g. 1.21.0]
|
||||||
|
|
||||||
Docker compose file:
|
Docker compose file:
|
||||||
```yaml
|
version: '3.5'
|
||||||
version: '3.5'
|
services:
|
||||||
services:
|
image: ...
|
||||||
image: ...
|
|
||||||
```
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ $fixer
|
|||||||
'lowercase_static_reference' => true,
|
'lowercase_static_reference' => true,
|
||||||
'magic_constant_casing' => true,
|
'magic_constant_casing' => true,
|
||||||
'native_function_casing' => true,
|
'native_function_casing' => true,
|
||||||
'new_with_braces' => true,
|
'new_with_parentheses' => true,
|
||||||
'no_blank_lines_after_class_opening' => true,
|
'no_blank_lines_after_class_opening' => true,
|
||||||
'no_blank_lines_after_phpdoc' => true,
|
'no_blank_lines_after_phpdoc' => true,
|
||||||
'no_empty_comment' => true,
|
'no_empty_comment' => true,
|
||||||
@@ -82,7 +82,7 @@ $fixer
|
|||||||
'no_singleline_whitespace_before_semicolons' => true,
|
'no_singleline_whitespace_before_semicolons' => true,
|
||||||
'no_spaces_around_offset' => true,
|
'no_spaces_around_offset' => true,
|
||||||
'no_trailing_comma_in_singleline' => true,
|
'no_trailing_comma_in_singleline' => true,
|
||||||
'no_unneeded_curly_braces' => true,
|
'no_unneeded_braces' => true,
|
||||||
'no_unneeded_final_method' => true,
|
'no_unneeded_final_method' => true,
|
||||||
'no_unused_imports' => true,
|
'no_unused_imports' => true,
|
||||||
'no_whitespace_before_comma_in_array' => true,
|
'no_whitespace_before_comma_in_array' => true,
|
||||||
@@ -151,7 +151,7 @@ $fixer
|
|||||||
],
|
],
|
||||||
'scope' => 'namespaced'
|
'scope' => 'namespaced'
|
||||||
],
|
],
|
||||||
'native_function_type_declaration_casing' => true,
|
'native_type_declaration_casing' => true,
|
||||||
'no_alias_functions' => [
|
'no_alias_functions' => [
|
||||||
'sets' => [
|
'sets' => [
|
||||||
'@internal'
|
'@internal'
|
||||||
|
|||||||
@@ -202,7 +202,7 @@
|
|||||||
"symfony": {
|
"symfony": {
|
||||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||||
"allow-contrib": true,
|
"allow-contrib": true,
|
||||||
"require": "6.3.*"
|
"require": "6.4.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1669
composer.lock
generated
1669
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,11 @@
|
|||||||
framework:
|
framework:
|
||||||
|
# Deprecations for 7.0
|
||||||
|
handle_all_throwables: true
|
||||||
|
# cannot be disabled, because of https://github.com/nelmio/NelmioApiDocBundle/pull/2164
|
||||||
|
annotations:
|
||||||
|
enabled: true
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
exceptions:
|
exceptions:
|
||||||
App\Validator\ValidationFailedException:
|
App\Validator\ValidationFailedException:
|
||||||
log_level: debug
|
log_level: debug
|
||||||
|
|||||||
82
phpstan.neon
82
phpstan.neon
@@ -636,16 +636,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Command/TranslationCommand.php
|
path: src/Command/TranslationCommand.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot access property \\$original on SimpleXMLElement\\|null\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Command/TranslationCommand.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot access property \\$target\\-language on SimpleXMLElement\\|null\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Command/TranslationCommand.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method asXML\\(\\) on SimpleXMLElement\\|false\\.$#"
|
message: "#^Cannot call method asXML\\(\\) on SimpleXMLElement\\|false\\.$#"
|
||||||
count: 5
|
count: 5
|
||||||
@@ -816,11 +806,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Configuration/SystemConfiguration.php
|
path: src/Configuration/SystemConfiguration.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:getThemeColorChoices\\(\\) should return string\\|null but returns float\\|int\\<min, \\-1\\>\\|int\\<1, max\\>\\|string\\|true\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Configuration/SystemConfiguration.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:getUserDefaultCurrency\\(\\) should return string but returns bool\\|float\\|int\\|string\\|null\\.$#"
|
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:getUserDefaultCurrency\\(\\) should return string but returns bool\\|float\\|int\\|string\\|null\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -1141,11 +1126,6 @@ parameters:
|
|||||||
count: 3
|
count: 3
|
||||||
path: src/Controller/Reporting/ProjectDateRangeController.php
|
path: src/Controller/Reporting/ProjectDateRangeController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$begin of method App\\\\Form\\\\Model\\\\DateRange\\:\\:setBegin\\(\\) expects DateTime, DateTime\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/Reporting/ProjectDateRangeController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#2 \\$begin of method App\\\\Project\\\\ProjectStatisticService\\:\\:getBudgetStatisticModelForProjectsByDateRange\\(\\) expects DateTime, DateTime\\|null given\\.$#"
|
message: "#^Parameter \\#2 \\$begin of method App\\\\Project\\\\ProjectStatisticService\\:\\:getBudgetStatisticModelForProjectsByDateRange\\(\\) expects DateTime, DateTime\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -3471,16 +3451,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Form/Type/ColorChoiceType.php
|
path: src/Form/Type/ColorChoiceType.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Form\\\\Type\\\\ColorChoiceType\\:\\:convertStringToColorArray\\(\\) return type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/Type/ColorChoiceType.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$config of method App\\\\Form\\\\Type\\\\ColorChoiceType\\:\\:convertStringToColorArray\\(\\) expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/Type/ColorChoiceType.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Class App\\\\Form\\\\Type\\\\ColorPickerType implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#"
|
message: "#^Class App\\\\Form\\\\Type\\\\ColorPickerType implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4231,29 +4201,14 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Plugin/PluginMetadata.php
|
path: src/Plugin/PluginMetadata.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot access offset 'amount' on mixed\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot access offset 'billable' on mixed\\.$#"
|
|
||||||
count: 3
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'duration' on mixed\\.$#"
|
message: "#^Cannot access offset 'duration' on mixed\\.$#"
|
||||||
count: 9
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot access offset 'exported' on mixed\\.$#"
|
|
||||||
count: 3
|
count: 3
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'id' on mixed\\.$#"
|
message: "#^Cannot access offset 'id' on mixed\\.$#"
|
||||||
count: 8
|
count: 4
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -4261,11 +4216,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot access offset 'rate' on mixed\\.$#"
|
|
||||||
count: 6
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method getEnd\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
message: "#^Cannot call method getEnd\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
||||||
count: 2
|
count: 2
|
||||||
@@ -4331,11 +4281,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$durationTotal of method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:setDurationTotal\\(\\) expects int, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$durationWeek of method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:setDurationWeek\\(\\) expects int, mixed given\\.$#"
|
message: "#^Parameter \\#1 \\$durationWeek of method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:setDurationWeek\\(\\) expects int, mixed given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4351,11 +4296,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$rateTotal of method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:setRateTotal\\(\\) expects float, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$recordDuration of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setDurationBillable\\(\\) expects int, float\\|int given\\.$#"
|
message: "#^Parameter \\#1 \\$recordDuration of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setDurationBillable\\(\\) expects int, float\\|int given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4371,11 +4311,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$timesheetCounter of method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:setTimesheetCounter\\(\\) expects int, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$totalDuration of method App\\\\Model\\\\Statistic\\\\Timesheet\\:\\:setTotalDuration\\(\\) expects int, float\\|int given\\.$#"
|
message: "#^Parameter \\#1 \\$totalDuration of method App\\\\Model\\\\Statistic\\\\Timesheet\\:\\:setTotalDuration\\(\\) expects int, float\\|int given\\.$#"
|
||||||
count: 2
|
count: 2
|
||||||
@@ -4396,11 +4331,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Reporting/CustomerMonthlyProjects/CustomerMonthlyProjectsRepository.php
|
path: src/Reporting/CustomerMonthlyProjects/CustomerMonthlyProjectsRepository.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Property App\\\\Reporting\\\\ProjectDateRange\\\\ProjectDateRangeQuery\\:\\:\\$month \\(DateTime\\) does not accept DateTime\\|null\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Reporting/ProjectDateRange/ProjectDateRangeQuery.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4421,11 +4351,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Reporting/ProjectDetails/ProjectDetailsModel.php
|
path: src/Reporting/ProjectDetails/ProjectDetailsModel.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Reporting\\\\ProjectView\\\\ProjectViewModel\\:\\:getBudgetStatisticModel\\(\\) should return App\\\\Model\\\\BudgetStatisticModelInterface but returns App\\\\Model\\\\BudgetStatisticModelInterface\\|null\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Reporting/ProjectView/ProjectViewModel.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -5136,11 +5061,6 @@ parameters:
|
|||||||
count: 2
|
count: 2
|
||||||
path: src/Saml/SamlProvider.php
|
path: src/Saml/SamlProvider.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$identifier of method Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserProviderInterface\\:\\:loadUserByIdentifier\\(\\) expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Saml/SamlProvider.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, mixed given\\.$#"
|
message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, mixed given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ final class SessionAuthenticator extends AbstractAuthenticator
|
|||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supports(Request $request): ?bool
|
public function supports(Request $request): bool
|
||||||
{
|
{
|
||||||
if (str_contains($request->getRequestUri(), '/api/')) {
|
if (str_contains($request->getRequestUri(), '/api/')) {
|
||||||
// API docs can only be access, when the user is logged in
|
// API docs can only be access, when the user is logged in
|
||||||
@@ -62,7 +62,7 @@ final class SessionAuthenticator extends AbstractAuthenticator
|
|||||||
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
|
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||||
{
|
{
|
||||||
return $this->authenticator->onAuthenticationFailure($request, $exception);
|
return $this->authenticator->onAuthenticationFailure($request, $exception);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class TokenAuthenticator extends AbstractAuthenticator
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supports(Request $request): ?bool
|
public function supports(Request $request): bool
|
||||||
{
|
{
|
||||||
if (str_contains($request->getRequestUri(), '/api/')) {
|
if (str_contains($request->getRequestUri(), '/api/')) {
|
||||||
return !str_contains($request->getRequestUri(), '/api/doc');
|
return !str_contains($request->getRequestUri(), '/api/doc');
|
||||||
@@ -95,7 +95,7 @@ final class TokenAuthenticator extends AbstractAuthenticator
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
'message' => $exception instanceof CustomUserMessageAuthenticationException ? $exception->getMessage() : 'Invalid credentials'
|
'message' => $exception instanceof CustomUserMessageAuthenticationException ? $exception->getMessage() : 'Invalid credentials'
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final class RecentActivitiesSource implements DragAndDropSource
|
|||||||
return $this->entries;
|
return $this->entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBlockInclude(): ?string
|
public function getBlockInclude(): string
|
||||||
{
|
{
|
||||||
return 'calendar/drag-drop.html.twig';
|
return 'calendar/drag-drop.html.twig';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,14 @@ final class TimesheetEntry implements DragAndDropEntry
|
|||||||
|
|
||||||
public function getTitle(): string
|
public function getTitle(): string
|
||||||
{
|
{
|
||||||
if ($this->timesheet->getActivity() !== null && $this->timesheet->getActivity()->getName() !== null) {
|
$activity = $this->timesheet->getActivity();
|
||||||
return $this->timesheet->getActivity()->getName();
|
if ($activity !== null && $activity->getName() !== null) {
|
||||||
|
return $activity->getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $this->timesheet->getProject() && $this->timesheet->getProject()->getName() !== null) {
|
$project = $this->timesheet->getProject();
|
||||||
return $this->timesheet->getProject()->getName();
|
if ($project !== null && $project->getName() !== null) {
|
||||||
|
return $project->getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->timesheet->getDescription() ?? '';
|
return $this->timesheet->getDescription() ?? '';
|
||||||
@@ -60,7 +62,7 @@ final class TimesheetEntry implements DragAndDropEntry
|
|||||||
return $this->color;
|
return $this->color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBlockName(): ?string
|
public function getBlockName(): string
|
||||||
{
|
{
|
||||||
return 'dd_timesheet';
|
return 'dd_timesheet';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,10 +482,10 @@ final class SystemConfiguration
|
|||||||
return (bool) $this->find('theme.avatar_url');
|
return (bool) $this->find('theme.avatar_url');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getThemeColorChoices(): ?string
|
public function getThemeColorChoices(): string
|
||||||
{
|
{
|
||||||
$config = $this->find('theme.color_choices');
|
$config = $this->find('theme.color_choices');
|
||||||
if (!empty($config)) {
|
if (\is_string($config) && $config !== '') {
|
||||||
return $config;
|
return $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.5.0';
|
public const VERSION = '2.6.0';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 20500;
|
public const VERSION_ID = 20600;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ use App\Form\Type\ActivityType;
|
|||||||
use App\Repository\ActivityRateRepository;
|
use App\Repository\ActivityRateRepository;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\Query\ActivityQuery;
|
use App\Repository\Query\ActivityQuery;
|
||||||
|
use App\Repository\Query\TeamQuery;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Repository\TeamRepository;
|
use App\Repository\TeamRepository;
|
||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
@@ -135,6 +137,22 @@ final class ActivityController extends AbstractController
|
|||||||
$defaultTeam = null;
|
$defaultTeam = null;
|
||||||
$now = $this->getDateTimeFactory()->createDateTime();
|
$now = $this->getDateTimeFactory()->createDateTime();
|
||||||
|
|
||||||
|
$exportUrl = null;
|
||||||
|
$invoiceUrl = null;
|
||||||
|
$params = ['customers[]' => '', 'projects[]' => '', 'activities[]' => $activity->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true];
|
||||||
|
if ($activity->getProject() !== null) {
|
||||||
|
$params['projects[]'] = $activity->getProject()->getId();
|
||||||
|
if ($activity->getProject()->getCustomer() !== null) {
|
||||||
|
$params['customers[]'] = $activity->getProject()->getCustomer()->getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($this->isGranted('create_export')) {
|
||||||
|
$exportUrl = $this->generateUrl('export', array_merge($params, ['preview' => true]));
|
||||||
|
}
|
||||||
|
if ($this->isGranted('view_invoice')) {
|
||||||
|
$invoiceUrl = $this->generateUrl('invoice', $params);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isGranted('edit', $activity)) {
|
if ($this->isGranted('edit', $activity)) {
|
||||||
if ($this->isGranted('create_team')) {
|
if ($this->isGranted('create_team')) {
|
||||||
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
$defaultTeam = $teamRepository->findOneBy(['name' => $activity->getName()]);
|
||||||
@@ -147,7 +165,9 @@ final class ActivityController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isGranted('permissions', $activity) || $this->isGranted('details', $activity) || $this->isGranted('view_team')) {
|
if ($this->isGranted('permissions', $activity) || $this->isGranted('details', $activity) || $this->isGranted('view_team')) {
|
||||||
$teams = $activity->getTeams();
|
$query = new TeamQuery();
|
||||||
|
$query->addActivity($activity);
|
||||||
|
$teams = $teamRepository->getTeamsForQuery($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
// additional boxes by plugins
|
// additional boxes by plugins
|
||||||
@@ -168,7 +188,9 @@ final class ActivityController extends AbstractController
|
|||||||
'team' => $defaultTeam,
|
'team' => $defaultTeam,
|
||||||
'teams' => $teams,
|
'teams' => $teams,
|
||||||
'now' => $now,
|
'now' => $now,
|
||||||
'boxes' => $boxes
|
'boxes' => $boxes,
|
||||||
|
'export_url' => $exportUrl,
|
||||||
|
'invoice_url' => $invoiceUrl,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ namespace App\Controller\Auth;
|
|||||||
use App\Configuration\SamlConfigurationInterface;
|
use App\Configuration\SamlConfigurationInterface;
|
||||||
use App\Saml\SamlAuthFactory;
|
use App\Saml\SamlAuthFactory;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||||
|
|
||||||
#[Route(path: '/saml')]
|
#[Route(path: '/saml')]
|
||||||
final class SamlController extends AbstractController
|
final class SamlController extends AbstractController
|
||||||
@@ -32,7 +32,7 @@ final class SamlController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$session = $request->getSession();
|
$session = $request->getSession();
|
||||||
$authErrorKey = Security::AUTHENTICATION_ERROR;
|
$authErrorKey = SecurityRequestAttributes::AUTHENTICATION_ERROR;
|
||||||
|
|
||||||
$error = null;
|
$error = null;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ use App\Repository\CustomerRepository;
|
|||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\CustomerQuery;
|
use App\Repository\Query\CustomerQuery;
|
||||||
use App\Repository\Query\ProjectQuery;
|
use App\Repository\Query\ProjectQuery;
|
||||||
|
use App\Repository\Query\TeamQuery;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Repository\TeamRepository;
|
use App\Repository\TeamRepository;
|
||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
use App\Utils\PageSetup;
|
use App\Utils\PageSetup;
|
||||||
@@ -307,6 +309,15 @@ final class CustomerController extends AbstractController
|
|||||||
$rates = [];
|
$rates = [];
|
||||||
$now = $this->getDateTimeFactory()->createDateTime();
|
$now = $this->getDateTimeFactory()->createDateTime();
|
||||||
|
|
||||||
|
$exportUrl = null;
|
||||||
|
$invoiceUrl = null;
|
||||||
|
if ($this->isGranted('create_export')) {
|
||||||
|
$exportUrl = $this->generateUrl('export', ['customers[]' => $customer->getId(), 'projects[]' => '', 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'preview' => true, 'billable' => true]);
|
||||||
|
}
|
||||||
|
if ($this->isGranted('view_invoice')) {
|
||||||
|
$invoiceUrl = $this->generateUrl('invoice', ['customers[]' => $customer->getId(), 'projects[]' => '', 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isGranted('edit', $customer)) {
|
if ($this->isGranted('edit', $customer)) {
|
||||||
if ($this->isGranted('create_team')) {
|
if ($this->isGranted('create_team')) {
|
||||||
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
$defaultTeam = $teamRepository->findOneBy(['name' => $customer->getName()]);
|
||||||
@@ -328,7 +339,9 @@ final class CustomerController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isGranted('permissions', $customer) || $this->isGranted('details', $customer) || $this->isGranted('view_team')) {
|
if ($this->isGranted('permissions', $customer) || $this->isGranted('details', $customer) || $this->isGranted('view_team')) {
|
||||||
$teams = $customer->getTeams();
|
$query = new TeamQuery();
|
||||||
|
$query->addCustomer($customer);
|
||||||
|
$teams = $teamRepository->getTeamsForQuery($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
// additional boxes by plugins
|
// additional boxes by plugins
|
||||||
@@ -353,7 +366,9 @@ final class CustomerController extends AbstractController
|
|||||||
'customer_now' => new \DateTime('now', $timezone),
|
'customer_now' => new \DateTime('now', $timezone),
|
||||||
'rates' => $rates,
|
'rates' => $rates,
|
||||||
'now' => $now,
|
'now' => $now,
|
||||||
'boxes' => $boxes
|
'boxes' => $boxes,
|
||||||
|
'export_url' => $exportUrl,
|
||||||
|
'invoice_url' => $invoiceUrl,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ final class ProfileController extends AbstractController
|
|||||||
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher, UserRepository $userRepository): Response
|
public function preferencesAction(User $profile, Request $request, EventDispatcherInterface $dispatcher, UserRepository $userRepository): Response
|
||||||
{
|
{
|
||||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||||
$event = new PrepareUserEvent($profile);
|
$event = new PrepareUserEvent($profile, false);
|
||||||
$dispatcher->dispatch($event);
|
$dispatcher->dispatch($event);
|
||||||
|
|
||||||
$form = $this->createPreferencesForm($profile);
|
$form = $this->createPreferencesForm($profile);
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ use App\Repository\ProjectRateRepository;
|
|||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\ActivityQuery;
|
use App\Repository\Query\ActivityQuery;
|
||||||
use App\Repository\Query\ProjectQuery;
|
use App\Repository\Query\ProjectQuery;
|
||||||
|
use App\Repository\Query\TeamQuery;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Repository\TeamRepository;
|
use App\Repository\TeamRepository;
|
||||||
use App\Utils\Context;
|
use App\Utils\Context;
|
||||||
use App\Utils\DataTable;
|
use App\Utils\DataTable;
|
||||||
@@ -334,6 +336,15 @@ final class ProjectController extends AbstractController
|
|||||||
$rates = [];
|
$rates = [];
|
||||||
$now = $this->getDateTimeFactory()->createDateTime();
|
$now = $this->getDateTimeFactory()->createDateTime();
|
||||||
|
|
||||||
|
$exportUrl = null;
|
||||||
|
$invoiceUrl = null;
|
||||||
|
if ($this->isGranted('create_export') && $project->getCustomer() !== null) {
|
||||||
|
$exportUrl = $this->generateUrl('export', ['customers[]' => $project->getCustomer()->getId(), 'projects[]' => $project->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'preview' => true, 'billable' => true]);
|
||||||
|
}
|
||||||
|
if ($this->isGranted('view_invoice') && $project->getCustomer() !== null) {
|
||||||
|
$invoiceUrl = $this->generateUrl('invoice', ['customers[]' => $project->getCustomer()->getId(), 'projects[]' => $project->getId(), 'daterange' => '', 'exported' => TimesheetQuery::STATE_NOT_EXPORTED, 'billable' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isGranted('edit', $project)) {
|
if ($this->isGranted('edit', $project)) {
|
||||||
if ($this->isGranted('create_team')) {
|
if ($this->isGranted('create_team')) {
|
||||||
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
$defaultTeam = $teamRepository->findOneBy(['name' => $project->getName()]);
|
||||||
@@ -351,7 +362,9 @@ final class ProjectController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isGranted('permissions', $project) || $this->isGranted('details', $project) || $this->isGranted('view_team')) {
|
if ($this->isGranted('permissions', $project) || $this->isGranted('details', $project) || $this->isGranted('view_team')) {
|
||||||
$teams = $project->getTeams();
|
$query = new TeamQuery();
|
||||||
|
$query->addProject($project);
|
||||||
|
$teams = $teamRepository->getTeamsForQuery($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
// additional boxes by plugins
|
// additional boxes by plugins
|
||||||
@@ -375,7 +388,9 @@ final class ProjectController extends AbstractController
|
|||||||
'teams' => $teams,
|
'teams' => $teams,
|
||||||
'rates' => $rates,
|
'rates' => $rates,
|
||||||
'now' => $now,
|
'now' => $now,
|
||||||
'boxes' => $boxes
|
'boxes' => $boxes,
|
||||||
|
'export_url' => $exportUrl,
|
||||||
|
'invoice_url' => $invoiceUrl,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,15 @@ final class ProjectDateRangeController extends AbstractController
|
|||||||
$dateFactory = $this->getDateTimeFactory();
|
$dateFactory = $this->getDateTimeFactory();
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
|
|
||||||
$query = new ProjectDaterangeQuery($dateFactory->getStartOfMonth(), $user);
|
$defaultStart = $dateFactory->getStartOfMonth();
|
||||||
|
$query = new ProjectDaterangeQuery($defaultStart, $user);
|
||||||
$form = $this->createFormForGetRequest(ProjectDateRangeForm::class, $query, [
|
$form = $this->createFormForGetRequest(ProjectDateRangeForm::class, $query, [
|
||||||
'timezone' => $user->getTimezone()
|
'timezone' => $user->getTimezone()
|
||||||
]);
|
]);
|
||||||
$form->submit($request->query->all(), false);
|
$form->submit($request->query->all(), false);
|
||||||
|
|
||||||
$dateRange = new DateRange(true);
|
$dateRange = new DateRange(true);
|
||||||
$dateRange->setBegin($query->getMonth());
|
$dateRange->setBegin($query->getMonth() ?? $defaultStart);
|
||||||
$dateRange->setEnd($dateFactory->getEndOfMonth($dateRange->getBegin()));
|
$dateRange->setEnd($dateFactory->getEndOfMonth($dateRange->getBegin()));
|
||||||
|
|
||||||
$projects = $service->findProjectsForDateRange($query, $dateRange);
|
$projects = $service->findProjectsForDateRange($query, $dateRange);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ final class PasswordResetController extends AbstractController
|
|||||||
$username = $request->request->get('username');
|
$username = $request->request->get('username');
|
||||||
$user = $this->userService->findUserByUsernameOrEmail($username);
|
$user = $this->userService->findUserByUsernameOrEmail($username);
|
||||||
|
|
||||||
if (null !== $user && !$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||||
if (!$user->isInternalUser()) {
|
if (!$user->isInternalUser()) {
|
||||||
throw $this->createAccessDeniedException(
|
throw $this->createAccessDeniedException(
|
||||||
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUserIdentifier(), $user->getAuth())
|
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUserIdentifier(), $user->getAuth())
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Configuration\SystemConfiguration;
|
use App\Configuration\SystemConfiguration;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Event\PrepareUserEvent;
|
||||||
use App\Event\UserPreferenceDisplayEvent;
|
use App\Event\UserPreferenceDisplayEvent;
|
||||||
use App\Export\Spreadsheet\UserExporter;
|
use App\Export\Spreadsheet\UserExporter;
|
||||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||||
@@ -112,7 +113,7 @@ final class UserController extends AbstractController
|
|||||||
|
|
||||||
#[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])]
|
#[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])]
|
||||||
#[IsGranted('create_user')]
|
#[IsGranted('create_user')]
|
||||||
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository): Response
|
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository, EventDispatcherInterface $dispatcher): Response
|
||||||
{
|
{
|
||||||
$user = $this->createNewDefaultUser($config);
|
$user = $this->createNewDefaultUser($config);
|
||||||
$editForm = $this->getCreateUserForm($user);
|
$editForm = $this->getCreateUserForm($user);
|
||||||
@@ -126,6 +127,14 @@ final class UserController extends AbstractController
|
|||||||
$userRepository->saveUser($user);
|
$userRepository->saveUser($user);
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$event = new PrepareUserEvent($user, false);
|
||||||
|
$dispatcher->dispatch($event);
|
||||||
|
$userRepository->saveUser($user);
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
// it should be no problem, if creating default user preferences fails
|
||||||
|
}
|
||||||
|
|
||||||
return $this->redirectToRouteAfterCreate('user_profile_edit', ['username' => $user->getUserIdentifier()]);
|
return $this->redirectToRouteAfterCreate('user_profile_edit', ['username' => $user->getUserIdentifier()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ final class WizardController extends AbstractController
|
|||||||
$userService->updateUser($user);
|
$userService->updateUser($user);
|
||||||
|
|
||||||
if ($data['reload'] === '1') {
|
if ($data['reload'] === '1') {
|
||||||
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
|
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $user->getLanguage()]);
|
||||||
} else {
|
} else {
|
||||||
return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $data['language']]);
|
return $this->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $user->getLanguage()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,15 +36,12 @@ trait MetaTableTypeTrait
|
|||||||
private ?string $name = null;
|
private ?string $name = null;
|
||||||
/**
|
/**
|
||||||
* Value of the meta (custom) field
|
* Value of the meta (custom) field
|
||||||
*
|
|
||||||
* ATTENTION:
|
|
||||||
* This field can be used to temporary hold data in another format (e.g. array) during form transformation,
|
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)]
|
#[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)]
|
||||||
#[Assert\Length(max: 65535)]
|
#[Assert\Length(max: 65535)]
|
||||||
#[Serializer\Expose]
|
#[Serializer\Expose]
|
||||||
#[Serializer\Groups(['Default'])]
|
#[Serializer\Groups(['Default'])]
|
||||||
private mixed $value = null;
|
private ?string $value = null;
|
||||||
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => false])]
|
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
private bool $visible = false;
|
private bool $visible = false;
|
||||||
@@ -61,6 +58,14 @@ trait MetaTableTypeTrait
|
|||||||
*/
|
*/
|
||||||
private array $options = [];
|
private array $options = [];
|
||||||
private int $order = 0;
|
private int $order = 0;
|
||||||
|
/**
|
||||||
|
* Used for data conversion during form transformation.
|
||||||
|
*
|
||||||
|
* ATTENTION: This field can be used to temporary hold data in another format (e.g. array) during form transformation.
|
||||||
|
* TODO unclear when "array" should happen. the above statement is old and maybe we can remove the "mixed” type
|
||||||
|
*/
|
||||||
|
private mixed $data = null;
|
||||||
|
private bool $updated = false;
|
||||||
|
|
||||||
public function getName(): ?string
|
public function getName(): ?string
|
||||||
{
|
{
|
||||||
@@ -80,41 +85,47 @@ trait MetaTableTypeTrait
|
|||||||
|
|
||||||
public function getValue(): mixed
|
public function getValue(): mixed
|
||||||
{
|
{
|
||||||
if ($this->value === null) {
|
$value = $this->updated ? $this->data : $this->value;
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return match ($this->type) {
|
return match ($this->type) {
|
||||||
YesNoType::class, CheckboxType::class => (\is_string($this->value) || \is_int($this->value)) ? (bool) $this->value : $this->value,
|
YesNoType::class, CheckboxType::class => (\is_string($value) || \is_int($value)) ? (bool) $value : $value,
|
||||||
IntegerType::class => (\is_string($this->value) || \is_int($this->value)) ? (int) $this->value : $this->value,
|
IntegerType::class => (\is_string($value) || \is_int($value)) ? (int) $value : $value,
|
||||||
NumberType::class => (\is_string($this->value) || \is_float($this->value)) ? (float) $this->value : $this->value,
|
NumberType::class => (\is_string($value) || \is_float($value)) ? (float) $value : $value,
|
||||||
default => $this->value,
|
default => $value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value will not be serialized before its stored, so it should be a primitive type.
|
* Value will not be serialized before its stored, so it should be a primitive/scalar type.
|
||||||
*
|
|
||||||
* @param mixed $value
|
|
||||||
* @return MetaTableTypeInterface
|
|
||||||
*/
|
*/
|
||||||
public function setValue(mixed $value): MetaTableTypeInterface
|
public function setValue(mixed $value): MetaTableTypeInterface
|
||||||
{
|
{
|
||||||
|
$this->data = $value;
|
||||||
|
$this->updated = true;
|
||||||
|
|
||||||
// unchecked checkboxes / false bool would save an empty string in the database
|
// unchecked checkboxes / false bool would save an empty string in the database
|
||||||
// those cannot be searched in the database
|
// those cannot be searched in the database
|
||||||
if (null !== $value) {
|
if (null !== $value) {
|
||||||
switch ($this->type) {
|
switch ($this->type) {
|
||||||
case YesNoType::class:
|
case YesNoType::class:
|
||||||
case CheckboxType::class:
|
case CheckboxType::class:
|
||||||
if (\is_string($value) || \is_bool($value)) {
|
if (!\is_int($value) && !\is_bool($value) && !\is_string($value)) {
|
||||||
$value = (int) $value;
|
|
||||||
} else {
|
|
||||||
throw new \InvalidArgumentException('Failed converting meta-field bool value');
|
throw new \InvalidArgumentException('Failed converting meta-field bool value');
|
||||||
|
} else {
|
||||||
|
$value = (string) $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->value = $value;
|
if ($value === null) {
|
||||||
|
$this->value = $value;
|
||||||
|
} elseif (\is_scalar($value)) {
|
||||||
|
$this->value = (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1023,6 +1023,10 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->enabled !== $user->isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,24 @@ use App\Entity\User;
|
|||||||
use Symfony\Contracts\EventDispatcher\Event;
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This event should be used, if a user profile is loaded and want to fill the dynamic user preferences
|
* To be used when a user profile is loaded and should be filled with dynamic user preferences.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class PrepareUserEvent extends Event
|
final class PrepareUserEvent extends Event
|
||||||
{
|
{
|
||||||
public function __construct(private User $user)
|
public function __construct(private User $user, private bool $booting = true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this event is dispatched for the currently logged in user during kernel boot.
|
||||||
|
*/
|
||||||
|
public function isBooting(): bool
|
||||||
|
{
|
||||||
|
return $this->booting;
|
||||||
|
}
|
||||||
|
|
||||||
public function getUser(): User
|
public function getUser(): User
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->user;
|
||||||
|
|||||||
@@ -14,18 +14,26 @@ use App\Entity\UserPreference;
|
|||||||
use Symfony\Contracts\EventDispatcher\Event;
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This event should be used, if further user preferences should be added dynamically.
|
* Add further user-preference definitions dynamically.
|
||||||
|
* This is used on every page load, do not query the database when this is dispatched.
|
||||||
*/
|
*/
|
||||||
final class UserPreferenceEvent extends Event
|
final class UserPreferenceEvent extends Event
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param User $user
|
* @param array<UserPreference> $preferences
|
||||||
* @param UserPreference[] $preferences
|
|
||||||
*/
|
*/
|
||||||
public function __construct(private User $user, private array $preferences)
|
public function __construct(private readonly User $user, private array $preferences, private readonly bool $booting = true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this event is dispatched for the currently logged in user during kernel boot.
|
||||||
|
*/
|
||||||
|
public function isBooting(): bool
|
||||||
|
{
|
||||||
|
return $this->booting;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not set the preferences directly to the user object, but ONLY via addPreference()
|
* Do not set the preferences directly to the user object, but ONLY via addPreference()
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
{
|
{
|
||||||
$user = $event->getUser();
|
$user = $event->getUser();
|
||||||
|
|
||||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user));
|
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user), $event->isBooting());
|
||||||
$this->eventDispatcher->dispatch($event);
|
$this->eventDispatcher->dispatch($event);
|
||||||
|
|
||||||
foreach ($event->getPreferences() as $preference) {
|
foreach ($event->getPreferences() as $preference) {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
namespace App\Form;
|
namespace App\Form;
|
||||||
|
|
||||||
use App\Configuration\SystemConfiguration;
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Customer;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Form\Type\CustomerType;
|
||||||
use App\Form\Type\DatePickerType;
|
use App\Form\Type\DatePickerType;
|
||||||
use App\Form\Type\DescriptionType;
|
use App\Form\Type\DescriptionType;
|
||||||
use App\Form\Type\DurationType;
|
use App\Form\Type\DurationType;
|
||||||
@@ -23,6 +25,7 @@ use App\Form\Type\TimesheetBillableType;
|
|||||||
use App\Form\Type\UserType;
|
use App\Form\Type\UserType;
|
||||||
use App\Form\Type\YesNoType;
|
use App\Form\Type\YesNoType;
|
||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
|
use App\Repository\Query\CustomerFormTypeQuery;
|
||||||
use App\Timesheet\Calculator\BillableCalculator;
|
use App\Timesheet\Calculator\BillableCalculator;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\CallbackTransformer;
|
use Symfony\Component\Form\CallbackTransformer;
|
||||||
@@ -49,7 +52,6 @@ class TimesheetEditForm extends AbstractType
|
|||||||
$project = null;
|
$project = null;
|
||||||
$customer = null;
|
$customer = null;
|
||||||
$currency = false;
|
$currency = false;
|
||||||
$customerCount = $this->customers->countCustomer(true);
|
|
||||||
$timezone = $options['timezone'];
|
$timezone = $options['timezone'];
|
||||||
$isNew = true;
|
$isNew = true;
|
||||||
|
|
||||||
@@ -100,15 +102,33 @@ class TimesheetEditForm extends AbstractType
|
|||||||
$this->addDuration($builder, $options, (!$options['allow_begin_datetime'] || !$options['allow_end_datetime']), $isNew);
|
$this->addDuration($builder, $options, (!$options['allow_begin_datetime'] || !$options['allow_end_datetime']), $isNew);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------
|
||||||
|
$query = new CustomerFormTypeQuery($customer);
|
||||||
|
$query->setUser($options['user']); // @phpstan-ignore-line
|
||||||
|
$qb = $this->customers->getQueryBuilderForFormType($query);
|
||||||
|
/** @var array<Customer> $customers */
|
||||||
|
$customers = $qb->getQuery()->getResult();
|
||||||
|
$customerCount = \count($customers);
|
||||||
|
|
||||||
if ($this->showCustomer($options, $isNew, $customerCount)) {
|
if ($this->showCustomer($options, $isNew, $customerCount)) {
|
||||||
$this->addCustomer($builder, $customer);
|
$builder->add('customer', CustomerType::class, [
|
||||||
|
'choices' => $customers,
|
||||||
|
'data' => $customer,
|
||||||
|
'required' => false,
|
||||||
|
'placeholder' => '',
|
||||||
|
'mapped' => false,
|
||||||
|
'project_enabled' => true,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowCreate = (bool) $this->systemConfiguration->find('activity.allow_inline_create');
|
// TODO pre-select if only one exists
|
||||||
|
|
||||||
$this->addProject($builder, $isNew, $project, $customer);
|
$this->addProject($builder, $isNew, $project, $customer);
|
||||||
|
|
||||||
|
// TODO make creation possible
|
||||||
|
//$allowCreate = (bool) $this->systemConfiguration->find('activity.allow_inline_create');
|
||||||
$this->addActivity($builder, $activity, $project, [
|
$this->addActivity($builder, $activity, $project, [
|
||||||
'allow_create' => $allowCreate && $options['create_activity'],
|
'allow_create' => false,
|
||||||
|
// 'allow_create' => $allowCreate && $options['create_activity'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$descriptionOptions = ['required' => false];
|
$descriptionOptions = ['required' => false];
|
||||||
@@ -394,7 +414,7 @@ class TimesheetEditForm extends AbstractType
|
|||||||
public function configureOptions(OptionsResolver $resolver): void
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$maxMinutes = $this->systemConfiguration->getTimesheetLongRunningDuration();
|
$maxMinutes = $this->systemConfiguration->getTimesheetLongRunningDuration();
|
||||||
$maxHours = 8;
|
$maxHours = 10;
|
||||||
if ($maxMinutes > 0) {
|
if ($maxMinutes > 0) {
|
||||||
$maxHours = (int) ($maxMinutes / 60);
|
$maxHours = (int) ($maxMinutes / 60);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ final class CalendarTitlePatternType extends AbstractType
|
|||||||
public const PATTERN_PROJECT_DESCRIPTION = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_DESCRIPTION;
|
public const PATTERN_PROJECT_DESCRIPTION = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_DESCRIPTION;
|
||||||
public const PATTERN_CUSTOMER_DESCRIPTION = self::PATTERN_CUSTOMER . self::SPACER . self::PATTERN_DESCRIPTION;
|
public const PATTERN_CUSTOMER_DESCRIPTION = self::PATTERN_CUSTOMER . self::SPACER . self::PATTERN_DESCRIPTION;
|
||||||
public const PATTERN_PROJECT_CUSTOMER = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_CUSTOMER;
|
public const PATTERN_PROJECT_CUSTOMER = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_CUSTOMER;
|
||||||
|
public const PATTERN_CUSTOMER_PROJECT = self::PATTERN_CUSTOMER . self::SPACER . self::PATTERN_PROJECT;
|
||||||
|
public const PATTERN_PROJECT_ACTIVITY = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_ACTIVITY;
|
||||||
|
|
||||||
public function __construct(private TranslatorInterface $translator)
|
public function __construct(private TranslatorInterface $translator)
|
||||||
{
|
{
|
||||||
@@ -54,6 +56,8 @@ final class CalendarTitlePatternType extends AbstractType
|
|||||||
$project . self::SPACER . $description => CalendarTitlePatternType::PATTERN_PROJECT_DESCRIPTION,
|
$project . self::SPACER . $description => CalendarTitlePatternType::PATTERN_PROJECT_DESCRIPTION,
|
||||||
$customer . self::SPACER . $description => CalendarTitlePatternType::PATTERN_CUSTOMER_DESCRIPTION,
|
$customer . self::SPACER . $description => CalendarTitlePatternType::PATTERN_CUSTOMER_DESCRIPTION,
|
||||||
$project . self::SPACER . $customer => CalendarTitlePatternType::PATTERN_PROJECT_CUSTOMER,
|
$project . self::SPACER . $customer => CalendarTitlePatternType::PATTERN_PROJECT_CUSTOMER,
|
||||||
|
$customer . self::SPACER . $project => CalendarTitlePatternType::PATTERN_CUSTOMER_PROJECT,
|
||||||
|
$project . self::SPACER . $activity => CalendarTitlePatternType::PATTERN_PROJECT_ACTIVITY,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ final class ColorChoiceType extends AbstractType implements DataTransformerInter
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $config
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
private function convertStringToColorArray(string $config): array
|
private function convertStringToColorArray(string $config): array
|
||||||
{
|
{
|
||||||
$config = explode(',', $config);
|
$config = explode(',', $config);
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
|||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template-implements UserProviderInterface<User>
|
||||||
|
*/
|
||||||
final class LdapUserProvider implements UserProviderInterface
|
final class LdapUserProvider implements UserProviderInterface
|
||||||
{
|
{
|
||||||
public function __construct(private LdapManager $ldapManager, private ?LoggerInterface $logger = null)
|
public function __construct(private LdapManager $ldapManager, private ?LoggerInterface $logger = null)
|
||||||
|
|||||||
@@ -727,13 +727,11 @@ class ProjectStatisticService
|
|||||||
$endMonth = (clone $startOfWeek)->modify('last day of this month');
|
$endMonth = (clone $startOfWeek)->modify('last day of this month');
|
||||||
|
|
||||||
$projectViews = [];
|
$projectViews = [];
|
||||||
foreach ($projects as $project) {
|
|
||||||
$projectViews[$project->getId()] = new ProjectViewModel($project);
|
|
||||||
}
|
|
||||||
|
|
||||||
$budgetStats = $this->getBudgetStatisticModelForProjects($projects, $today);
|
$budgetStats = $this->getBudgetStatisticModelForProjects($projects, $today);
|
||||||
foreach ($budgetStats as $model) {
|
foreach ($budgetStats as $model) {
|
||||||
$projectViews[$model->getProject()->getId()]->setBudgetStatisticModel($model);
|
$project = $model->getProject();
|
||||||
|
$projectViews[$project->getId()] = new ProjectViewModel($model);
|
||||||
}
|
}
|
||||||
|
|
||||||
$projectIds = array_keys($projectViews);
|
$projectIds = array_keys($projectViews);
|
||||||
@@ -741,7 +739,6 @@ class ProjectStatisticService
|
|||||||
$tplQb = $this->timesheetRepository->createQueryBuilder('t');
|
$tplQb = $this->timesheetRepository->createQueryBuilder('t');
|
||||||
$tplQb
|
$tplQb
|
||||||
->select('IDENTITY(t.project) AS id')
|
->select('IDENTITY(t.project) AS id')
|
||||||
->addSelect('COUNT(t.id) as amount')
|
|
||||||
->addSelect('COALESCE(SUM(t.duration), 0) AS duration')
|
->addSelect('COALESCE(SUM(t.duration), 0) AS duration')
|
||||||
->addSelect('COALESCE(SUM(t.rate), 0) AS rate')
|
->addSelect('COALESCE(SUM(t.rate), 0) AS rate')
|
||||||
->andWhere($tplQb->expr()->in('t.project', ':project'))
|
->andWhere($tplQb->expr()->in('t.project', ':project'))
|
||||||
@@ -749,14 +746,11 @@ class ProjectStatisticService
|
|||||||
->setParameter('project', array_values($projectIds))
|
->setParameter('project', array_values($projectIds))
|
||||||
;
|
;
|
||||||
|
|
||||||
|
// find the most recent timesheet for each project
|
||||||
$qb = clone $tplQb;
|
$qb = clone $tplQb;
|
||||||
$qb->addSelect('MAX(t.date) as lastRecord');
|
$qb->addSelect('MAX(t.date) as lastRecord');
|
||||||
|
|
||||||
$result = $qb->getQuery()->getScalarResult();
|
$result = $qb->getQuery()->getScalarResult();
|
||||||
foreach ($result as $row) {
|
foreach ($result as $row) {
|
||||||
$projectViews[$row['id']]->setDurationTotal($row['duration']);
|
|
||||||
$projectViews[$row['id']]->setRateTotal($row['rate']);
|
|
||||||
$projectViews[$row['id']]->setTimesheetCounter($row['amount']);
|
|
||||||
if ($row['lastRecord'] !== null) {
|
if ($row['lastRecord'] !== null) {
|
||||||
// might be the wrong timezone
|
// might be the wrong timezone
|
||||||
$projectViews[$row['id']]->setLastRecord($factory->createDateTime($row['lastRecord']));
|
$projectViews[$row['id']]->setLastRecord($factory->createDateTime($row['lastRecord']));
|
||||||
@@ -801,34 +795,6 @@ class ProjectStatisticService
|
|||||||
$projectViews[$row['id']]->setDurationMonth($row['duration']);
|
$projectViews[$row['id']]->setDurationMonth($row['duration']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$qb = clone $tplQb;
|
|
||||||
$qb
|
|
||||||
->addSelect('t.exported')
|
|
||||||
->addSelect('t.billable')
|
|
||||||
->addGroupBy('t.exported')
|
|
||||||
->addGroupBy('t.billable')
|
|
||||||
;
|
|
||||||
$result = $qb->getQuery()->getScalarResult();
|
|
||||||
foreach ($result as $row) {
|
|
||||||
/** @var ProjectViewModel $view */
|
|
||||||
$view = $projectViews[$row['id']];
|
|
||||||
if ($row['billable'] === 1 && $row['exported'] === 1) {
|
|
||||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
|
||||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
|
||||||
} elseif ($row['billable'] === 1 && $row['exported'] === 0) {
|
|
||||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
|
||||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
|
||||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
|
||||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
|
||||||
$view->setNotBilledDuration($view->getNotBilledDuration() + $row['duration']);
|
|
||||||
$view->setNotBilledRate($view->getNotBilledRate() + $row['rate']);
|
|
||||||
} elseif ($row['billable'] === 0 && $row['exported'] === 0) {
|
|
||||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
|
||||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
|
||||||
}
|
|
||||||
// the last possible case $row['billable'] === 0 && $row['exported'] === 1 is extremely unlikely and not used
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_values($projectViews);
|
return array_values($projectViews);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ final class ProjectDateRangeForm extends AbstractType
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->add('month', MonthPickerType::class, [
|
$builder->add('month', MonthPickerType::class, [
|
||||||
|
'required' => true,
|
||||||
'label' => false,
|
'label' => false,
|
||||||
'view_timezone' => $options['timezone'],
|
'view_timezone' => $options['timezone'],
|
||||||
'model_timezone' => $options['timezone'],
|
'model_timezone' => $options['timezone'],
|
||||||
|
|||||||
@@ -14,16 +14,14 @@ use App\Entity\User;
|
|||||||
|
|
||||||
final class ProjectDateRangeQuery
|
final class ProjectDateRangeQuery
|
||||||
{
|
{
|
||||||
private \DateTime $month;
|
private ?\DateTime $month;
|
||||||
private ?User $user;
|
|
||||||
private ?Customer $customer = null;
|
private ?Customer $customer = null;
|
||||||
private bool $includeNoWork = false;
|
private bool $includeNoWork = false;
|
||||||
private ?string $budgetType = null;
|
private ?string $budgetType = null;
|
||||||
|
|
||||||
public function __construct(\DateTime $month, User $user)
|
public function __construct(\DateTime $month, private User $user)
|
||||||
{
|
{
|
||||||
$this->month = clone $month;
|
$this->month = clone $month;
|
||||||
$this->user = $user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isBudgetIndependent(): bool
|
public function isBudgetIndependent(): bool
|
||||||
@@ -46,7 +44,7 @@ final class ProjectDateRangeQuery
|
|||||||
$this->includeNoWork = $includeNoWork;
|
$this->includeNoWork = $includeNoWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUser(): ?User
|
public function getUser(): User
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,13 @@ use DateTime;
|
|||||||
final class ProjectInactiveQuery
|
final class ProjectInactiveQuery
|
||||||
{
|
{
|
||||||
private DateTime $lastChange;
|
private DateTime $lastChange;
|
||||||
private User $user;
|
|
||||||
|
|
||||||
public function __construct(DateTime $lastChange, User $user)
|
public function __construct(DateTime $lastChange, private User $user)
|
||||||
{
|
{
|
||||||
$this->lastChange = clone $lastChange;
|
$this->lastChange = clone $lastChange;
|
||||||
$this->user = $user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUser(): ?User
|
public function getUser(): User
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,42 +11,24 @@ namespace App\Reporting\ProjectView;
|
|||||||
|
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Model\BudgetStatisticModelInterface;
|
use App\Model\BudgetStatisticModelInterface;
|
||||||
|
use App\Model\ProjectBudgetStatisticModel;
|
||||||
|
use App\Model\Statistic\BudgetStatistic;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
|
||||||
final class ProjectViewModel
|
final class ProjectViewModel
|
||||||
{
|
{
|
||||||
private int $timesheetCounter = 0;
|
|
||||||
private int $durationDay = 0;
|
private int $durationDay = 0;
|
||||||
private int $durationWeek = 0;
|
private int $durationWeek = 0;
|
||||||
private int $durationMonth = 0;
|
private int $durationMonth = 0;
|
||||||
private int $durationTotal = 0;
|
|
||||||
private float $rateTotal = 0.00;
|
|
||||||
private int $notExportedDuration = 0;
|
|
||||||
private float $notExportedRate = 0.00;
|
|
||||||
private int $notBilledDuration = 0;
|
|
||||||
private float $notBilledRate = 0.00;
|
|
||||||
private int $billableDuration = 0;
|
|
||||||
private float $billableRate = 0.00;
|
|
||||||
private ?DateTime $lastRecord = null;
|
private ?DateTime $lastRecord = null;
|
||||||
private ?BudgetStatisticModelInterface $budgetStatisticModel = null;
|
|
||||||
|
|
||||||
public function __construct(private Project $project)
|
public function __construct(private ProjectBudgetStatisticModel $budgetStatisticModel)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProject(): Project
|
public function getProject(): Project
|
||||||
{
|
{
|
||||||
return $this->project;
|
return $this->budgetStatisticModel->getProject();
|
||||||
}
|
|
||||||
|
|
||||||
public function getTimesheetCounter(): int
|
|
||||||
{
|
|
||||||
return $this->timesheetCounter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setTimesheetCounter(int $timesheetCounter): void
|
|
||||||
{
|
|
||||||
$this->timesheetCounter = $timesheetCounter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDurationDay(): int
|
public function getDurationDay(): int
|
||||||
@@ -79,84 +61,47 @@ final class ProjectViewModel
|
|||||||
$this->durationMonth = $durationMonth;
|
$this->durationMonth = $durationMonth;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDurationTotal(): int
|
private function getTotals(): BudgetStatistic
|
||||||
{
|
{
|
||||||
return $this->durationTotal;
|
if ($this->budgetStatisticModel->getStatisticTotal() === null) {
|
||||||
|
throw new \InvalidArgumentException('Totals must not be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->budgetStatisticModel->getStatisticTotal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDurationTotal(int $durationTotal): void
|
public function getDurationTotal(): int
|
||||||
{
|
{
|
||||||
$this->durationTotal = $durationTotal;
|
return $this->getTotals()->getDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNotExportedDuration(): int
|
public function getNotExportedDuration(): int
|
||||||
{
|
{
|
||||||
return $this->notExportedDuration;
|
$totals = $this->getTotals();
|
||||||
}
|
|
||||||
|
|
||||||
public function setNotExportedDuration(int $notExportedDuration): void
|
return $totals->getDurationBillable() - $totals->getDurationBillableExported();
|
||||||
{
|
|
||||||
$this->notExportedDuration = $notExportedDuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNotExportedRate(): float
|
public function getNotExportedRate(): float
|
||||||
{
|
{
|
||||||
return $this->notExportedRate;
|
$totals = $this->getTotals();
|
||||||
}
|
|
||||||
|
|
||||||
public function setNotExportedRate(float $notExportedRate): void
|
return $totals->getRateBillable() - $totals->getRateBillableExported();
|
||||||
{
|
|
||||||
$this->notExportedRate = $notExportedRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getNotBilledDuration(): int
|
|
||||||
{
|
|
||||||
return $this->notBilledDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setNotBilledDuration(int $notBilledDuration): void
|
|
||||||
{
|
|
||||||
$this->notBilledDuration = $notBilledDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getNotBilledRate(): float
|
|
||||||
{
|
|
||||||
return $this->notBilledRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setNotBilledRate(float $notBilledRate): void
|
|
||||||
{
|
|
||||||
$this->notBilledRate = $notBilledRate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBillableDuration(): int
|
public function getBillableDuration(): int
|
||||||
{
|
{
|
||||||
return $this->billableDuration;
|
return $this->getTotals()->getDurationBillable();
|
||||||
}
|
|
||||||
|
|
||||||
public function setBillableDuration(int $billableDuration): void
|
|
||||||
{
|
|
||||||
$this->billableDuration = $billableDuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBillableRate(): float
|
public function getBillableRate(): float
|
||||||
{
|
{
|
||||||
return $this->billableRate;
|
return $this->getTotals()->getRateBillable();
|
||||||
}
|
|
||||||
|
|
||||||
public function setBillableRate(float $billableRate): void
|
|
||||||
{
|
|
||||||
$this->billableRate = $billableRate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRateTotal(): float
|
public function getRateTotal(): float
|
||||||
{
|
{
|
||||||
return $this->rateTotal;
|
return $this->getTotals()->getRate();
|
||||||
}
|
|
||||||
|
|
||||||
public function setRateTotal(float $rateTotal): void
|
|
||||||
{
|
|
||||||
$this->rateTotal = $rateTotal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastRecord(): ?DateTime
|
public function getLastRecord(): ?DateTime
|
||||||
@@ -173,9 +118,4 @@ final class ProjectViewModel
|
|||||||
{
|
{
|
||||||
return $this->budgetStatisticModel;
|
return $this->budgetStatisticModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setBudgetStatisticModel(BudgetStatisticModelInterface $budgetStatisticModel): void
|
|
||||||
{
|
|
||||||
$this->budgetStatisticModel = $budgetStatisticModel;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ final class ProjectViewQuery
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUser(): ?User
|
public function getUser(): User
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,8 +142,12 @@ class BaseQuery
|
|||||||
return $this->orderBy;
|
return $this->orderBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setOrderBy(string $orderBy): self
|
public function setOrderBy(?string $orderBy): self
|
||||||
{
|
{
|
||||||
|
if ($orderBy === null) {
|
||||||
|
$orderBy = (string) $this->defaults['orderBy']; // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
|
||||||
$this->orderBy = $orderBy;
|
$this->orderBy = $orderBy;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@@ -154,8 +158,12 @@ class BaseQuery
|
|||||||
return $this->order;
|
return $this->order;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setOrder(string $order): self
|
public function setOrder(?string $order): self
|
||||||
{
|
{
|
||||||
|
if ($order === null) {
|
||||||
|
$order = (string) $this->defaults['order']; // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
|
||||||
if (\in_array($order, [self::ORDER_ASC, self::ORDER_DESC])) {
|
if (\in_array($order, [self::ORDER_ASC, self::ORDER_DESC])) {
|
||||||
$this->order = $order;
|
$this->order = $order;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
|
|
||||||
namespace App\Repository\Query;
|
namespace App\Repository\Query;
|
||||||
|
|
||||||
|
use App\Entity\Activity;
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Project;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
|
||||||
class TeamQuery extends BaseQuery
|
class TeamQuery extends BaseQuery
|
||||||
@@ -19,12 +22,27 @@ class TeamQuery extends BaseQuery
|
|||||||
* @var User[]
|
* @var User[]
|
||||||
*/
|
*/
|
||||||
private array $users = [];
|
private array $users = [];
|
||||||
|
/**
|
||||||
|
* @var array<Customer>
|
||||||
|
*/
|
||||||
|
private array $customers = [];
|
||||||
|
/**
|
||||||
|
* @var array<Project>
|
||||||
|
*/
|
||||||
|
private array $projects = [];
|
||||||
|
/**
|
||||||
|
* @var array<Activity>
|
||||||
|
*/
|
||||||
|
private array $activities = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->setDefaults([
|
$this->setDefaults([
|
||||||
'orderBy' => 'name',
|
'orderBy' => 'name',
|
||||||
'users' => [],
|
'users' => [],
|
||||||
|
'customers' => [],
|
||||||
|
'projects' => [],
|
||||||
|
'activities' => [],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,20 +51,16 @@ class TeamQuery extends BaseQuery
|
|||||||
return !empty($this->users);
|
return !empty($this->users);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addUser(User $user): self
|
public function addUser(User $user): void
|
||||||
{
|
{
|
||||||
$this->users[$user->getId()] = $user;
|
$this->users[$user->getId()] = $user;
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function removeUser(User $user): self
|
public function removeUser(User $user): void
|
||||||
{
|
{
|
||||||
if (isset($this->users[$user->getId()])) {
|
if (isset($this->users[$user->getId()])) {
|
||||||
unset($this->users[$user->getId()]);
|
unset($this->users[$user->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,4 +70,82 @@ class TeamQuery extends BaseQuery
|
|||||||
{
|
{
|
||||||
return array_values($this->users);
|
return array_values($this->users);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasCustomers(): bool
|
||||||
|
{
|
||||||
|
return \count($this->customers) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Customer[]
|
||||||
|
*/
|
||||||
|
public function getCustomers(): array
|
||||||
|
{
|
||||||
|
return $this->customers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<Customer> $customers
|
||||||
|
*/
|
||||||
|
public function setCustomers(array $customers): void
|
||||||
|
{
|
||||||
|
$this->customers = $customers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addCustomer(Customer $customer): void
|
||||||
|
{
|
||||||
|
$this->customers[] = $customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasProjects(): bool
|
||||||
|
{
|
||||||
|
return \count($this->projects) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Project[]
|
||||||
|
*/
|
||||||
|
public function getProjects(): array
|
||||||
|
{
|
||||||
|
return $this->projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<Project> $projects
|
||||||
|
*/
|
||||||
|
public function setProjects(array $projects): void
|
||||||
|
{
|
||||||
|
$this->projects = $projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addProject(Project $project): void
|
||||||
|
{
|
||||||
|
$this->projects[] = $project;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasActivities(): bool
|
||||||
|
{
|
||||||
|
return \count($this->activities) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Activity[]
|
||||||
|
*/
|
||||||
|
public function getActivities(): array
|
||||||
|
{
|
||||||
|
return $this->activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<Activity> $activities
|
||||||
|
*/
|
||||||
|
public function setActivities(array $activities): void
|
||||||
|
{
|
||||||
|
$this->activities = $activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addActivity(Activity $activity): void
|
||||||
|
{
|
||||||
|
$this->activities[] = $activity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,12 +148,9 @@ class TeamRepository extends EntityRepository
|
|||||||
|
|
||||||
private function getQueryBuilderForQuery(TeamQuery $query): QueryBuilder
|
private function getQueryBuilderForQuery(TeamQuery $query): QueryBuilder
|
||||||
{
|
{
|
||||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
$qb = $this->createQueryBuilder('t');
|
||||||
|
|
||||||
$qb
|
$qb->select('t');
|
||||||
->select('t')
|
|
||||||
->from(Team::class, 't')
|
|
||||||
;
|
|
||||||
|
|
||||||
$orderBy = $query->getOrderBy();
|
$orderBy = $query->getOrderBy();
|
||||||
switch ($orderBy) {
|
switch ($orderBy) {
|
||||||
@@ -162,6 +159,30 @@ class TeamRepository extends EntityRepository
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($query->hasCustomers()) {
|
||||||
|
$qb->leftJoin('t.customers', 'qCustomers');
|
||||||
|
$qb->orWhere(
|
||||||
|
$qb->expr()->in('qCustomers', ':customers')
|
||||||
|
);
|
||||||
|
$qb->setParameter('customers', $query->getCustomers());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->hasProjects()) {
|
||||||
|
$qb->leftJoin('t.projects', 'qProjects');
|
||||||
|
$qb->orWhere(
|
||||||
|
$qb->expr()->in('qProjects', ':projects')
|
||||||
|
);
|
||||||
|
$qb->setParameter('projects', $query->getProjects());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->hasActivities()) {
|
||||||
|
$qb->leftJoin('t.activities', 'qActivities');
|
||||||
|
$qb->orWhere(
|
||||||
|
$qb->expr()->in('qActivities', ':activities')
|
||||||
|
);
|
||||||
|
$qb->setParameter('activities', $query->getActivities());
|
||||||
|
}
|
||||||
|
|
||||||
if ($query->hasUsers()) {
|
if ($query->hasUsers()) {
|
||||||
$qb->leftJoin('t.members', 'qMembers');
|
$qb->leftJoin('t.members', 'qMembers');
|
||||||
$qb->orWhere(
|
$qb->orWhere(
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
|||||||
/**
|
/**
|
||||||
* @extends \Doctrine\ORM\EntityRepository<User>
|
* @extends \Doctrine\ORM\EntityRepository<User>
|
||||||
* @template-implements PasswordUpgraderInterface<User>
|
* @template-implements PasswordUpgraderInterface<User>
|
||||||
|
* @template-implements UserProviderInterface<User>
|
||||||
*/
|
*/
|
||||||
class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface
|
class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
|||||||
|
|
||||||
final class SamlProvider
|
final class SamlProvider
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param UserProviderInterface<User> $userProvider
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private UserRepository $repository,
|
private UserRepository $repository,
|
||||||
private UserProviderInterface $userProvider,
|
private UserProviderInterface $userProvider,
|
||||||
@@ -30,8 +33,10 @@ final class SamlProvider
|
|||||||
$user = null;
|
$user = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** @var User $user */
|
if ($token->getUserIdentifier() !== null) {
|
||||||
$user = $this->userProvider->loadUserByIdentifier($token->getUserIdentifier());
|
/** @var User $user */
|
||||||
|
$user = $this->userProvider->loadUserByIdentifier($token->getUserIdentifier());
|
||||||
|
}
|
||||||
} catch (UserNotFoundException $e) {
|
} catch (UserNotFoundException $e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @template-implements PasswordUpgraderInterface<User>
|
* @template-implements PasswordUpgraderInterface<User>
|
||||||
|
* @template-implements UserProviderInterface<User>
|
||||||
*/
|
*/
|
||||||
final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface
|
||||||
{
|
{
|
||||||
private ?ChainUserProvider $provider = null;
|
private ?ChainUserProvider $provider = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param iterable|UserProviderInterface[] $providers
|
* @param iterable<UserProviderInterface<User>> $providers
|
||||||
*/
|
*/
|
||||||
public function __construct(private iterable $providers, private SystemConfiguration $configuration)
|
public function __construct(private iterable $providers, private SystemConfiguration $configuration)
|
||||||
{
|
{
|
||||||
@@ -60,12 +61,12 @@ final class KimaiUserProvider implements UserProviderInterface, PasswordUpgrader
|
|||||||
|
|
||||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||||
{
|
{
|
||||||
return $this->getInternalProvider()->loadUserByIdentifier($identifier);
|
return $this->getInternalProvider()->loadUserByIdentifier($identifier); // @phpstan-ignore-line
|
||||||
}
|
}
|
||||||
|
|
||||||
public function refreshUser(UserInterface $user): UserInterface
|
public function refreshUser(UserInterface $user): UserInterface
|
||||||
{
|
{
|
||||||
return $this->getInternalProvider()->refreshUser($user);
|
return $this->getInternalProvider()->refreshUser($user); // @phpstan-ignore-line
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supportsClass(string $class): bool
|
public function supportsClass(string $class): bool
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ use App\Event\PageActionsEvent;
|
|||||||
use App\Event\ThemeEvent;
|
use App\Event\ThemeEvent;
|
||||||
use App\Event\ThemeJavascriptTranslationsEvent;
|
use App\Event\ThemeJavascriptTranslationsEvent;
|
||||||
use App\Utils\Color;
|
use App\Utils\Color;
|
||||||
use App\Utils\FormFormatConverter;
|
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
@@ -110,11 +109,8 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
|||||||
return (new Color())->getRandom($identifier);
|
return (new Color())->getRandom($identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTimePresets(string $timezone, string $format): array
|
public function getTimePresets(string $timezone): array
|
||||||
{
|
{
|
||||||
$converter = new FormFormatConverter();
|
|
||||||
$format = $converter->convert($format);
|
|
||||||
|
|
||||||
$intervalMinutes = $this->configuration->getTimesheetIncrementMinutes();
|
$intervalMinutes = $this->configuration->getTimesheetIncrementMinutes();
|
||||||
|
|
||||||
if ($intervalMinutes < 5) {
|
if ($intervalMinutes < 5) {
|
||||||
@@ -123,17 +119,15 @@ final class ThemeExtension implements RuntimeExtensionInterface
|
|||||||
|
|
||||||
$maxMinutes = 24 * 60 - $intervalMinutes;
|
$maxMinutes = 24 * 60 - $intervalMinutes;
|
||||||
|
|
||||||
$date = new \DateTime('now', new \DateTimeZone($timezone));
|
$date = new \DateTimeImmutable('now', new \DateTimeZone($timezone));
|
||||||
$date->setTime(0, 0, 0);
|
$date = $date->setTime(0, 0, 0);
|
||||||
|
|
||||||
$presets = [
|
$presets = [$date];
|
||||||
$date->format($format)
|
|
||||||
];
|
|
||||||
|
|
||||||
for ($minutes = $intervalMinutes; $minutes <= $maxMinutes; $minutes += $intervalMinutes) {
|
for ($minutes = $intervalMinutes; $minutes <= $maxMinutes; $minutes += $intervalMinutes) {
|
||||||
$date->modify('+' . $intervalMinutes . ' minutes');
|
$date = $date->modify('+' . $intervalMinutes . ' minutes');
|
||||||
|
|
||||||
$presets[] = $date->format($format);
|
$presets[] = $date;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $presets;
|
return $presets;
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class UserService
|
|||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findUserByUsernameOrEmail(string $usernameOrEmail): ?User
|
public function findUserByUsernameOrEmail(string $usernameOrEmail): User
|
||||||
{
|
{
|
||||||
return $this->repository->loadUserByIdentifier($usernameOrEmail);
|
return $this->repository->loadUserByIdentifier($usernameOrEmail);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,14 +57,14 @@ final class SearchTerm
|
|||||||
return $this->fields;
|
return $this->fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSearchTerm(): ?string
|
public function getSearchTerm(): string
|
||||||
{
|
{
|
||||||
return $this->term;
|
return $this->term;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasSearchTerm(): bool
|
public function hasSearchTerm(): bool
|
||||||
{
|
{
|
||||||
return !empty($this->term);
|
return $this->term !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOriginalSearch(): string
|
public function getOriginalSearch(): string
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class Customer extends Constraint
|
|||||||
|
|
||||||
public string $message = 'This customer has invalid settings.';
|
public string $message = 'This customer has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class DateTimeFormat extends Constraint
|
|||||||
public ?string $separator = null;
|
public ?string $separator = null;
|
||||||
public ?string $message = 'This datetime format is invalid.';
|
public ?string $message = 'This datetime format is invalid.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::PROPERTY_CONSTRAINT;
|
return self::PROPERTY_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class Project extends Constraint
|
|||||||
|
|
||||||
public string $message = 'This project has invalid settings.';
|
public string $message = 'This project has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class Team extends Constraint
|
|||||||
|
|
||||||
public string $message = 'The team has invalid settings.';
|
public string $message = 'The team has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ final class TimeFormat extends Constraint
|
|||||||
|
|
||||||
public string $message = 'This time format is invalid.';
|
public string $message = 'This time format is invalid.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::PROPERTY_CONSTRAINT;
|
return self::PROPERTY_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ final class Timesheet extends Constraint
|
|||||||
{
|
{
|
||||||
public string $message = 'This timesheet has invalid settings.';
|
public string $message = 'This timesheet has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class TimesheetBasic extends TimesheetConstraint
|
|||||||
|
|
||||||
public string $message = 'This timesheet has invalid settings.';
|
public string $message = 'This timesheet has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ final class TimesheetDeactivated extends TimesheetConstraint
|
|||||||
|
|
||||||
public string $message = 'This timesheet has invalid settings.';
|
public string $message = 'This timesheet has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ final class TimesheetExported extends TimesheetConstraint
|
|||||||
*/
|
*/
|
||||||
public null|\DateTime|string $now;
|
public null|\DateTime|string $now;
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class TimesheetFutureTimes extends TimesheetConstraint
|
|||||||
|
|
||||||
public string $message = 'The date cannot be in the future.';
|
public string $message = 'The date cannot be in the future.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ final class TimesheetLockdown extends TimesheetConstraint
|
|||||||
*/
|
*/
|
||||||
public \DateTime|string|null $now;
|
public \DateTime|string|null $now;
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class TimesheetLongRunning extends TimesheetConstraint
|
|||||||
public string $message = 'Maximum duration of {{ value }} hours exceeded.';
|
public string $message = 'Maximum duration of {{ value }} hours exceeded.';
|
||||||
public string $maximumMessage = 'Maximum duration exceeded.';
|
public string $maximumMessage = 'Maximum duration exceeded.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ final class TimesheetMultiUpdate extends Constraint
|
|||||||
|
|
||||||
public string $message = 'This form has invalid settings.';
|
public string $message = 'This form has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class TimesheetMultiUser extends Constraint
|
|||||||
|
|
||||||
public string $message = 'This form has invalid settings.';
|
public string $message = 'This form has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ final class TimesheetOverlapping extends TimesheetConstraint
|
|||||||
|
|
||||||
public string $message = 'You already have an entry for this time.';
|
public string $message = 'You already have an entry for this time.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ final class TimesheetRestart extends TimesheetConstraint
|
|||||||
|
|
||||||
public string $message = 'You are not allowed to start this timesheet record.';
|
public string $message = 'You are not allowed to start this timesheet record.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ final class TimesheetZeroDuration extends TimesheetConstraint
|
|||||||
|
|
||||||
public string $message = 'Duration cannot be zero.';
|
public string $message = 'Duration cannot be zero.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ final class User extends Constraint
|
|||||||
|
|
||||||
public string $message = 'The user has invalid settings.';
|
public string $message = 'The user has invalid settings.';
|
||||||
|
|
||||||
public function getTargets(): string|array
|
public function getTargets(): string
|
||||||
{
|
{
|
||||||
return self::CLASS_CONSTRAINT;
|
return self::CLASS_CONSTRAINT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if year is defined %}
|
{% if year is defined %}
|
||||||
{% embed '@theme/embeds/collapsible.html.twig' with {open: not box_configuration.collapsed, id: 'working_time_detail_box', border: false, item: {options: {bodyExtraClass: 'border-top'}, bodyClasses: 'p-0 table-responsive'}} %}
|
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'working_time_detail_box', border: false, item: {options: {bodyExtraClass: 'border-top', open: not box_configuration.collapsed}, bodyClasses: 'p-0 table-responsive'}} %}
|
||||||
{% set yearShould = 0 %}
|
{% set yearShould = 0 %}
|
||||||
{% set yearIs = 0 %}
|
{% set yearIs = 0 %}
|
||||||
{% set approved = 0 %}
|
{% set approved = 0 %}
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
{% if withWorkHourConfiguration %}
|
{% if withWorkHourConfiguration %}
|
||||||
{% set expectedWeekTimes = user.workHoursMonday + user.workHoursTuesday + user.workHoursWednesday + user.workHoursThursday + user.workHoursFriday + user.workHoursSaturday + user.workHoursSunday %}
|
{% set expectedWeekTimes = user.workHoursMonday + user.workHoursTuesday + user.workHoursWednesday + user.workHoursThursday + user.workHoursFriday + user.workHoursSaturday + user.workHoursSunday %}
|
||||||
|
|
||||||
{% embed '@theme/embeds/collapsible.html.twig' with {open: not box_configuration.collapsed, id: 'work_contract_should_preview', border: false, item: {options: {bodyExtraClass: 'border-top'}}} %}
|
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'work_contract_should_preview', border: false, item: {options: {bodyExtraClass: 'border-top'}}} %}
|
||||||
{% from "macros/status.html.twig" import status_duration %}
|
{% from "macros/status.html.twig" import status_duration %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ 'work_times_should'|trans }}
|
{{ 'work_times_should'|trans }}
|
||||||
|
|||||||
@@ -15,14 +15,14 @@
|
|||||||
<table class="table table-hover dataTable">
|
<table class="table table-hover dataTable">
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ 'timeBudget'|trans }}</td>
|
<td>{{ 'timeBudget'|trans }}</td>
|
||||||
<td class="text-nowrap text-end">
|
<td class="text-nowrap text-end w-min">
|
||||||
{% if entity.timeBudget > 0 %}
|
{% if entity.timeBudget > 0 %}
|
||||||
{{ entity.timeBudget|duration }}
|
{{ entity.timeBudget|duration }}
|
||||||
{% else %}
|
{% else %}
|
||||||
-
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap text-end">
|
<td class="text-nowrap text-end w-min">
|
||||||
{% if entity.timeBudget > 0 %}
|
{% if entity.timeBudget > 0 %}
|
||||||
100%
|
100%
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -48,6 +48,18 @@
|
|||||||
<td class="text-nowrap text-end">{{ stats.durationBillable|duration }}</td>
|
<td class="text-nowrap text-end">{{ stats.durationBillable|duration }}</td>
|
||||||
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
|
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ 'not_exported'|trans }}</td>
|
||||||
|
<td class="text-nowrap text-end">
|
||||||
|
{% set not_exported = (stats.statisticTotal.durationBillable - stats.statisticTotal.durationBillableExported) %}
|
||||||
|
{% if export_url is defined and export_url is not null %}
|
||||||
|
<a href="{{ export_url }}">{{ not_exported|duration }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ not_exported|duration }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap text-end">-</td>
|
||||||
|
</tr>
|
||||||
{% if entity.timeBudget > 0 %}
|
{% if entity.timeBudget > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="3" class="text-nowrap text-end">
|
<th colspan="3" class="text-nowrap text-end">
|
||||||
@@ -87,14 +99,14 @@
|
|||||||
<table class="table table-hover dataTable">
|
<table class="table table-hover dataTable">
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ 'budget'|trans }}</td>
|
<td>{{ 'budget'|trans }}</td>
|
||||||
<td class="text-nowrap text-end">
|
<td class="text-nowrap text-end w-min">
|
||||||
{% if entity.budget > 0 %}
|
{% if entity.budget > 0 %}
|
||||||
{{ entity.budget|money(currency) }}
|
{{ entity.budget|money(currency) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
-
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap text-end">
|
<td class="text-nowrap text-end w-min">
|
||||||
{% if entity.budget > 0 %}
|
{% if entity.budget > 0 %}
|
||||||
100%
|
100%
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -120,6 +132,18 @@
|
|||||||
<td class="text-nowrap text-end">{{ stats.rateBillable|money(currency) }}</td>
|
<td class="text-nowrap text-end">{{ stats.rateBillable|money(currency) }}</td>
|
||||||
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
|
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ 'not_invoiced'|trans }}</td>
|
||||||
|
<td class="text-nowrap text-end">
|
||||||
|
{% set not_exported = (stats.statisticTotal.rateBillable - stats.statisticTotal.rateBillableExported) %}
|
||||||
|
{% if invoice_url is defined and invoice_url is not null %}
|
||||||
|
<a href="{{ invoice_url }}">{{ not_exported|money(currency) }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ not_exported|money(currency) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap text-end">-</td>
|
||||||
|
</tr>
|
||||||
{% set percentReached = 0 %}
|
{% set percentReached = 0 %}
|
||||||
{% if stats.rateBillable > 0 %}
|
{% if stats.rateBillable > 0 %}
|
||||||
{% set percentReached = (stats.internalRate / (stats.rateBillable / 100)) %}
|
{% set percentReached = (stats.internalRate / (stats.rateBillable / 100)) %}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
<a href="#" data-form-widget="date-now" data-format="{{ jsFormat }}" data-target="{{ id }}">{{ icon('calendar') }}</a>
|
<a href="#" data-form-widget="date-now" data-format="{{ jsFormat }}" data-target="{{ id }}">{{ icon('calendar') }}</a>
|
||||||
</span>
|
</span>
|
||||||
{{ block('form_widget_simple') }}
|
{{ block('form_widget_simple') }}
|
||||||
{% set time_presets = form_time_presets(app.user.timezone, format) %}
|
{% set time_presets = form_time_presets(app.user.timezone) %}
|
||||||
{% if time_presets|length > 0 and form.vars.disabled is same as (false) %}
|
{% if time_presets|length > 0 and form.vars.disabled is same as (false) %}
|
||||||
<button type="button" class="input-group-text dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
|
<button type="button" class="input-group-text dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
|
||||||
<div class="dropdown-menu dropdown-menu-end pre-scrollable">
|
<div class="dropdown-menu dropdown-menu-end pre-scrollable">
|
||||||
@@ -155,6 +155,7 @@
|
|||||||
{% for index in [0, 1, 2, 3] %}
|
{% for index in [0, 1, 2, 3] %}
|
||||||
<div class="dropdown-menu-column" style="min-width: 4rem">
|
<div class="dropdown-menu-column" style="min-width: 4rem">
|
||||||
{% for value in time_presets %}
|
{% for value in time_presets %}
|
||||||
|
{% set value = value|time %}
|
||||||
{% if loop.index0 % 4 == index %}
|
{% if loop.index0 % 4 == index %}
|
||||||
<a class="dropdown-item justify-content-center" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ value }}" data-event="change">{{ value }}</a>
|
<a class="dropdown-item justify-content-center" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ value }}" data-event="change">{{ value }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -11,32 +11,30 @@
|
|||||||
{% set help = null %}
|
{% set help = null %}
|
||||||
<div class="page-actions">
|
<div class="page-actions">
|
||||||
<div class="pa-desktop d-none d-sm-inline-flex btn-list">
|
<div class="pa-desktop d-none d-sm-inline-flex btn-list">
|
||||||
{%- apply spaceless -%}
|
{%- for icon, values in actions %}
|
||||||
{%- for icon, values in actions %}
|
{% if 'help' in icon %}
|
||||||
{% if 'help' in icon %}
|
{% set help = values %}
|
||||||
{% set help = values %}
|
{% elseif 'divider' in icon and values is null %}
|
||||||
{% elseif 'divider' in icon and values is null %}
|
{# what to do here ? #}
|
||||||
{# what to do here ? #}
|
{% else %}
|
||||||
{% else %}
|
{% if values.children is defined and values.children|length > 0 %}
|
||||||
{% if values.children is defined and values.children|length > 0 %}
|
<div class="dropdown">
|
||||||
<div class="dropdown">
|
<button type="button" class="btn {{ btnClasses}} dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<button type="button" class="btn {{ btnClasses}} dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
{{ icon(icon, true) }}{% if large %} {{ values.title is defined ? values.title|trans : icon|trans({}, 'actions') }}{% endif %}
|
||||||
{{ icon(icon, true) }}{% if large %} {{ values.title is defined ? values.title|trans : icon|trans({}, 'actions') }}{% endif %}
|
</button>
|
||||||
</button>
|
<div class="dropdown-menu">
|
||||||
<div class="dropdown-menu">
|
{%- for icon, values in values.children %}
|
||||||
{%- for icon, values in values.children %}
|
{% set values = values|merge({class: 'dropdown-item action-' ~ icon ~ ' ' ~ (values.class|default(''))}) %}
|
||||||
{% set values = values|merge({class: 'dropdown-item action-' ~ icon ~ ' ' ~ (values.class|default(''))}) %}
|
{{ _self.action_button(false, values, false) }}
|
||||||
{{ _self.action_button(false, values, false) }}
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
{% set values = values|merge({combined: large, class: btnClasses ~ ' action-' ~ icon ~ ' ' ~ (values.class|default(''))}) %}
|
{% else %}
|
||||||
{{ _self.action_button(icon, values) }}
|
{% set values = values|merge({combined: large, class: btnClasses ~ ' action-' ~ icon ~ ' ' ~ (values.class|default(''))}) %}
|
||||||
{% endif %}
|
{{ _self.action_button(icon, values) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor -%}
|
{% endif %}
|
||||||
{% endapply %}
|
{% endfor -%}
|
||||||
{% if help is not null %}
|
{% if help is not null %}
|
||||||
{% set help = help|merge({class: helpClasses ~ ' action-help ' ~ (help.class|default(''))}) %}
|
{% set help = help|merge({class: helpClasses ~ ' action-help ' ~ (help.class|default(''))}) %}
|
||||||
{{ _self.action_button('help', help) }}
|
{{ _self.action_button('help', help) }}
|
||||||
@@ -191,14 +189,12 @@
|
|||||||
|
|
||||||
{# for @internal use only #}
|
{# for @internal use only #}
|
||||||
{% macro label_color_dot(type, isVisible, name, url, color) %}
|
{% macro label_color_dot(type, isVisible, name, url, color) %}
|
||||||
{% apply spaceless %}
|
|
||||||
{%- if url is not empty -%}<a href="{{ url }}">{% endif %}
|
{%- if url is not empty -%}<a href="{{ url }}">{% endif %}
|
||||||
<span class="pe-1 label-{{ type }}{{ isVisible is same as (false) ? ' label-invisible' : '' }}">
|
<span class="pe-1 label-{{ type }}{{ isVisible is same as (false) ? ' label-invisible' : '' }}">
|
||||||
<span class="badge me-1" {% if color is not empty %} style="background-color:{{ color }}"{% endif %}></span>
|
<span class="badge me-1" {% if color is not empty %} style="background-color:{{ color }}"{% endif %}></span>
|
||||||
{{- name -}}
|
{{- name -}}
|
||||||
</span>
|
</span>
|
||||||
{%- if url is not empty -%}</a>{% endif %}
|
{%- if url is not empty -%}</a>{% endif %}
|
||||||
{% endapply %}
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro badge_team_access(teams) %}
|
{% macro badge_team_access(teams) %}
|
||||||
|
|||||||
@@ -395,7 +395,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="datagrid-content">
|
<div class="datagrid-content">
|
||||||
<a href="{{ path('invoice', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': ''}) }}">
|
<a href="{{ path('invoice', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': ''}) }}">
|
||||||
{{ project_view.notBilledRate|money(currency) }}
|
{{ project_view.notExportedRate|money(currency) }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% elseif name == 'invoiced' %}
|
{% elseif name == 'invoiced' %}
|
||||||
<a href="{{ path('invoice', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': ''}) }}">
|
<a href="{{ path('invoice', {'customers[]': project.customer.id, 'projects[]': project.id, 'daterange': ''}) }}">
|
||||||
{{ entry.notBilledRate|money(currency) }}
|
{{ entry.notExportedRate|money(currency) }}
|
||||||
</a>
|
</a>
|
||||||
{% elseif name == 'projectStart' %}
|
{% elseif name == 'projectStart' %}
|
||||||
{% if project.start is not null %}{{ project.start|date_short }}{% endif %}
|
{% if project.start is not null %}{{ project.start|date_short }}{% endif %}
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ use PHPUnit\Framework\TestCase;
|
|||||||
*/
|
*/
|
||||||
class PrepareUserEventTest extends TestCase
|
class PrepareUserEventTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testGetterAndSetter()
|
public function testGetterAndSetter(): void
|
||||||
{
|
{
|
||||||
$user = new User();
|
$user = new User();
|
||||||
$sut = new PrepareUserEvent($user);
|
$sut = new PrepareUserEvent($user);
|
||||||
$this->assertSame($user, $sut->getUser());
|
$this->assertSame($user, $sut->getUser());
|
||||||
|
$this->assertTrue($sut->isBooting());
|
||||||
|
|
||||||
|
$sut = new PrepareUserEvent($user, false);
|
||||||
|
$this->assertSame($user, $sut->getUser());
|
||||||
|
$this->assertFalse($sut->isBooting());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
*/
|
*/
|
||||||
class UserPreferenceEventTest extends TestCase
|
class UserPreferenceEventTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testGetterAndSetter()
|
public function testGetterAndSetter(): void
|
||||||
{
|
{
|
||||||
$user = new User();
|
$user = new User();
|
||||||
$user->setAlias('foo');
|
$user->setAlias('foo');
|
||||||
@@ -28,14 +28,18 @@ class UserPreferenceEventTest extends TestCase
|
|||||||
$sut = new UserPreferenceEvent($user, []);
|
$sut = new UserPreferenceEvent($user, []);
|
||||||
|
|
||||||
$this->assertEquals($user, $sut->getUser());
|
$this->assertEquals($user, $sut->getUser());
|
||||||
|
$this->assertTrue($sut->isBooting());
|
||||||
$this->assertEquals([], $sut->getPreferences());
|
$this->assertEquals([], $sut->getPreferences());
|
||||||
|
|
||||||
$sut->addPreference($pref);
|
$sut->addPreference($pref);
|
||||||
|
|
||||||
$this->assertEquals([$pref], $sut->getPreferences());
|
$this->assertEquals([$pref], $sut->getPreferences());
|
||||||
|
|
||||||
|
$sut = new UserPreferenceEvent($user, [], false);
|
||||||
|
$this->assertFalse($sut->isBooting());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDuplicatePreferenceThrowsException()
|
public function testDuplicatePreferenceThrowsException(): void
|
||||||
{
|
{
|
||||||
$this->expectException(\InvalidArgumentException::class);
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
namespace App\Tests\Reporting\ProjectView;
|
namespace App\Tests\Reporting\ProjectView;
|
||||||
|
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
|
use App\Model\ProjectBudgetStatisticModel;
|
||||||
|
use App\Model\Statistic\BudgetStatistic;
|
||||||
use App\Reporting\ProjectView\ProjectViewModel;
|
use App\Reporting\ProjectView\ProjectViewModel;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@@ -18,10 +20,13 @@ use PHPUnit\Framework\TestCase;
|
|||||||
*/
|
*/
|
||||||
class ProjectViewModelTest extends TestCase
|
class ProjectViewModelTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testDefaults()
|
public function testDefaults(): void
|
||||||
{
|
{
|
||||||
$project = new Project();
|
$project = new Project();
|
||||||
$sut = new ProjectViewModel($project);
|
$model = new ProjectBudgetStatisticModel($project);
|
||||||
|
$total = new BudgetStatistic();
|
||||||
|
$model->setStatisticTotal($total);
|
||||||
|
$sut = new ProjectViewModel($model);
|
||||||
|
|
||||||
self::assertSame($project, $sut->getProject());
|
self::assertSame($project, $sut->getProject());
|
||||||
self::assertSame(0, $sut->getDurationDay());
|
self::assertSame(0, $sut->getDurationDay());
|
||||||
@@ -32,27 +37,33 @@ class ProjectViewModelTest extends TestCase
|
|||||||
self::assertSame(0.0, $sut->getNotExportedRate());
|
self::assertSame(0.0, $sut->getNotExportedRate());
|
||||||
self::assertSame(0.0, $sut->getRateTotal());
|
self::assertSame(0.0, $sut->getRateTotal());
|
||||||
self::assertNull($sut->getLastRecord());
|
self::assertNull($sut->getLastRecord());
|
||||||
self::assertSame(0, $sut->getTimesheetCounter());
|
|
||||||
self::assertNull($sut->getLastRecord());
|
self::assertNull($sut->getLastRecord());
|
||||||
self::assertSame(0.0, $sut->getBillableRate());
|
self::assertSame(0.0, $sut->getBillableRate());
|
||||||
self::assertSame(0, $sut->getBillableDuration());
|
self::assertSame(0, $sut->getBillableDuration());
|
||||||
self::assertSame(0, $sut->getNotBilledDuration());
|
|
||||||
self::assertSame(0.0, $sut->getNotBilledRate());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetterGetter()
|
public function testSetterGetter(): void
|
||||||
{
|
{
|
||||||
$sut = new ProjectViewModel(new Project());
|
$project = new Project();
|
||||||
|
|
||||||
|
$model = new ProjectBudgetStatisticModel($project);
|
||||||
|
$total = new BudgetStatistic();
|
||||||
|
$total->setCounter(123);
|
||||||
|
$total->setDuration(3456789);
|
||||||
|
$total->setDurationBillable(3456789);
|
||||||
|
$total->setDurationBillableExported(3400000);
|
||||||
|
$total->setRate(789.0);
|
||||||
|
$total->setRateBillable(16789.0);
|
||||||
|
$total->setRateBillableExported(10000.0);
|
||||||
|
$model->setStatisticTotal($total);
|
||||||
|
|
||||||
|
$sut = new ProjectViewModel($model);
|
||||||
|
|
||||||
$date = new \DateTime();
|
$date = new \DateTime();
|
||||||
|
|
||||||
$sut->setDurationDay(123456789);
|
$sut->setDurationDay(123456789);
|
||||||
$sut->setDurationMonth(23456789);
|
$sut->setDurationMonth(23456789);
|
||||||
$sut->setDurationTotal(3456789);
|
|
||||||
$sut->setDurationWeek(456789);
|
$sut->setDurationWeek(456789);
|
||||||
$sut->setNotExportedDuration(56789);
|
|
||||||
$sut->setNotExportedRate(6789);
|
|
||||||
$sut->setRateTotal(789);
|
|
||||||
$sut->setLastRecord($date);
|
$sut->setLastRecord($date);
|
||||||
|
|
||||||
self::assertSame(123456789, $sut->getDurationDay());
|
self::assertSame(123456789, $sut->getDurationDay());
|
||||||
@@ -65,18 +76,10 @@ class ProjectViewModelTest extends TestCase
|
|||||||
self::assertSame($date, $sut->getLastRecord());
|
self::assertSame($date, $sut->getLastRecord());
|
||||||
|
|
||||||
$date = new \DateTime();
|
$date = new \DateTime();
|
||||||
$sut->setTimesheetCounter(123);
|
|
||||||
$sut->setLastRecord($date);
|
$sut->setLastRecord($date);
|
||||||
$sut->setBillableRate(123.456);
|
|
||||||
$sut->setBillableDuration(321);
|
|
||||||
$sut->setNotBilledDuration(9876);
|
|
||||||
$sut->setNotBilledRate(4705.23);
|
|
||||||
|
|
||||||
self::assertSame(123, $sut->getTimesheetCounter());
|
|
||||||
self::assertSame($date, $sut->getLastRecord());
|
self::assertSame($date, $sut->getLastRecord());
|
||||||
self::assertSame(123.456, $sut->getBillableRate());
|
self::assertSame(16789.0, $sut->getBillableRate());
|
||||||
self::assertSame(321, $sut->getBillableDuration());
|
self::assertSame(3456789, $sut->getBillableDuration());
|
||||||
self::assertSame(9876, $sut->getNotBilledDuration());
|
|
||||||
self::assertSame(4705.23, $sut->getNotBilledRate());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4477,11 +4477,6 @@ parameters:
|
|||||||
count: 5
|
count: 5
|
||||||
path: Event/PermissionsEventTest.php
|
path: Event/PermissionsEventTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\PrepareUserEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Event/PrepareUserEventTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\ProjectBudgetStatisticEventTest\\:\\:testStatistic\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\Event\\\\ProjectBudgetStatisticEventTest\\:\\:testStatistic\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4607,16 +4602,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Event/UserPreferenceDisplayEventTest.php
|
path: Event/UserPreferenceDisplayEventTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\UserPreferenceEventTest\\:\\:testDuplicatePreferenceThrowsException\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Event/UserPreferenceEventTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\UserPreferenceEventTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Event/UserPreferenceEventTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Event\\\\UserRevenueStatisticEventTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\Event\\\\UserRevenueStatisticEventTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -7137,16 +7122,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Reporting/ProjectInactive/ProjectInactiveQueryTest.php
|
path: Reporting/ProjectInactive/ProjectInactiveQueryTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Reporting\\\\ProjectView\\\\ProjectViewModelTest\\:\\:testDefaults\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Reporting/ProjectView/ProjectViewModelTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Reporting\\\\ProjectView\\\\ProjectViewModelTest\\:\\:testSetterGetter\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Reporting/ProjectView/ProjectViewModelTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Reporting\\\\ProjectView\\\\ProjectViewQueryTest\\:\\:testDefaults\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\Reporting\\\\ProjectView\\\\ProjectViewQueryTest\\:\\:testDefaults\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -8457,11 +8432,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: TranslationsTest.php
|
path: TranslationsTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Offset 'target\\-language' does not exist on SimpleXMLElement\\|null\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: TranslationsTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Twig\\\\ContextTest\\:\\:getDefaultSettings\\(\\) return type has no value type specified in iterable type array\\.$#"
|
message: "#^Method App\\\\Tests\\\\Twig\\\\ContextTest\\:\\:getDefaultSettings\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -687,6 +687,10 @@
|
|||||||
<source>holiday</source>
|
<source>holiday</source>
|
||||||
<target>Urlaub</target>
|
<target>Urlaub</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="XE01Ar1" resname="parental">
|
||||||
|
<source>parental</source>
|
||||||
|
<target>Elternzeit</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="MQKiG33" resname="profile.preferences">
|
<trans-unit id="MQKiG33" resname="profile.preferences">
|
||||||
<source>profile.preferences</source>
|
<source>profile.preferences</source>
|
||||||
<target>Einstellungen</target>
|
<target>Einstellungen</target>
|
||||||
@@ -1589,6 +1593,10 @@
|
|||||||
<source>sickness</source>
|
<source>sickness</source>
|
||||||
<target>Krankheit</target>
|
<target>Krankheit</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Nw2x5hI" resname="sickness_child">
|
||||||
|
<source>sickness_child</source>
|
||||||
|
<target>Krankheit eines Kindes</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="2SmKENG" resname="other">
|
<trans-unit id="2SmKENG" resname="other">
|
||||||
<source>other</source>
|
<source>other</source>
|
||||||
<target>Andere</target>
|
<target>Andere</target>
|
||||||
|
|||||||
@@ -687,6 +687,10 @@
|
|||||||
<source>holiday</source>
|
<source>holiday</source>
|
||||||
<target>Holiday</target>
|
<target>Holiday</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="XE01Ar1" resname="parental">
|
||||||
|
<source>parental</source>
|
||||||
|
<target>Parental leave</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="MQKiG33" resname="profile.preferences">
|
<trans-unit id="MQKiG33" resname="profile.preferences">
|
||||||
<source>profile.preferences</source>
|
<source>profile.preferences</source>
|
||||||
<target>Preferences</target>
|
<target>Preferences</target>
|
||||||
@@ -1589,6 +1593,10 @@
|
|||||||
<source>sickness</source>
|
<source>sickness</source>
|
||||||
<target>Sickness</target>
|
<target>Sickness</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Nw2x5hI" resname="sickness_child">
|
||||||
|
<source>sickness_child</source>
|
||||||
|
<target>Sickness of a child</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="2SmKENG" resname="other">
|
<trans-unit id="2SmKENG" resname="other">
|
||||||
<source>other</source>
|
<source>other</source>
|
||||||
<target>Other</target>
|
<target>Other</target>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ByeEMxW" resname="You do not have enough overtime, remaining are {{ value }}.">
|
<trans-unit id="ByeEMxW" resname="You do not have enough overtime, remaining are {{ value }}.">
|
||||||
<source>You do not have enough overtime, remaining are {{ value }}.</source>
|
<source>You do not have enough overtime, remaining are {{ value }}.</source>
|
||||||
<target>Sie haben nicht genug Überstunden, es verbleiben {{ Wert }}.</target>
|
<target>Sie haben nicht genug Überstunden, es verbleiben {{ value }}.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="DiaaRa1" resname="You cannot have two absences of this type at one day.">
|
<trans-unit id="DiaaRa1" resname="You cannot have two absences of this type at one day.">
|
||||||
<source>You cannot have two absences of this type at one day.</source>
|
<source>You cannot have two absences of this type at one day.</source>
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c2B6.lW" resname="You can book a maximum of {{ value }} days at once.">
|
<trans-unit id="c2B6.lW" resname="You can book a maximum of {{ value }} days at once.">
|
||||||
<source>You can book a maximum of {{ value }} days at once.</source>
|
<source>You can book a maximum of {{ value }} days at once.</source>
|
||||||
<target>Sie können maximal {{ Wert }} Tage auf einmal buchen.</target>
|
<target>Sie können maximal {{ value }} Tage auf einmal buchen.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
Reference in New Issue
Block a user