Release 2.0.35 (#4302)

This commit is contained in:
Kevin Papst
2023-09-23 18:15:22 +02:00
committed by GitHub
parent de419350b2
commit 7a5b12762a
122 changed files with 1555 additions and 2054 deletions

View File

@@ -16,10 +16,10 @@
# For MySQL that would be "serverVersion=5.7" as in: # For MySQL that would be "serverVersion=5.7" as in:
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7 # DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
# #
# For MariaDB it would be "serverVersion=mariadb-10.5.8": # For MariaDB it would be "serverVersion=10.5.8-MariaDB":
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=mariadb-10.5.8 # DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
# #
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7 DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
#================================================================================ #================================================================================
# The full documentation can be found at https://www.kimai.org/documentation/emails.html # The full documentation can be found at https://www.kimai.org/documentation/emails.html

View File

@@ -33,7 +33,7 @@ $fixer
'no_break_comment' => true, 'no_break_comment' => true,
'no_closing_tag' => true, 'no_closing_tag' => true,
'no_spaces_after_function_name' => true, 'no_spaces_after_function_name' => true,
'no_spaces_inside_parenthesis' => true, 'spaces_inside_parentheses' => ['space' => 'none'],
'no_trailing_whitespace' => true, 'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true, 'no_trailing_whitespace_in_comment' => true,
'single_blank_line_at_eof' => true, 'single_blank_line_at_eof' => true,
@@ -54,7 +54,7 @@ $fixer
'class_attributes_separation' => ['elements' => ['method' => 'one']], 'class_attributes_separation' => ['elements' => ['method' => 'one']],
'concat_space' => ['spacing' => 'one'], 'concat_space' => ['spacing' => 'one'],
'declare_equal_normalize' => true, 'declare_equal_normalize' => true,
'function_typehint_space' => true, 'type_declaration_spaces' => ['elements' => ['function', 'property']],
'include' => true, 'include' => true,
'lowercase_cast' => true, 'lowercase_cast' => true,
'lowercase_static_reference' => true, 'lowercase_static_reference' => true,
@@ -123,7 +123,7 @@ $fixer
'return_type_declaration' => true, 'return_type_declaration' => true,
'semicolon_after_instruction' => true, 'semicolon_after_instruction' => true,
'short_scalar_cast' => true, 'short_scalar_cast' => true,
'single_blank_line_before_namespace' => true, 'blank_lines_before_namespace' => ['min_line_breaks' => 2, 'max_line_breaks' => 2],
'single_line_comment_style' => [ 'single_line_comment_style' => [
'comment_types' => ['hash'], 'comment_types' => ['hash'],
], ],

View File

@@ -11,7 +11,7 @@ Perform EACH version specific task between your version and the new one, otherwi
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30) ## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
The `DATABASE_URL` in your environment settings ([.env](https://github.com/kimai/kimai/issues/4246) or in your [Docker](https://github.com/tobybatch/kimai2/issues/531) or webserver config) The `DATABASE_URL` in your environment settings ([.env](https://github.com/kimai/kimai/issues/4246) or in your [Docker](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7` (examples in `.env`). now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0) ## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)

View File

@@ -37,10 +37,6 @@
border-radius: var(--tblr-border-radius); border-radius: var(--tblr-border-radius);
} }
fieldset:empty {
display: none;
}
/* e.g. the work contract settings is user profile render rows with mb-3 and the last row doesn't need that */ /* e.g. the work contract settings is user profile render rows with mb-3 and the last row doesn't need that */
fieldset > .mb-3.row:last-child { fieldset > .mb-3.row:last-child {
margin-bottom: 0!important; margin-bottom: 0!important;

View File

@@ -16,3 +16,19 @@ fieldset.form-fieldset legend {
.dropdown-divider { .dropdown-divider {
--tblr-dropdown-divider-margin-y: .5rem; --tblr-dropdown-divider-margin-y: .5rem;
} }
/*
fixes translucent scroll border, e.g. in timesheet modal duration dropdown
https://github.com/tabler/tabler/issues/1606
*/
.dropdown-menu {
background-clip: border-box;
}
/*
hide empty fieldset
https://github.com/tabler/tabler/issues/1650
*/
fieldset:empty {
display: none;
}

View File

@@ -30,7 +30,7 @@
"doctrine/doctrine-bundle": "^2.7", "doctrine/doctrine-bundle": "^2.7",
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.8", "doctrine/orm": "^2.8",
"endroid/qr-code": "^4.4.9", "endroid/qr-code": "^4.8",
"erusev/parsedown": "^1.6", "erusev/parsedown": "^1.6",
"friendsofsymfony/rest-bundle": "^3.0", "friendsofsymfony/rest-bundle": "^3.0",
"gedmo/doctrine-extensions": "^3.6", "gedmo/doctrine-extensions": "^3.6",
@@ -44,8 +44,8 @@
"pagerfanta/pagerfanta": "^3.0", "pagerfanta/pagerfanta": "^3.0",
"phpoffice/phpspreadsheet": "^1.16", "phpoffice/phpspreadsheet": "^1.16",
"phpoffice/phpword": "^1.0", "phpoffice/phpword": "^1.0",
"psr/container": "^1.0||^2.0", "psr/container": "^2.0",
"psr/log": "^2.0||^3.0", "psr/log": "^3.0",
"scheb/2fa-backup-code": "^6.2", "scheb/2fa-backup-code": "^6.2",
"scheb/2fa-bundle": "^6.2", "scheb/2fa-bundle": "^6.2",
"scheb/2fa-totp": "^6.2", "scheb/2fa-totp": "^6.2",
@@ -68,7 +68,7 @@
"symfony/translation": "^6.0", "symfony/translation": "^6.0",
"symfony/twig-bundle": "^6.0", "symfony/twig-bundle": "^6.0",
"symfony/validator": "^6.0", "symfony/validator": "^6.0",
"symfony/webpack-encore-bundle": "^1.16", "symfony/webpack-encore-bundle": "^2.0",
"symfony/yaml": "^6.0", "symfony/yaml": "^6.0",
"twig/cssinliner-extra": "^3.0", "twig/cssinliner-extra": "^3.0",
"twig/extra-bundle": "^3.0", "twig/extra-bundle": "^3.0",
@@ -201,7 +201,7 @@
"symfony": { "symfony": {
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR", "id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
"allow-contrib": true, "allow-contrib": true,
"require": "6.2.*" "require": "6.3.*"
} }
} }
} }

1229
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -226,16 +226,6 @@ parameters:
count: 2 count: 2
path: src/Command/AbstractBundleInstallerCommand.php path: src/Command/AbstractBundleInstallerCommand.php
-
message: "#^Method App\\\\Command\\\\AbstractBundleInstallerCommand\\:\\:importMigrations\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/AbstractBundleInstallerCommand.php
-
message: "#^Method App\\\\Command\\\\AbstractBundleInstallerCommand\\:\\:installAssets\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/AbstractBundleInstallerCommand.php
- -
message: "#^Parameter \\#1 \\$directory of function chdir expects string, string\\|false given\\.$#" message: "#^Parameter \\#1 \\$directory of function chdir expects string, string\\|false given\\.$#"
count: 1 count: 1
@@ -256,11 +246,6 @@ parameters:
count: 1 count: 1
path: src/Command/AbstractResetCommand.php path: src/Command/AbstractResetCommand.php
-
message: "#^Method App\\\\Command\\\\AbstractRoleCommand\\:\\:executeRoleCommand\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/AbstractRoleCommand.php
- -
message: "#^Method App\\\\Command\\\\AbstractRoleCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$role with no type specified\\.$#" message: "#^Method App\\\\Command\\\\AbstractRoleCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$role with no type specified\\.$#"
count: 1 count: 1
@@ -331,11 +316,6 @@ parameters:
count: 2 count: 2
path: src/Command/DeactivateUserCommand.php path: src/Command/DeactivateUserCommand.php
-
message: "#^Method App\\\\Command\\\\DemoteUserCommand\\:\\:executeRoleCommand\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/DemoteUserCommand.php
- -
message: "#^Method App\\\\Command\\\\DemoteUserCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$role with no type specified\\.$#" message: "#^Method App\\\\Command\\\\DemoteUserCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$role with no type specified\\.$#"
count: 1 count: 1
@@ -351,11 +331,6 @@ parameters:
count: 2 count: 2
path: src/Command/ExportCreateCommand.php path: src/Command/ExportCreateCommand.php
-
message: "#^Method App\\\\Command\\\\ExportCreateCommand\\:\\:configure\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/ExportCreateCommand.php
- -
message: "#^Parameter \\#1 \\$customerIDs of method App\\\\Repository\\\\CustomerRepository\\:\\:findByIds\\(\\) expects array\\<int\\>, mixed given\\.$#" message: "#^Parameter \\#1 \\$customerIDs of method App\\\\Repository\\\\CustomerRepository\\:\\:findByIds\\(\\) expects array\\<int\\>, mixed given\\.$#"
count: 1 count: 1
@@ -561,11 +536,6 @@ parameters:
count: 1 count: 1
path: src/Command/PluginCommand.php path: src/Command/PluginCommand.php
-
message: "#^Method App\\\\Command\\\\PromoteUserCommand\\:\\:executeRoleCommand\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/PromoteUserCommand.php
- -
message: "#^Method App\\\\Command\\\\PromoteUserCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$role with no type specified\\.$#" message: "#^Method App\\\\Command\\\\PromoteUserCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$role with no type specified\\.$#"
count: 1 count: 1
@@ -586,11 +556,6 @@ parameters:
count: 4 count: 4
path: src/Command/ReloadCommand.php path: src/Command/ReloadCommand.php
-
message: "#^Method App\\\\Command\\\\ReloadCommand\\:\\:rebuildCaches\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/ReloadCommand.php
- -
message: "#^Parameter \\#1 \\$directory of function chdir expects string, string\\|false given\\.$#" message: "#^Parameter \\#1 \\$directory of function chdir expects string, string\\|false given\\.$#"
count: 1 count: 1
@@ -886,16 +851,6 @@ parameters:
count: 1 count: 1
path: src/Configuration/SystemConfiguration.php path: src/Configuration/SystemConfiguration.php
-
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:offsetSet\\(\\) has no return type specified\\.$#"
count: 1
path: src/Configuration/SystemConfiguration.php
-
message: "#^Method App\\\\Configuration\\\\SystemConfiguration\\:\\:offsetUnset\\(\\) has no return type specified\\.$#"
count: 1
path: src/Configuration/SystemConfiguration.php
- -
message: "#^Parameter \\#1 \\$key of method App\\\\Configuration\\\\SystemConfiguration\\:\\:set\\(\\) expects string, mixed given\\.$#" message: "#^Parameter \\#1 \\$key of method App\\\\Configuration\\\\SystemConfiguration\\:\\:set\\(\\) expects string, mixed given\\.$#"
count: 1 count: 1
@@ -941,26 +896,6 @@ parameters:
count: 1 count: 1
path: src/Controller/ActivityController.php path: src/Controller/ActivityController.php
-
message: "#^Method App\\\\Controller\\\\Auth\\\\SamlController\\:\\:assertionConsumerServiceAction\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/Auth/SamlController.php
-
message: "#^Method App\\\\Controller\\\\Auth\\\\SamlController\\:\\:loginAction\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/Auth/SamlController.php
-
message: "#^Method App\\\\Controller\\\\Auth\\\\SamlController\\:\\:logoutAction\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/Auth/SamlController.php
-
message: "#^Method App\\\\Controller\\\\Auth\\\\SamlController\\:\\:metadataAction\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/Auth/SamlController.php
- -
message: "#^Parameter \\#1 \\$returnTo of method OneLogin\\\\Saml2\\\\Auth\\:\\:login\\(\\) expects string\\|null, mixed given\\.$#" message: "#^Parameter \\#1 \\$returnTo of method OneLogin\\\\Saml2\\\\Auth\\:\\:login\\(\\) expects string\\|null, mixed given\\.$#"
count: 1 count: 1
@@ -1106,11 +1041,6 @@ parameters:
count: 1 count: 1
path: src/Controller/InvoiceController.php path: src/Controller/InvoiceController.php
-
message: "#^Method App\\\\Controller\\\\InvoiceController\\:\\:exportAction\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/InvoiceController.php
- -
message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#" message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
count: 1 count: 1
@@ -1121,11 +1051,6 @@ parameters:
count: 1 count: 1
path: src/Controller/InvoiceController.php path: src/Controller/InvoiceController.php
-
message: "#^Method App\\\\Controller\\\\PermissionController\\:\\:permissions\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/PermissionController.php
- -
message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(string\\|null\\)\\: mixed\\)\\|null, 'strtoupper' given\\.$#" message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(string\\|null\\)\\: mixed\\)\\|null, 'strtoupper' given\\.$#"
count: 1 count: 1
@@ -1186,11 +1111,6 @@ parameters:
count: 3 count: 3
path: src/Controller/QuickEntryController.php path: src/Controller/QuickEntryController.php
-
message: "#^Method App\\\\Controller\\\\QuickEntryController\\:\\:quickEntry\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/QuickEntryController.php
- -
message: "#^Method App\\\\Controller\\\\Reporting\\\\AbstractUserReportController\\:\\:getStatisticDataRaw\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Controller\\\\Reporting\\\\AbstractUserReportController\\:\\:getStatisticDataRaw\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -1331,16 +1251,6 @@ parameters:
count: 1 count: 1
path: src/Controller/Security/PasswordResetController.php path: src/Controller/Security/PasswordResetController.php
-
message: "#^Method App\\\\Controller\\\\Security\\\\SecurityController\\:\\:checkAction\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/Security/SecurityController.php
-
message: "#^Method App\\\\Controller\\\\Security\\\\SecurityController\\:\\:logoutAction\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/Security/SecurityController.php
- -
message: "#^Method App\\\\Controller\\\\Security\\\\SelfRegistrationController\\:\\:getTargetUrlFromSession\\(\\) should return string\\|null but returns mixed\\.$#" message: "#^Method App\\\\Controller\\\\Security\\\\SelfRegistrationController\\:\\:getTargetUrlFromSession\\(\\) should return string\\|null but returns mixed\\.$#"
count: 1 count: 1
@@ -1371,16 +1281,6 @@ parameters:
count: 1 count: 1
path: src/Controller/TimesheetAbstractController.php path: src/Controller/TimesheetAbstractController.php
-
message: "#^Method App\\\\Controller\\\\TimesheetAbstractController\\:\\:multiDelete\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/TimesheetAbstractController.php
-
message: "#^Method App\\\\Controller\\\\TimesheetAbstractController\\:\\:prepareQuery\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/TimesheetAbstractController.php
- -
message: "#^Parameter \\#1 \\$id of method App\\\\Export\\\\ServiceExport\\:\\:getTimesheetExporterById\\(\\) expects string, bool\\|float\\|int\\|string given\\.$#" message: "#^Parameter \\#1 \\$id of method App\\\\Export\\\\ServiceExport\\:\\:getTimesheetExporterById\\(\\) expects string, bool\\|float\\|int\\|string given\\.$#"
count: 1 count: 1
@@ -1391,11 +1291,6 @@ parameters:
count: 1 count: 1
path: src/Controller/TimesheetAbstractController.php path: src/Controller/TimesheetAbstractController.php
-
message: "#^Method App\\\\Controller\\\\TimesheetTeamController\\:\\:prepareQuery\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/TimesheetTeamController.php
- -
message: "#^PHPDoc tag @var for variable \\$teams contains generic class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection but does not specify its types\\: TKey, T$#" message: "#^PHPDoc tag @var for variable \\$teams contains generic class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection but does not specify its types\\: TKey, T$#"
count: 1 count: 1
@@ -1406,11 +1301,6 @@ parameters:
count: 1 count: 1
path: src/Controller/TimesheetTeamController.php path: src/Controller/TimesheetTeamController.php
-
message: "#^Method App\\\\Controller\\\\UserController\\:\\:exportAction\\(\\) has no return type specified\\.$#"
count: 1
path: src/Controller/UserController.php
- -
message: "#^Parameter \\#1 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\UserExporter\\:\\:export\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#" message: "#^Parameter \\#1 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\UserExporter\\:\\:export\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
count: 1 count: 1
@@ -1546,16 +1436,6 @@ parameters:
count: 1 count: 1
path: src/DependencyInjection/AppExtension.php path: src/DependencyInjection/AppExtension.php
-
message: "#^Method App\\\\DependencyInjection\\\\Configuration\\:\\:getProjectNode\\(\\) has no return type specified\\.$#"
count: 1
path: src/DependencyInjection/Configuration.php
-
message: "#^Method App\\\\Doctrine\\\\AbstractMigration\\:\\:abortIfPlatformNotSupported\\(\\) has no return type specified\\.$#"
count: 1
path: src/Doctrine/AbstractMigration.php
- -
message: "#^Method App\\\\Doctrine\\\\TimesheetSubscriber\\:\\:calculateFields\\(\\) has parameter \\$changes with no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Doctrine\\\\TimesheetSubscriber\\:\\:calculateFields\\(\\) has parameter \\$changes with no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -2036,11 +1916,6 @@ parameters:
count: 1 count: 1
path: src/Event/ThemeJavascriptTranslationsEvent.php path: src/Event/ThemeJavascriptTranslationsEvent.php
-
message: "#^Method App\\\\Event\\\\UserPreferenceDisplayEvent\\:\\:addPreference\\(\\) has no return type specified\\.$#"
count: 1
path: src/Event/UserPreferenceDisplayEvent.php
- -
message: "#^Parameter \\#1 \\$string of function strtolower expects string, string\\|null given\\.$#" message: "#^Parameter \\#1 \\$string of function strtolower expects string, string\\|null given\\.$#"
count: 2 count: 2
@@ -2451,11 +2326,6 @@ parameters:
count: 1 count: 1
path: src/Form/API/TimesheetApiEditForm.php path: src/Form/API/TimesheetApiEditForm.php
-
message: "#^Method App\\\\Form\\\\AbstractRateForm\\:\\:addFields\\(\\) has no return type specified\\.$#"
count: 1
path: src/Form/AbstractRateForm.php
- -
message: "#^Cannot access offset mixed on mixed\\.$#" message: "#^Cannot access offset mixed on mixed\\.$#"
count: 1 count: 1
@@ -3841,11 +3711,6 @@ parameters:
count: 1 count: 1
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
-
message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceItemDefaultHydrator\\:\\:setInvoiceModel\\(\\) has no return type specified\\.$#"
count: 1
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
- -
message: "#^Parameter \\#1 \\$date of method App\\\\Invoice\\\\InvoiceFormatter\\:\\:getFormattedDateTime\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#" message: "#^Parameter \\#1 \\$date of method App\\\\Invoice\\\\InvoiceFormatter\\:\\:getFormattedDateTime\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#"
count: 3 count: 3
@@ -4021,11 +3886,6 @@ parameters:
count: 1 count: 1
path: src/Invoice/InvoiceItemHydrator.php path: src/Invoice/InvoiceItemHydrator.php
-
message: "#^Method App\\\\Invoice\\\\InvoiceItemHydrator\\:\\:setInvoiceModel\\(\\) has no return type specified\\.$#"
count: 1
path: src/Invoice/InvoiceItemHydrator.php
- -
message: "#^Method App\\\\Invoice\\\\InvoiceModel\\:\\:itemToArray\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Invoice\\\\InvoiceModel\\:\\:itemToArray\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -4066,11 +3926,6 @@ parameters:
count: 1 count: 1
path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php
-
message: "#^Method App\\\\Invoice\\\\Renderer\\\\AbstractSpreadsheetRenderer\\:\\:addTemplateRows\\(\\) has no return type specified\\.$#"
count: 1
path: src/Invoice/Renderer/AbstractSpreadsheetRenderer.php
- -
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, mixed given\\.$#" message: "#^Parameter \\#1 \\$haystack of function stripos expects string, mixed given\\.$#"
count: 5 count: 5
@@ -4151,21 +4006,6 @@ parameters:
count: 1 count: 1
path: src/Invoice/ServiceInvoice.php path: src/Invoice/ServiceInvoice.php
-
message: "#^Method App\\\\Invoice\\\\ServiceInvoice\\:\\:changeInvoiceStatus\\(\\) has no return type specified\\.$#"
count: 1
path: src/Invoice/ServiceInvoice.php
-
message: "#^Method App\\\\Invoice\\\\ServiceInvoice\\:\\:deleteInvoice\\(\\) has no return type specified\\.$#"
count: 1
path: src/Invoice/ServiceInvoice.php
-
message: "#^Method App\\\\Invoice\\\\ServiceInvoice\\:\\:markEntriesAsExported\\(\\) has no return type specified\\.$#"
count: 1
path: src/Invoice/ServiceInvoice.php
- -
message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, int\\|null given\\.$#" message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, int\\|null given\\.$#"
count: 1 count: 1
@@ -4206,11 +4046,6 @@ parameters:
count: 2 count: 2
path: src/Kernel.php path: src/Kernel.php
-
message: "#^Method App\\\\Kernel\\:\\:build\\(\\) has no return type specified\\.$#"
count: 1
path: src/Kernel.php
- -
message: "#^Method App\\\\Kernel\\:\\:configureContainer\\(\\) has no return type specified\\.$#" message: "#^Method App\\\\Kernel\\:\\:configureContainer\\(\\) has no return type specified\\.$#"
count: 1 count: 1
@@ -6291,51 +6126,6 @@ parameters:
count: 1 count: 1
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractActiveUsers\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AbstractActiveUsers.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractAmountPeriod\\:\\:getRevenue\\(\\) has no return type specified\\.$#"
count: 1
path: src/Widget/Type/AbstractAmountPeriod.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractAmountPeriod\\:\\:getRevenue\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AbstractAmountPeriod.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractCounterDuration\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AbstractCounterDuration.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractCounterYear\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AbstractCounterYear.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractSimpleStatisticChart\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AbstractSimpleStatisticChart.php
-
message: "#^Strict comparison using \\!\\=\\= between null and App\\\\Entity\\\\User will always evaluate to true\\.$#"
count: 1
path: src/Widget/Type/AbstractSimpleStatisticChart.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractUserRevenuePeriod\\:\\:getRevenue\\(\\) has no return type specified\\.$#"
count: 1
path: src/Widget/Type/AbstractUserRevenuePeriod.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractUserRevenuePeriod\\:\\:getRevenue\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AbstractUserRevenuePeriod.php
- -
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractWidget\\:\\:getUser\\(\\) should return App\\\\Entity\\\\User but returns App\\\\Entity\\\\User\\|null\\.$#" message: "#^Method App\\\\Widget\\\\Type\\\\AbstractWidget\\:\\:getUser\\(\\) should return App\\\\Entity\\\\User but returns App\\\\Entity\\\\User\\|null\\.$#"
count: 1 count: 1
@@ -6356,106 +6146,6 @@ parameters:
count: 1 count: 1
path: src/Widget/Type/AbstractWidgetType.php path: src/Widget/Type/AbstractWidgetType.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\ActiveTimesheets\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/ActiveTimesheets.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\ActiveUsersMonth\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/ActiveUsersMonth.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\ActiveUsersToday\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/ActiveUsersToday.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\ActiveUsersTotal\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/ActiveUsersTotal.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\ActiveUsersWeek\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/ActiveUsersWeek.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\ActiveUsersYear\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/ActiveUsersYear.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AmountMonth\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AmountMonth.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AmountToday\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AmountToday.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AmountTotal\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AmountTotal.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AmountWeek\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AmountWeek.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\AmountYear\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/AmountYear.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\DailyWorkingTimeChart\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/DailyWorkingTimeChart.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\DurationMonth\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/DurationMonth.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\DurationToday\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/DurationToday.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\DurationTotal\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/DurationTotal.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\DurationWeek\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/DurationWeek.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\DurationYear\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/DurationYear.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\More\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/More.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\More\\:\\:setData\\(\\) has parameter \\$data with no type specified\\.$#"
count: 1
path: src/Widget/Type/More.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\PaginatedWorkingTimeChart\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/PaginatedWorkingTimeChart.php
- -
message: "#^Method App\\\\Widget\\\\Type\\\\PaginatedWorkingTimeChart\\:\\:getLastWeekInYear\\(\\) has parameter \\$year with no type specified\\.$#" message: "#^Method App\\\\Widget\\\\Type\\\\PaginatedWorkingTimeChart\\:\\:getLastWeekInYear\\(\\) has parameter \\$year with no type specified\\.$#"
count: 1 count: 1
@@ -6466,101 +6156,16 @@ parameters:
count: 1 count: 1
path: src/Widget/Type/PaginatedWorkingTimeChart.php path: src/Widget/Type/PaginatedWorkingTimeChart.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\TotalsActivity\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/TotalsActivity.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\TotalsCustomer\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/TotalsCustomer.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\TotalsProject\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/TotalsProject.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\TotalsUser\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/TotalsUser.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserAmountMonth\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserAmountMonth.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserAmountToday\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserAmountToday.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserAmountTotal\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserAmountTotal.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserAmountWeek\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserAmountWeek.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserAmountYear\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserAmountYear.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserDurationMonth\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserDurationMonth.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserDurationToday\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserDurationToday.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserDurationTotal\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserDurationTotal.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserDurationWeek\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserDurationWeek.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserDurationYear\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserDurationYear.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserTeamProjects\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserTeamProjects.php
- -
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\TeamLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\Team\\|int\\>, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#" message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\TeamLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\Team\\|int\\>, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#"
count: 1 count: 1
path: src/Widget/Type/UserTeamProjects.php path: src/Widget/Type/UserTeamProjects.php
-
message: "#^Method App\\\\Widget\\\\Type\\\\UserTeams\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/Type/UserTeams.php
- -
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\UserLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\User\\|int\\>, array\\<int, int\\|null\\> given\\.$#" message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\UserLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\User\\|int\\>, array\\<int, int\\|null\\> given\\.$#"
count: 1 count: 1
path: src/Widget/Type/UserTeams.php path: src/Widget/Type/UserTeams.php
-
message: "#^Method App\\\\Widget\\\\WidgetInterface\\:\\:getData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Widget/WidgetInterface.php
- -
message: "#^Property App\\\\Widget\\\\WidgetService\\:\\:\\$widgets type has no value type specified in iterable type array\\.$#" message: "#^Property App\\\\Widget\\\\WidgetService\\:\\:\\$widgets type has no value type specified in iterable type array\\.$#"
count: 1 count: 1

View File

@@ -15,7 +15,7 @@
<env name="APP_ENV" value="test" force="true"/> <env name="APP_ENV" value="test" force="true"/>
<env name="APP_DEBUG" value="0" force="true"/> <env name="APP_DEBUG" value="0" force="true"/>
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/> <env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?serverVersion=mariadb-10.7.3"/> <env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?serverVersion=10.5.8-MariaDB"/>
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/> <env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
<env name="MAILER_URL" value="null://null"/> <env name="MAILER_URL" value="null://null"/>
<env name="MAILER_FROM" value="kimai@example.com"/> <env name="MAILER_FROM" value="kimai@example.com"/>

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
"/build/app.9acfcfc8.js" "/build/app.9acfcfc8.js"
], ],
"css": [ "css": [
"/build/app.405a508c.css" "/build/app.fc41168c.css"
] ]
}, },
"export-pdf": { "export-pdf": {
@@ -64,7 +64,7 @@
"integrity": { "integrity": {
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV", "/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
"/build/app.9acfcfc8.js": "sha384-tzbF0uEmHghIo+D5IuKarLYkwB3AyrLMTEFqR5mqaXCP5VSOWx3t+ZfD81kd80T+", "/build/app.9acfcfc8.js": "sha384-tzbF0uEmHghIo+D5IuKarLYkwB3AyrLMTEFqR5mqaXCP5VSOWx3t+ZfD81kd80T+",
"/build/app.405a508c.css": "sha384-D8LqbrX2x0UHuxQasN8nli7f/tzVYlNeJleATqXW1o0Pjwycyo0t1jKRe1h1MeIV", "/build/app.fc41168c.css": "sha384-3Td9W7KogJoBiywzWOgpkPPNHEIT8/a/SkMZM5NgIRmuChj/3PeOKkOHVJQlhP2E",
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u", "/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg", "/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ", "/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ",

View File

@@ -1,5 +1,5 @@
{ {
"build/app.css": "/build/app.405a508c.css", "build/app.css": "/build/app.fc41168c.css",
"build/app.js": "/build/app.9acfcfc8.js", "build/app.js": "/build/app.9acfcfc8.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css", "build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.d367a32e.js", "build/export-pdf.js": "/build/export-pdf.d367a32e.js",

View File

@@ -148,7 +148,7 @@ abstract class AbstractBundleInstallerCommand extends Command
return Command::SUCCESS; return Command::SUCCESS;
} }
protected function installAssets(SymfonyStyle $io, OutputInterface $output) protected function installAssets(SymfonyStyle $io, OutputInterface $output): void
{ {
$command = $this->getApplication()->find('assets:install'); $command = $this->getApplication()->find('assets:install');
$cmdInput = new ArrayInput([]); $cmdInput = new ArrayInput([]);
@@ -160,12 +160,12 @@ abstract class AbstractBundleInstallerCommand extends Command
$io->writeln(''); $io->writeln('');
} }
protected function importMigrations(SymfonyStyle $io, OutputInterface $output) protected function importMigrations(SymfonyStyle $io, OutputInterface $output): void
{ {
$config = $this->getMigrationConfigFilename(); $config = $this->getMigrationConfigFilename();
if (null === $config) { if (null === $config) {
return false; return;
} }
if (!file_exists($config)) { if (!file_exists($config)) {

View File

@@ -56,5 +56,5 @@ abstract class AbstractRoleCommand extends Command
return Command::SUCCESS; return Command::SUCCESS;
} }
abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role); abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void;
} }

View File

@@ -36,7 +36,7 @@ final class DemoteUserCommand extends AbstractRoleCommand
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role) protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
{ {
$username = $user->getUserIdentifier(); $username = $user->getUserIdentifier();
if ($super) { if ($super) {

View File

@@ -46,10 +46,7 @@ final class ExportCreateCommand extends Command
parent::__construct(); parent::__construct();
} }
/** protected function configure(): void
* {@inheritdoc}
*/
protected function configure()
{ {
$this $this
->setDescription('Create exports') ->setDescription('Create exports')

View File

@@ -33,7 +33,7 @@ final class PromoteUserCommand extends AbstractRoleCommand
); );
} }
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role) protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
{ {
$username = $user->getUserIdentifier(); $username = $user->getUserIdentifier();
if ($super) { if ($super) {

View File

@@ -106,7 +106,7 @@ final class ReloadCommand extends Command
] ]
); );
return (int) $cacheResult; return $cacheResult;
} }
$io->success( $io->success(
@@ -116,7 +116,7 @@ final class ReloadCommand extends Command
return Command::SUCCESS; return Command::SUCCESS;
} }
private function rebuildCaches(string $environment, SymfonyStyle $io, InputInterface $input, OutputInterface $output) private function rebuildCaches(string $environment, SymfonyStyle $io, InputInterface $input, OutputInterface $output): int
{ {
$io->text('Rebuilding your cache, please be patient ...'); $io->text('Rebuilding your cache, please be patient ...');

View File

@@ -32,14 +32,11 @@ final class SystemConfiguration
/** /**
* Set an array item to a given value using "dot" notation. * Set an array item to a given value using "dot" notation.
*
* If no key is given to the method, the entire array will be replaced. * If no key is given to the method, the entire array will be replaced.
* *
* @param string $key * @internal
* @param mixed $value
* @return void
*/ */
private function set(string $key, $value): void public function set(string $key, mixed $value): void
{ {
if (\array_key_exists($key, $this->settings)) { if (\array_key_exists($key, $this->settings)) {
if (\is_bool($this->settings[$key])) { if (\is_bool($this->settings[$key])) {
@@ -125,31 +122,35 @@ final class SystemConfiguration
// ========== Array access methods ========== // ========== Array access methods ==========
/**
* @deprecated since 2.0.35
*/
public function offsetExists($offset): bool public function offsetExists($offset): bool
{ {
return $this->has($offset); return $this->has($offset);
} }
/**
* @deprecated since 2.0.35
*/
public function offsetGet($offset): mixed public function offsetGet($offset): mixed
{ {
return $this->find($offset); return $this->find($offset);
} }
/** /**
* @param mixed $offset * @deprecated since 2.0.35
* @param mixed $value
* @throws \BadMethodCallException
*/ */
public function offsetSet($offset, $value) public function offsetSet(mixed $offset, mixed $value): void
{ {
$this->set($offset, $value); $this->set($offset, $value);
} }
/** /**
* @param mixed $offset * @deprecated since 2.0.35
* @throws \BadMethodCallException * @throws \BadMethodCallException
*/ */
public function offsetUnset($offset) public function offsetUnset(mixed $offset): void
{ {
throw new \BadMethodCallException('SystemBundleConfiguration does not support offsetUnset()'); throw new \BadMethodCallException('SystemBundleConfiguration does not support offsetUnset()');
} }

View File

@@ -17,11 +17,11 @@ class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '2.0.34'; public const VERSION = '2.0.35';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 20034; public const VERSION_ID = 20035;
/** /**
* The software name * The software name
*/ */

View File

@@ -25,7 +25,7 @@ final class SamlController extends AbstractController
} }
#[Route(path: '/login', name: 'saml_login')] #[Route(path: '/login', name: 'saml_login')]
public function loginAction(Request $request) public function loginAction(Request $request): Response
{ {
if (!$this->samlConfiguration->isActivated()) { if (!$this->samlConfiguration->isActivated()) {
throw $this->createNotFoundException('SAML deactivated'); throw $this->createNotFoundException('SAML deactivated');
@@ -50,11 +50,19 @@ final class SamlController extends AbstractController
throw new \RuntimeException($error); throw new \RuntimeException($error);
} }
$this->authFactory->create()->login($session->get('_security.main.target_path')); // this does set headers and exit as $stay is not set to true
$url = $this->authFactory->create()->login($session->get('_security.main.target_path'));
if ($url === null) {
throw new \RuntimeException('SAML login failed');
}
// this line is not (yet) reached, as the previous call will exit
return $this->redirect($url);
} }
#[Route(path: '/metadata', name: 'saml_metadata')] #[Route(path: '/metadata', name: 'saml_metadata')]
public function metadataAction() public function metadataAction(): Response
{ {
if (!$this->samlConfiguration->isActivated()) { if (!$this->samlConfiguration->isActivated()) {
throw $this->createNotFoundException('SAML deactivated'); throw $this->createNotFoundException('SAML deactivated');
@@ -69,7 +77,7 @@ final class SamlController extends AbstractController
} }
#[Route(path: '/acs', name: 'saml_acs')] #[Route(path: '/acs', name: 'saml_acs')]
public function assertionConsumerServiceAction() public function assertionConsumerServiceAction(): Response
{ {
if (!$this->samlConfiguration->isActivated()) { if (!$this->samlConfiguration->isActivated()) {
throw $this->createNotFoundException('SAML deactivated'); throw $this->createNotFoundException('SAML deactivated');
@@ -79,7 +87,7 @@ final class SamlController extends AbstractController
} }
#[Route(path: '/logout', name: 'saml_logout')] #[Route(path: '/logout', name: 'saml_logout')]
public function logoutAction() public function logoutAction(): Response
{ {
if (!$this->samlConfiguration->isActivated()) { if (!$this->samlConfiguration->isActivated()) {
throw $this->createNotFoundException('SAML deactivated'); throw $this->createNotFoundException('SAML deactivated');

View File

@@ -387,7 +387,7 @@ final class InvoiceController extends AbstractController
#[Route(path: '/export', name: 'invoice_export', methods: ['GET'])] #[Route(path: '/export', name: 'invoice_export', methods: ['GET'])]
#[IsGranted('view_invoice')] #[IsGranted('view_invoice')]
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter) public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
{ {
$query = new InvoiceArchiveQuery(); $query = new InvoiceArchiveQuery();
$query->setCurrentUser($this->getUser()); $query->setCurrentUser($this->getUser());

View File

@@ -46,7 +46,7 @@ final class PermissionController extends AbstractController
#[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])] #[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])]
#[IsGranted('role_permissions')] #[IsGranted('role_permissions')]
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService) public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService): Response
{ {
$all = $this->roleRepository->findAll(); $all = $this->roleRepository->findAll();
$existing = []; $existing = [];

View File

@@ -18,6 +18,7 @@ use App\Repository\TimesheetRepository;
use App\Timesheet\TimesheetService; use App\Timesheet\TimesheetService;
use App\Utils\PageSetup; use App\Utils\PageSetup;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -33,7 +34,7 @@ final class QuickEntryController extends AbstractController
} }
#[Route(path: '/{begin}', name: 'quick_entry', methods: ['GET', 'POST'])] #[Route(path: '/{begin}', name: 'quick_entry', methods: ['GET', 'POST'])]
public function quickEntry(Request $request, ?string $begin = null) public function quickEntry(Request $request, ?string $begin = null): Response
{ {
$factory = $this->getDateTimeFactory(); $factory = $this->getDateTimeFactory();

View File

@@ -49,13 +49,13 @@ final class SecurityController extends AbstractController
} }
#[Route(path: '/login_check', name: 'security_check', methods: ['POST'])] #[Route(path: '/login_check', name: 'security_check', methods: ['POST'])]
public function checkAction() public function checkAction(): Response
{ {
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.'); throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
} }
#[Route(path: '/logout', name: 'logout', methods: ['GET', 'POST'])] #[Route(path: '/logout', name: 'logout', methods: ['GET', 'POST'])]
public function logoutAction() public function logoutAction(): Response
{ {
throw new \RuntimeException('You must activate the logout in your security firewall configuration.'); throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
} }

View File

@@ -418,7 +418,7 @@ abstract class TimesheetAbstractController extends AbstractController
]); ]);
} }
protected function multiDelete(Request $request) protected function multiDelete(Request $request): Response
{ {
$form = $this->getMultiUpdateActionForm(); $form = $this->getMultiUpdateActionForm();
$form->handleRequest($request); $form->handleRequest($request);
@@ -446,7 +446,7 @@ abstract class TimesheetAbstractController extends AbstractController
return $this->redirectToRoute($this->getTimesheetRoute()); return $this->redirectToRoute($this->getTimesheetRoute());
} }
protected function prepareQuery(TimesheetQuery $query) protected function prepareQuery(TimesheetQuery $query): void
{ {
$query->setUser($this->getUser()); $query->setUser($this->getUser());
} }

View File

@@ -164,7 +164,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
return $this->multiDelete($request); return $this->multiDelete($request);
} }
protected function prepareQuery(TimesheetQuery $query) protected function prepareQuery(TimesheetQuery $query): void
{ {
$query->setCurrentUser($this->getUser()); $query->setCurrentUser($this->getUser());
} }

View File

@@ -193,7 +193,7 @@ final class UserController extends AbstractController
#[Route(path: '/export', name: 'user_export', methods: ['GET'])] #[Route(path: '/export', name: 'user_export', methods: ['GET'])]
#[IsGranted('view_user')] #[IsGranted('view_user')]
public function exportAction(Request $request, UserExporter $exporter) public function exportAction(Request $request, UserExporter $exporter): Response
{ {
$query = new UserQuery(); $query = new UserQuery();
$query->setCurrentUser($this->getUser()); $query->setCurrentUser($this->getUser());

View File

@@ -105,7 +105,7 @@ final class Configuration implements ConfigurationInterface
return $node; return $node;
} }
private function getProjectNode() private function getProjectNode(): ArrayNodeDefinition
{ {
$builder = new TreeBuilder('project'); $builder = new TreeBuilder('project');
/** @var ArrayNodeDefinition $node */ /** @var ArrayNodeDefinition $node */

View File

@@ -52,7 +52,7 @@ abstract class AbstractMigration extends BaseAbstractMigration
* *
* @throws Exception * @throws Exception
*/ */
protected function abortIfPlatformNotSupported() protected function abortIfPlatformNotSupported(): void
{ {
$platform = $this->connection->getDatabasePlatform(); $platform = $this->connection->getDatabasePlatform();
if (!($platform instanceof MySQLPlatform)) { if (!($platform instanceof MySQLPlatform)) {

View File

@@ -21,16 +21,16 @@ final class RevenueStatisticEvent extends Event
*/ */
private array $revenue = []; private array $revenue = [];
public function __construct(private ?\DateTime $begin, private ?\DateTime $end) public function __construct(private ?\DateTimeInterface $begin, private ?\DateTimeInterface $end)
{ {
} }
public function getBegin(): ?\DateTime public function getBegin(): ?\DateTimeInterface
{ {
return $this->begin; return $this->begin;
} }
public function getEnd(): ?\DateTime public function getEnd(): ?\DateTimeInterface
{ {
return $this->end; return $this->end;
} }

View File

@@ -37,7 +37,7 @@ final class UserPreferenceDisplayEvent extends Event
return $this->preferences; return $this->preferences;
} }
public function addPreference(UserPreference $preference) public function addPreference(UserPreference $preference): void
{ {
$this->preferences[] = $preference; $this->preferences[] = $preference;
} }

View File

@@ -19,17 +19,17 @@ final class UserRevenueStatisticEvent extends Event
{ {
private RevenueStatisticEvent $event; private RevenueStatisticEvent $event;
public function __construct(private User $user, ?\DateTime $begin, ?\DateTime $end) public function __construct(private User $user, ?\DateTimeInterface $begin, ?\DateTimeInterface $end)
{ {
$this->event = new RevenueStatisticEvent($begin, $end); $this->event = new RevenueStatisticEvent($begin, $end);
} }
public function getBegin(): ?\DateTime public function getBegin(): ?\DateTimeInterface
{ {
return $this->event->getBegin(); return $this->event->getBegin();
} }
public function getEnd(): ?\DateTime public function getEnd(): ?\DateTimeInterface
{ {
return $this->event->getEnd(); return $this->event->getEnd();
} }

View File

@@ -17,7 +17,7 @@ use Symfony\Component\Form\FormBuilderInterface;
abstract class AbstractRateForm extends AbstractType abstract class AbstractRateForm extends AbstractType
{ {
protected function addFields(FormBuilderInterface $builder, ?string $currency) protected function addFields(FormBuilderInterface $builder, ?string $currency): void
{ {
$builder $builder
->add('user', UserType::class, [ ->add('user', UserType::class, [

View File

@@ -113,8 +113,8 @@ class DateTimePickerType extends AbstractType
'example' => (new \DateTime())->format(BaseApiController::DATE_FORMAT_PHP), 'example' => (new \DateTime())->format(BaseApiController::DATE_FORMAT_PHP),
], ],
'input' => 'datetime', 'input' => 'datetime',
'model_timezone' => null, 'model_timezone' => date_default_timezone_get(),
'view_timezone' => null, 'view_timezone' => date_default_timezone_get(),
// Don't modify \DateTime classes by reference, we treat // Don't modify \DateTime classes by reference, we treat
// them like immutable value objects // them like immutable value objects
'by_reference' => false, 'by_reference' => false,

View File

@@ -17,7 +17,7 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
{ {
private InvoiceModel $model; private InvoiceModel $model;
public function setInvoiceModel(InvoiceModel $model) public function setInvoiceModel(InvoiceModel $model): void
{ {
$this->model = $model; $this->model = $model;
} }

View File

@@ -11,7 +11,7 @@ namespace App\Invoice;
interface InvoiceItemHydrator interface InvoiceItemHydrator
{ {
public function setInvoiceModel(InvoiceModel $model); public function setInvoiceModel(InvoiceModel $model): void;
public function hydrate(InvoiceItem $item): array; public function hydrate(InvoiceItem $item): array;
} }

View File

@@ -120,11 +120,9 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
} }
/** /**
* @param Worksheet $worksheet
* @param int $invoiceItemCount
* @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Exception
*/ */
protected function addTemplateRows(Worksheet $worksheet, int $invoiceItemCount) protected function addTemplateRows(Worksheet $worksheet, int $invoiceItemCount): void
{ {
$startRow = null; $startRow = null;
$rowCounter = 0; $rowCounter = 0;

View File

@@ -229,7 +229,7 @@ final class ServiceInvoice
return $filename; return $filename;
} }
public function changeInvoiceStatus(Invoice $invoice, string $status) public function changeInvoiceStatus(Invoice $invoice, string $status): void
{ {
switch ($status) { switch ($status) {
case Invoice::STATUS_NEW: case Invoice::STATUS_NEW:
@@ -256,7 +256,6 @@ final class ServiceInvoice
} }
/** /**
* @param InvoiceQuery $query
* @return ExportableItem[] * @return ExportableItem[]
*/ */
public function getInvoiceItems(InvoiceQuery $query): array public function getInvoiceItems(InvoiceQuery $query): array
@@ -273,7 +272,7 @@ final class ServiceInvoice
/** /**
* @param ExportableItem[] $entries * @param ExportableItem[] $entries
*/ */
private function markEntriesAsExported(array $entries) private function markEntriesAsExported(array $entries): void
{ {
foreach ($this->getInvoiceItemRepositories() as $repository) { foreach ($this->getInvoiceItemRepositories() as $repository) {
$repository->setExported($entries); $repository->setExported($entries);
@@ -309,9 +308,6 @@ final class ServiceInvoice
} }
/** /**
* @param InvoiceModel $model
* @param EventDispatcherInterface $dispatcher
* @return Invoice
* @throws \Exception * @throws \Exception
*/ */
public function createInvoice(InvoiceModel $model, EventDispatcherInterface $dispatcher): Invoice public function createInvoice(InvoiceModel $model, EventDispatcherInterface $dispatcher): Invoice
@@ -362,7 +358,7 @@ final class ServiceInvoice
); );
} }
public function deleteInvoice(Invoice $invoice, EventDispatcherInterface $dispatcher) public function deleteInvoice(Invoice $invoice, EventDispatcherInterface $dispatcher): void
{ {
$invoiceDirectory = $this->getInvoicesDirectory(); $invoiceDirectory = $this->getInvoicesDirectory();
@@ -377,8 +373,6 @@ final class ServiceInvoice
} }
/** /**
* @param InvoiceQuery $query
* @return InvoiceModel
* @throws \Exception * @throws \Exception
*/ */
public function createModel(InvoiceQuery $query): InvoiceModel public function createModel(InvoiceQuery $query): InvoiceModel
@@ -485,7 +479,6 @@ final class ServiceInvoice
} }
/** /**
* @param InvoiceQuery $query
* @return InvoiceModel[] * @return InvoiceModel[]
* @throws \Exception * @throws \Exception
*/ */

View File

@@ -72,7 +72,7 @@ class Kernel extends BaseKernel
return $this->getProjectDir() . '/var/log'; return $this->getProjectDir() . '/var/log';
} }
protected function build(ContainerBuilder $container) protected function build(ContainerBuilder $container): void
{ {
$container->registerForAutoconfiguration(TimesheetCalculator::class)->addTag(self::TAG_TIMESHEET_CALCULATOR); $container->registerForAutoconfiguration(TimesheetCalculator::class)->addTag(self::TAG_TIMESHEET_CALCULATOR);
$container->registerForAutoconfiguration(ExportRendererInterface::class)->addTag(self::TAG_EXPORT_RENDERER); $container->registerForAutoconfiguration(ExportRendererInterface::class)->addTag(self::TAG_EXPORT_RENDERER);

View File

@@ -42,10 +42,15 @@ class TimesheetRepository extends EntityRepository
{ {
use RepositorySearchTrait; use RepositorySearchTrait;
/** @deprecated since 2.0.35 */
public const STATS_QUERY_DURATION = 'duration'; public const STATS_QUERY_DURATION = 'duration';
/** @deprecated since 2.0.35 */
public const STATS_QUERY_RATE = 'rate'; public const STATS_QUERY_RATE = 'rate';
/** @deprecated since 2.0.35 */
public const STATS_QUERY_USER = 'users'; public const STATS_QUERY_USER = 'users';
/** @deprecated since 2.0.35 */
public const STATS_QUERY_AMOUNT = 'amount'; public const STATS_QUERY_AMOUNT = 'amount';
/** @deprecated since 2.0.35 */
public const STATS_QUERY_ACTIVE = 'active'; public const STATS_QUERY_ACTIVE = 'active';
/** /**
@@ -152,42 +157,44 @@ class TimesheetRepository extends EntityRepository
/** /**
* @param self::STATS_QUERY_* $type * @param self::STATS_QUERY_* $type
* @param DateTime|null $begin
* @param DateTime|null $end
* @param User|null $user
* @return int|mixed * @return int|mixed
* @throws \Doctrine\ORM\NonUniqueResultException * @deprecated since 2.0.35
*/ */
public function getStatistic(string $type, ?DateTime $begin, ?DateTime $end, ?User $user, ?bool $billable = null): mixed public function getStatistic(string $type, ?\DateTimeInterface $begin, ?\DateTimeInterface $end, ?User $user, ?bool $billable = null): mixed
{ {
@trigger_error('Repository method getStatistic() is deprecated, use explicit methods instead', E_USER_DEPRECATED);
switch ($type) { switch ($type) {
case self::STATS_QUERY_ACTIVE: case 'active':
return \count($this->getActiveEntries($user)); return $this->countActiveEntries($user);
case self::STATS_QUERY_DURATION: case 'duration':
$what = 'COALESCE(SUM(t.duration), 0)'; return $this->getDurationForTimeRange($begin, $end, $user, $billable);
break; case 'rate':
case self::STATS_QUERY_RATE:
return $this->getRevenue($begin, $end, $user); return $this->getRevenue($begin, $end, $user);
case self::STATS_QUERY_USER: case 'users':
$what = 'COUNT(DISTINCT(t.user))'; return $this->countActiveUsers($begin, $end, $billable);
break; case 'amount':
case self::STATS_QUERY_AMOUNT: return $this->queryTimeRange('COUNT(t.id)', $begin, $end, $user, $billable);
$what = 'COUNT(t.id)';
break;
default:
throw new InvalidArgumentException('Invalid query type: ' . $type);
} }
return $this->queryTimeRange($what, $begin, $end, $user, $billable); throw new InvalidArgumentException('Invalid query type: ' . $type); // @phpstan-ignore-line
}
public function getDurationForTimeRange(?\DateTimeInterface $begin, ?\DateTimeInterface $end, ?User $user, ?bool $billable = null): int
{
$tmp = $this->queryTimeRange('COALESCE(SUM(t.duration), 0)', $begin, $end, $user, $billable);
if (!is_numeric($tmp)) {
return 0;
}
return (int) $tmp;
} }
/** /**
* @param DateTime|null $begin
* @param DateTime|null $end
* @param User|null $user
* @return array<Revenue> * @return array<Revenue>
*/ */
public function getRevenue(?DateTime $begin, ?DateTime $end, ?User $user): array public function getRevenue(?\DateTimeInterface $begin, ?\DateTimeInterface $end, ?User $user): array
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
@@ -198,9 +205,8 @@ class TimesheetRepository extends EntityRepository
->leftJoin('t.project', 'p') ->leftJoin('t.project', 'p')
->leftJoin('p.customer', 'c') ->leftJoin('p.customer', 'c')
->groupBy('c.currency') ->groupBy('c.currency')
; ->andWhere($qb->expr()->eq('t.billable', ':billable'))
->setParameter('billable', true);
// TODO only billable?
if ($begin !== null) { if ($begin !== null) {
$qb->andWhere($qb->expr()->between('t.begin', ':from', ':to')) $qb->andWhere($qb->expr()->between('t.begin', ':from', ':to'))
@@ -223,14 +229,10 @@ class TimesheetRepository extends EntityRepository
/** /**
* @param string|string[] $select * @param string|string[] $select
* @param DateTime|null $begin
* @param DateTime|null $end
* @param User|null $user
* @param bool|null $billable
* @return int|mixed * @return int|mixed
* @throws \Doctrine\ORM\NonUniqueResultException * @throws \Doctrine\ORM\NonUniqueResultException
*/ */
protected function queryTimeRange(string|array $select, ?DateTime $begin, ?DateTime $end, ?User $user, ?bool $billable = null): mixed private function queryTimeRange(string|array $select, ?\DateTimeInterface $begin, ?\DateTimeInterface $end, ?User $user, ?bool $billable = null): mixed
{ {
$selects = $select; $selects = $select;
if (!\is_array($select)) { if (!\is_array($select)) {
@@ -349,6 +351,34 @@ class TimesheetRepository extends EntityRepository
return $this->getHydratedResultsByQuery($qb, false); return $this->getHydratedResultsByQuery($qb, false);
} }
public function countActiveEntries(?User $user = null): int
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('COUNT(t)')
->from(Timesheet::class, 't')
->andWhere($qb->expr()->isNull('t.end'))
->orderBy('t.begin', 'DESC');
if (null !== $user) {
$qb->andWhere('t.user = :user');
$qb->setParameter('user', $user);
}
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function countActiveUsers(?\DateTimeInterface $begin, ?\DateTimeInterface $end, ?bool $billable = null): int
{
$tmp = $this->queryTimeRange('COUNT(DISTINCT(t.user))', $begin, $end, null, $billable);
if (!is_numeric($tmp)) {
return 0;
}
return (int) $tmp;
}
/** /**
* This method causes me some headaches ... * This method causes me some headaches ...
* *
@@ -411,7 +441,7 @@ class TimesheetRepository extends EntityRepository
return new Pagination($this->getPaginatorForQuery($query), $query); return new Pagination($this->getPaginatorForQuery($query), $query);
} }
protected function getPaginatorForQuery(TimesheetQuery $query): PaginatorInterface private function getPaginatorForQuery(TimesheetQuery $query): PaginatorInterface
{ {
$qb = $this->getQueryBuilderForQuery($query); $qb = $this->getQueryBuilderForQuery($query);
$qb $qb

View File

@@ -134,11 +134,9 @@ final class SamlProvider
throw new \RuntimeException('Missing user attribute: ' . $key); throw new \RuntimeException('Missing user attribute: ' . $key);
} }
if (!\array_key_exists(0, $attributes[$key])) { if (\is_array($attributes[$key]) && isset($attributes[$key][0])) {
throw new \RuntimeException('Missing user attribute value: ' . $key);
}
$results[] = $attributes[$key][0]; $results[] = $attributes[$key][0];
}
} else { } else {
$results[] = $part; $results[] = $part;
} }

View File

@@ -21,6 +21,8 @@ use App\Repository\TimesheetRepository;
*/ */
final class FavoriteRecordService final class FavoriteRecordService
{ {
public const MAX_FAVORITES = 5;
public function __construct(private TimesheetRepository $repository, private BookmarkRepository $bookmarkRepository) public function __construct(private TimesheetRepository $repository, private BookmarkRepository $bookmarkRepository)
{ {
} }
@@ -35,7 +37,7 @@ final class FavoriteRecordService
/** @var array<int> $favIds */ /** @var array<int> $favIds */
$favIds = $this->getBookmark($user)->getContent(); $favIds = $this->getBookmark($user)->getContent();
$recentIds = []; $recentIds = [];
if (\count($favIds) < 5) { if (\count($favIds) < $limit) {
$recentIds = $this->repository->getRecentActivityIds($user, null, $limit); $recentIds = $this->repository->getRecentActivityIds($user, null, $limit);
} }
/** @var array<int> $ids */ /** @var array<int> $ids */
@@ -98,7 +100,7 @@ final class FavoriteRecordService
return; return;
} }
if (\count($ids) >= 5) { if (\count($ids) >= self::MAX_FAVORITES) {
array_pop($ids); // remove the last element and make space for a new id array_pop($ids); // remove the last element and make space for a new id
} }
array_unshift($ids, $timesheet->getId()); array_unshift($ids, $timesheet->getId());

View File

@@ -9,10 +9,12 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository; abstract class AbstractActiveUsers extends AbstractWidgetType
abstract class AbstractActiveUsers extends AbstractSimpleStatisticChart
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -20,14 +22,6 @@ abstract class AbstractActiveUsers extends AbstractSimpleStatisticChart
], parent::getOptions($options)); ], parent::getOptions($options));
} }
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(false);
$this->setQuery(TimesheetRepository::STATS_QUERY_USER);
return parent::getData($options);
}
public function getTitle(): string public function getTitle(): string
{ {
return 'stats.' . lcfirst($this->getId()); return 'stats.' . lcfirst($this->getId());

View File

@@ -29,6 +29,10 @@ abstract class AbstractAmountPeriod extends AbstractWidget
return 'widget/widget-counter-money.html.twig'; return 'widget/widget-counter-money.html.twig';
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -36,19 +40,12 @@ abstract class AbstractAmountPeriod extends AbstractWidget
], parent::getOptions($options)); ], parent::getOptions($options));
} }
protected function getRevenue(?string $begin, ?string $end, array $options = []) /**
* @param array<string, string|bool|int|null> $options
* @return array<string, float>
*/
protected function getRevenue(?\DateTimeInterface $begin, ?\DateTimeInterface $end, array $options = []): array
{ {
$user = $this->getUser();
$timezone = new \DateTimeZone($user->getTimezone());
if ($begin !== null) {
$begin = new \DateTime($begin, $timezone);
}
if ($end !== null) {
$end = new \DateTime($end, $timezone);
}
$data = $this->repository->getRevenue($begin, $end, null); $data = $this->repository->getRevenue($begin, $end, null);
$event = new RevenueStatisticEvent($begin, $end); $event = new RevenueStatisticEvent($begin, $end);

View File

@@ -9,10 +9,12 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository; abstract class AbstractCounterDuration extends AbstractWidgetType
abstract class AbstractCounterDuration extends AbstractSimpleStatisticChart
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -20,13 +22,6 @@ abstract class AbstractCounterDuration extends AbstractSimpleStatisticChart
], parent::getOptions($options)); ], parent::getOptions($options));
} }
public function getData(array $options = []): mixed
{
$this->setQuery(TimesheetRepository::STATS_QUERY_DURATION);
return parent::getData($options);
}
public function getTitle(): string public function getTitle(): string
{ {
return 'stats.' . lcfirst($this->getId()); return 'stats.' . lcfirst($this->getId());

View File

@@ -10,34 +10,39 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Configuration\SystemConfiguration; use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory; use App\Timesheet\DateTimeFactory;
abstract class AbstractCounterYear extends AbstractSimpleStatisticChart abstract class AbstractCounterYear extends AbstractWidgetType
{ {
private bool $isFinancialYear = false; private bool $isFinancialYear = false;
public function __construct(TimesheetRepository $repository, private SystemConfiguration $systemConfiguration) public function __construct(private SystemConfiguration $systemConfiguration)
{ {
parent::__construct($repository);
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setBegin('01 january this year 00:00:00'); $begin = $this->createDate('01 january this year 00:00:00');
$this->setEnd('31 december this year 23:59:59'); $end = $this->createDate('31 december this year 23:59:59');
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) { if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$factory = new DateTimeFactory($this->getTimezone()); $factory = new DateTimeFactory($this->getTimezone());
$begin = $factory->createStartOfFinancialYear($financialYear); $begin = $factory->createStartOfFinancialYear($financialYear);
$this->setBegin($begin); $end = $factory->createEndOfFinancialYear($begin);
$this->setEnd($factory->createEndOfFinancialYear($begin));
$this->isFinancialYear = true; $this->isFinancialYear = true;
} }
return parent::getData($options); return $this->getYearData($begin, $end, $options);
} }
/**
* @param array<string, string|bool|int|null> $options
*/
abstract protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed;
abstract protected function getFinancialYearTitle(): string; abstract protected function getFinancialYearTitle(): string;
public function getTitle(): string public function getTitle(): string

View File

@@ -1,139 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
abstract class AbstractSimpleStatisticChart extends AbstractWidgetType
{
/**
* @var TimesheetRepository::STATS_QUERY_*
*/
private string $query;
/**
* @var string|\DateTime|null
*/
private $begin;
/**
* @var string|\DateTime|null
*/
private $end;
private bool $queryWithUser = false;
public function __construct(private TimesheetRepository $repository)
{
}
public function getWidth(): int
{
return WidgetInterface::WIDTH_SMALL;
}
public function getHeight(): int
{
return WidgetInterface::HEIGHT_SMALL;
}
/**
* @param TimesheetRepository::STATS_QUERY_* $query
* @return void
*/
public function setQuery(string $query): void
{
$this->query = $query;
}
public function setBegin(null|string|\DateTime $begin): self
{
$this->begin = $begin;
return $this;
}
public function getBegin(): ?\DateTime
{
if ($this->begin === null) {
return null;
}
if ($this->begin instanceof \DateTime) {
return $this->begin;
}
return new \DateTime($this->begin, $this->getTimezone());
}
public function setEnd(null|string|\DateTime $end): self
{
$this->end = $end;
return $this;
}
public function getEnd(): ?\DateTime
{
if ($this->end === null) {
return null;
}
if ($this->end instanceof \DateTime) {
return $this->end;
}
return new \DateTime($this->end, $this->getTimezone());
}
public function setQueryWithUser(bool $queryWithUser): self
{
$this->queryWithUser = $queryWithUser;
return $this;
}
public function getTimezone(): \DateTimeZone
{
$timezone = date_default_timezone_get();
if (null !== $this->getUser()) {
$timezone = $this->getUser()->getTimezone();
}
return new \DateTimeZone($timezone);
}
/**
* @param array $options
* @return mixed|null
* @throws WidgetException
*/
public function getData(array $options = []): mixed
{
try {
$user = null;
if (true === $this->queryWithUser) {
$user = $this->getUser();
}
return $this->repository->getStatistic($this->query, $this->getBegin(), $this->getEnd(), $user);
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
public function getTemplateName(): string
{
$name = (new \ReflectionClass($this))->getShortName();
return sprintf('widget/widget-%s.html.twig', strtolower($name));
}
}

View File

@@ -34,6 +34,10 @@ abstract class AbstractUserRevenuePeriod extends AbstractWidget
return ['view_rate_own_timesheet']; return ['view_rate_own_timesheet'];
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -41,18 +45,13 @@ abstract class AbstractUserRevenuePeriod extends AbstractWidget
], parent::getOptions($options)); ], parent::getOptions($options));
} }
protected function getRevenue(?string $begin, ?string $end, array $options = []) /**
* @param array<string, string|bool|int|null> $options
* @return array<string, float>
*/
protected function getRevenue(?\DateTimeInterface $begin, ?\DateTimeInterface $end, array $options = []): array
{ {
$user = $this->getUser(); $user = $this->getUser();
$timezone = new \DateTimeZone($user->getTimezone());
if ($begin !== null) {
$begin = new \DateTime($begin, $timezone);
}
if ($end !== null) {
$end = new \DateTime($end, $timezone);
}
$data = $this->repository->getRevenue($begin, $end, $user); $data = $this->repository->getRevenue($begin, $end, $user);

View File

@@ -67,6 +67,10 @@ abstract class AbstractWidget implements WidgetInterface
$this->options[$name] = $value; $this->options[$name] = $value;
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge($this->options, $options); return array_merge($this->options, $options);
@@ -76,4 +80,56 @@ abstract class AbstractWidget implements WidgetInterface
{ {
return false; return false;
} }
protected function createDate(string $date): \DateTime
{
return new \DateTime($date, $this->getTimezone());
}
protected function createMonthStartDate(): \DateTime
{
return $this->createDate('first day of this month 00:00:00');
}
protected function createMonthEndDate(): \DateTime
{
return $this->createDate('last day of this month 23:59:59');
}
protected function createWeekStartDate(): \DateTime
{
return $this->createDate('monday this week 00:00:00');
}
protected function createWeekEndDate(): \DateTime
{
return $this->createDate('sunday this week 23:59:59');
}
protected function createTodayStartDate(): \DateTime
{
return $this->createDate('00:00:00');
}
protected function createTodayEndDate(): \DateTime
{
return $this->createDate('23:59:59');
}
public function getTimezone(): \DateTimeZone
{
$timezone = date_default_timezone_get();
if (null !== $this->user) {
$timezone = $this->user->getTimezone();
}
return new \DateTimeZone($timezone);
}
public function getTemplateName(): string
{
$name = (new \ReflectionClass($this))->getShortName();
return sprintf('widget/widget-%s.html.twig', strtolower($name));
}
} }

View File

@@ -9,48 +9,18 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Widget\WidgetInterface;
abstract class AbstractWidgetType extends AbstractWidget abstract class AbstractWidgetType extends AbstractWidget
{ {
private ?string $id = null; private ?string $id = null;
private string $title = ''; private string $title = '';
private int $height = WidgetInterface::HEIGHT_SMALL;
private int $width = WidgetInterface::WIDTH_SMALL;
/** /**
* @var array<string> * @var array<string>
*/ */
private array $permissions = []; private array $permissions = [];
public function getHeight(): int public function setId(string $id): void
{
return $this->height;
}
public function setHeight(int $height): AbstractWidgetType
{
$this->height = $height;
return $this;
}
public function getWidth(): int
{
return $this->width;
}
public function setWidth(int $width): AbstractWidgetType
{
$this->width = $width;
return $this;
}
public function setId(string $id): self
{ {
$this->id = $id; $this->id = $id;
return $this;
} }
public function getId(): string public function getId(): string
@@ -62,11 +32,9 @@ abstract class AbstractWidgetType extends AbstractWidget
return (new \ReflectionClass($this))->getShortName(); return (new \ReflectionClass($this))->getShortName();
} }
public function setTitle(string $title): self public function setTitle(string $title): void
{ {
$this->title = $title; $this->title = $title;
return $this;
} }
public function getTitle(): string public function getTitle(): string
@@ -74,13 +42,11 @@ abstract class AbstractWidgetType extends AbstractWidget
return $this->title; return $this->title;
} }
public function setOptions(array $options): self public function setOptions(array $options): void
{ {
foreach ($options as $key => $value) { foreach ($options as $key => $value) {
$this->setOption($key, $value); $this->setOption($key, $value);
} }
return $this;
} }
public function getPermissions(): array public function getPermissions(): array
@@ -88,10 +54,8 @@ abstract class AbstractWidgetType extends AbstractWidget
return $this->permissions; return $this->permissions;
} }
public function setPermissions(array $permissions): AbstractWidgetType public function setPermissions(array $permissions): void
{ {
$this->permissions = $permissions; $this->permissions = $permissions;
return $this;
} }
} }

View File

@@ -10,10 +10,19 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class ActiveTimesheets extends AbstractSimpleStatisticChart final class ActiveTimesheets extends AbstractWidgetType
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TOTAL, 'icon' => 'duration'], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TOTAL, 'icon' => 'duration'], parent::getOptions($options));
@@ -34,12 +43,18 @@ final class ActiveTimesheets extends AbstractSimpleStatisticChart
return 'stats.activeRecordings'; return 'stats.activeRecordings';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(false); try {
$this->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE); return $this->repository->countActiveEntries();
} catch (\Exception $ex) {
return parent::getData($options); throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
public function getTemplateName(): string public function getTemplateName(): string

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class ActiveUsersMonth extends AbstractActiveUsers final class ActiveUsersMonth extends AbstractActiveUsers
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
@@ -23,11 +33,17 @@ final class ActiveUsersMonth extends AbstractActiveUsers
return 'activeUsersMonth'; return 'activeUsersMonth';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setBegin('first day of this month 00:00:00'); try {
$this->setEnd('last day of this month 23:59:59'); return $this->repository->countActiveUsers($this->createMonthStartDate(), $this->createMonthEndDate(), null);
} catch (\Exception $ex) {
return parent::getData($options); throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class ActiveUsersToday extends AbstractActiveUsers final class ActiveUsersToday extends AbstractActiveUsers
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
@@ -23,11 +33,17 @@ final class ActiveUsersToday extends AbstractActiveUsers
return 'activeUsersToday'; return 'activeUsersToday';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setBegin('00:00:00'); try {
$this->setEnd('23:59:59'); return $this->repository->countActiveUsers($this->createTodayStartDate(), $this->createTodayEndDate(), null);
} catch (\Exception $ex) {
return parent::getData($options); throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class ActiveUsersTotal extends AbstractActiveUsers final class ActiveUsersTotal extends AbstractActiveUsers
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
@@ -23,8 +33,17 @@ final class ActiveUsersTotal extends AbstractActiveUsers
return 'activeUsersTotal'; return 'activeUsersTotal';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return parent::getData($options); try {
return $this->repository->countActiveUsers(null, null, null);
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class ActiveUsersWeek extends AbstractActiveUsers final class ActiveUsersWeek extends AbstractActiveUsers
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
@@ -23,11 +33,17 @@ final class ActiveUsersWeek extends AbstractActiveUsers
return 'activeUsersWeek'; return 'activeUsersWeek';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setBegin('monday this week 00:00:00'); try {
$this->setEnd('sunday this week 23:59:59'); return $this->repository->countActiveUsers($this->createWeekStartDate(), $this->createWeekEndDate(), null);
} catch (\Exception $ex) {
return parent::getData($options); throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,11 +9,22 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class ActiveUsersYear extends AbstractCounterYear final class ActiveUsersYear extends AbstractCounterYear
{ {
public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
{
parent::__construct($systemConfiguration);
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -22,12 +33,18 @@ final class ActiveUsersYear extends AbstractCounterYear
], parent::getOptions($options)); ], parent::getOptions($options));
} }
public function getData(array $options = []): mixed /**
* @param array<string, string|bool|int|null> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
{ {
$this->setQuery(TimesheetRepository::STATS_QUERY_USER); try {
$this->setQueryWithUser(false); return $this->repository->countActiveUsers($begin, $end, null);
} catch (\Exception $ex) {
return parent::getData($options); throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
protected function getFinancialYearTitle(): string protected function getFinancialYearTitle(): string

View File

@@ -13,6 +13,10 @@ use App\Widget\WidgetInterface;
final class AmountMonth extends AbstractAmountPeriod final class AmountMonth extends AbstractAmountPeriod
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
@@ -23,9 +27,12 @@ final class AmountMonth extends AbstractAmountPeriod
return 'AmountMonth'; return 'AmountMonth';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return $this->getRevenue('first day of this month 00:00:00', 'last day of this month 23:59:59', $options); return $this->getRevenue($this->createMonthStartDate(), $this->createMonthEndDate(), $options);
} }
public function getPermissions(): array public function getPermissions(): array

View File

@@ -13,6 +13,10 @@ use App\Widget\WidgetInterface;
final class AmountToday extends AbstractAmountPeriod final class AmountToday extends AbstractAmountPeriod
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
@@ -23,9 +27,12 @@ final class AmountToday extends AbstractAmountPeriod
return 'AmountToday'; return 'AmountToday';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return $this->getRevenue('00:00:00', '23:59:59', $options); return $this->getRevenue($this->createTodayStartDate(), $this->createTodayEndDate(), $options);
} }
public function getPermissions(): array public function getPermissions(): array

View File

@@ -13,6 +13,10 @@ use App\Widget\WidgetInterface;
final class AmountTotal extends AbstractAmountPeriod final class AmountTotal extends AbstractAmountPeriod
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
@@ -28,6 +32,9 @@ final class AmountTotal extends AbstractAmountPeriod
return ['view_all_data']; return ['view_all_data'];
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return $this->getRevenue(null, null, $options); return $this->getRevenue(null, null, $options);

View File

@@ -13,6 +13,10 @@ use App\Widget\WidgetInterface;
final class AmountWeek extends AbstractAmountPeriod final class AmountWeek extends AbstractAmountPeriod
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
@@ -23,9 +27,12 @@ final class AmountWeek extends AbstractAmountPeriod
return 'AmountWeek'; return 'AmountWeek';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return $this->getRevenue('monday this week 00:00:00', 'sunday this week 23:59:59', $options); return $this->getRevenue($this->createWeekStartDate(), $this->createWeekEndDate(), $options);
} }
public function getPermissions(): array public function getPermissions(): array

View File

@@ -13,16 +13,21 @@ use App\Configuration\SystemConfiguration;
use App\Event\RevenueStatisticEvent; use App\Event\RevenueStatisticEvent;
use App\Model\Revenue; use App\Model\Revenue;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class AmountYear extends AbstractCounterYear final class AmountYear extends AbstractCounterYear
{ {
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher) public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher)
{ {
parent::__construct($repository, $systemConfiguration); parent::__construct($systemConfiguration);
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -31,21 +36,27 @@ final class AmountYear extends AbstractCounterYear
], parent::getOptions($options)); ], parent::getOptions($options));
} }
public function getData(array $options = []): mixed /**
* @param array<string, string|bool|int|null> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
{ {
$this->setQuery(TimesheetRepository::STATS_QUERY_RATE); try {
$this->setQueryWithUser(false);
/** @var array<Revenue> $data */ /** @var array<Revenue> $data */
$data = parent::getData($options); $data = $this->repository->getRevenue($begin, $end, null);
$event = new RevenueStatisticEvent($this->getBegin(), $this->getEnd()); $event = new RevenueStatisticEvent($begin, $end);
foreach ($data as $row) { foreach ($data as $row) {
$event->addRevenue($row->getCurrency(), $row->getAmount()); $event->addRevenue($row->getCurrency(), $row->getAmount());
} }
$this->dispatcher->dispatch($event); $this->dispatcher->dispatch($event);
return $event->getRevenue(); return $event->getRevenue();
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
public function getId(): string public function getId(): string

View File

@@ -1,18 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
final class Counter extends AbstractSimpleStatisticChart
{
public function getTemplateName(): string
{
return 'widget/widget-counter.html.twig';
}
}

View File

@@ -45,6 +45,10 @@ final class DailyWorkingTimeChart extends AbstractWidget
return true; return true;
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -56,30 +60,30 @@ final class DailyWorkingTimeChart extends AbstractWidget
], parent::getOptions($options)); ], parent::getOptions($options));
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$user = $this->getUser(); $user = $this->getUser();
$dateTimeFactory = DateTimeFactory::createByUser($user); $dateTimeFactory = DateTimeFactory::createByUser($user);
if ($options['begin'] === null) {
$options['begin'] = $dateTimeFactory->getStartOfWeek();
}
if ($options['begin'] instanceof \DateTimeInterface) {
$begin = $options['begin']; $begin = $options['begin'];
if (!($begin instanceof \DateTimeInterface)) {
if (\is_string($begin)) {
$begin = new DateTime($begin, new \DateTimeZone($user->getTimezone()));
} else { } else {
$begin = new DateTime($options['begin'], new \DateTimeZone($user->getTimezone())); $begin = $dateTimeFactory->getStartOfWeek();
}
} }
if ($options['end'] === null) {
$options['end'] = $dateTimeFactory->getEndOfWeek($begin);
}
if ($options['end'] instanceof \DateTimeInterface) {
$end = $options['end']; $end = $options['end'];
if (!($end instanceof \DateTimeInterface)) {
if (\is_string($end)) {
$end = new DateTime($end, new \DateTimeZone($user->getTimezone()));
} else { } else {
$end = new DateTime($options['end'], new \DateTimeZone($user->getTimezone())); $end = $dateTimeFactory->getEndOfWeek($begin);
}
} }
$activities = []; $activities = [];

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class DurationMonth extends AbstractCounterDuration final class DurationMonth extends AbstractCounterDuration
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
@@ -33,12 +43,17 @@ final class DurationMonth extends AbstractCounterDuration
return 'stats.durationMonth'; return 'stats.durationMonth';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(false); try {
$this->setBegin('first day of this month 00:00:00'); return $this->repository->getDurationForTimeRange($this->createMonthStartDate(), $this->createMonthEndDate(), null);
$this->setEnd('last day of this month 23:59:59'); } catch (\Exception $ex) {
throw new WidgetException(
return parent::getData($options); 'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class DurationToday extends AbstractCounterDuration final class DurationToday extends AbstractCounterDuration
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
@@ -33,12 +43,17 @@ final class DurationToday extends AbstractCounterDuration
return 'stats.durationToday'; return 'stats.durationToday';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(false); try {
$this->setBegin('00:00:00'); return $this->repository->getDurationForTimeRange($this->createTodayStartDate(), $this->createTodayEndDate(), null);
$this->setEnd('23:59:59'); } catch (\Exception $ex) {
throw new WidgetException(
return parent::getData($options); 'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class DurationTotal extends AbstractCounterDuration final class DurationTotal extends AbstractCounterDuration
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
@@ -33,10 +43,17 @@ final class DurationTotal extends AbstractCounterDuration
return 'stats.durationTotal'; return 'stats.durationTotal';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(false); try {
return $this->repository->getDurationForTimeRange(null, null, null);
return parent::getData($options); } catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class DurationWeek extends AbstractCounterDuration final class DurationWeek extends AbstractCounterDuration
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
@@ -33,12 +43,17 @@ final class DurationWeek extends AbstractCounterDuration
return 'stats.durationWeek'; return 'stats.durationWeek';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(false); try {
$this->setBegin('monday this week 00:00:00'); return $this->repository->getDurationForTimeRange($this->createWeekStartDate(), $this->createWeekEndDate(), null);
$this->setEnd('sunday this week 23:59:59'); } catch (\Exception $ex) {
throw new WidgetException(
return parent::getData($options); 'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,11 +9,22 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class DurationYear extends AbstractCounterYear final class DurationYear extends AbstractCounterYear
{ {
public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
{
parent::__construct($systemConfiguration);
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -22,12 +33,18 @@ final class DurationYear extends AbstractCounterYear
], parent::getOptions($options)); ], parent::getOptions($options));
} }
public function getData(array $options = []): mixed /**
* @param array<string, string|bool|int|null> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
{ {
$this->setQuery(TimesheetRepository::STATS_QUERY_DURATION); try {
$this->setQueryWithUser(false); return $this->repository->getDurationForTimeRange($begin, $end, null);
} catch (\Exception $ex) {
return parent::getData($options); throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
public function getPermissions(): array public function getPermissions(): array

View File

@@ -12,6 +12,7 @@ namespace App\Widget\Type;
use App\Configuration\SystemConfiguration; use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory; use App\Timesheet\DateTimeFactory;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
use DateTime; use DateTime;
@@ -46,6 +47,10 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
return 'widget/widget-paginatedworkingtimechart.html.twig'; return 'widget/widget-paginatedworkingtimechart.html.twig';
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
$options = parent::getOptions($options); $options = parent::getOptions($options);
@@ -81,6 +86,9 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
return $lastWeekInYear->format('W') === '53' ? 53 : 52; return $lastWeekInYear->format('W') === '53' ? 53 : 52;
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$user = $this->getUser(); $user = $this->getUser();
@@ -88,7 +96,18 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
$dateTimeFactory = DateTimeFactory::createByUser($user); $dateTimeFactory = DateTimeFactory::createByUser($user);
$year = $options['year']; $year = $options['year'];
if (\is_string($year)) {
$year = (int) $year;
} elseif (!\is_int($year)) {
throw new WidgetException('Invalid year given');
}
$week = $options['week']; $week = $options['week'];
if (\is_string($week)) {
$week = (int) $week;
} elseif (!\is_int($week)) {
throw new WidgetException('Invalid week given');
}
$weekBegin = ($dateTimeFactory->createDateTime())->setISODate($year, $week, 1)->setTime(0, 0, 0); $weekBegin = ($dateTimeFactory->createDateTime())->setISODate($year, $week, 1)->setTime(0, 0, 0);
@@ -99,7 +118,7 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
$lastWeekInLastYear = $this->getLastWeekInYear($year - 1); $lastWeekInLastYear = $this->getLastWeekInYear($year - 1);
$thisMonth = clone $weekBegin; $thisMonth = clone $weekBegin;
if ((int) $week === 1) { if ($week === 1) {
$thisMonth = ($dateTimeFactory->createDateTime())->setISODate($year, $week, 1)->setTime(0, 0, 0); $thisMonth = ($dateTimeFactory->createDateTime())->setISODate($year, $week, 1)->setTime(0, 0, 0);
} }
@@ -111,7 +130,7 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
$yearBegin = $dateTimeFactory->createDateTime(sprintf('01 january %s 00:00:00', $year)); $yearBegin = $dateTimeFactory->createDateTime(sprintf('01 january %s 00:00:00', $year));
$yearEnd = $dateTimeFactory->createDateTime(sprintf('31 december %s 23:59:59', $year)); $yearEnd = $dateTimeFactory->createDateTime(sprintf('31 december %s 23:59:59', $year));
$yearData = $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $yearBegin, $yearEnd, $user); $yearData = $this->repository->getDurationForTimeRange($yearBegin, $yearEnd, $user);
$financialYearData = null; $financialYearData = null;
$financialYearBegin = null; $financialYearBegin = null;
@@ -119,7 +138,7 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) { if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$financialYearBegin = $dateTimeFactory->createStartOfFinancialYear($financialYear); $financialYearBegin = $dateTimeFactory->createStartOfFinancialYear($financialYear);
$financialYearEnd = $dateTimeFactory->createEndOfFinancialYear($financialYearBegin); $financialYearEnd = $dateTimeFactory->createEndOfFinancialYear($financialYearBegin);
$financialYearData = $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $financialYearBegin, $financialYearEnd, $user); $financialYearData = $this->repository->getDurationForTimeRange($financialYearBegin, $financialYearEnd, $user);
} }
return [ return [
@@ -128,9 +147,9 @@ final class PaginatedWorkingTimeChart extends AbstractWidget
'thisMonth' => $thisMonth, 'thisMonth' => $thisMonth,
'lastWeekInYear' => $lastWeekInYear, 'lastWeekInYear' => $lastWeekInYear,
'lastWeekInLastYear' => $lastWeekInLastYear, 'lastWeekInLastYear' => $lastWeekInLastYear,
'day' => $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $dayBegin, $dayEnd, $user), 'day' => $this->repository->getDurationForTimeRange($dayBegin, $dayEnd, $user),
'week' => $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $weekBegin, $weekEnd, $user), 'week' => $this->repository->getDurationForTimeRange($weekBegin, $weekEnd, $user),
'month' => $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $monthBegin, $monthEnd, $user), 'month' => $this->repository->getDurationForTimeRange($monthBegin, $monthEnd, $user),
'year' => $yearData, 'year' => $yearData,
'financial' => $financialYearData, 'financial' => $financialYearData,
'financialBegin' => $financialYearBegin, 'financialBegin' => $financialYearBegin,

View File

@@ -24,6 +24,10 @@ final class TotalsActivity extends AbstractWidget
return 'stats.activityTotal'; return 'stats.activityTotal';
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -33,6 +37,9 @@ final class TotalsActivity extends AbstractWidget
], parent::getOptions($options)); ], parent::getOptions($options));
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$user = $this->getUser(); $user = $this->getUser();

View File

@@ -24,6 +24,10 @@ final class TotalsCustomer extends AbstractWidget
return 'stats.customerTotal'; return 'stats.customerTotal';
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -33,6 +37,9 @@ final class TotalsCustomer extends AbstractWidget
], parent::getOptions($options)); ], parent::getOptions($options));
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$user = $this->getUser(); $user = $this->getUser();

View File

@@ -24,6 +24,10 @@ final class TotalsProject extends AbstractWidget
return 'stats.projectTotal'; return 'stats.projectTotal';
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -33,6 +37,9 @@ final class TotalsProject extends AbstractWidget
], parent::getOptions($options)); ], parent::getOptions($options));
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$user = $this->getUser(); $user = $this->getUser();

View File

@@ -19,6 +19,10 @@ final class TotalsUser extends AbstractWidget
{ {
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -28,6 +32,9 @@ final class TotalsUser extends AbstractWidget
], parent::getOptions($options)); ], parent::getOptions($options));
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$user = $this->getUser(); $user = $this->getUser();

View File

@@ -13,6 +13,10 @@ use App\Widget\WidgetInterface;
final class UserAmountMonth extends AbstractUserRevenuePeriod final class UserAmountMonth extends AbstractUserRevenuePeriod
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
@@ -23,8 +27,11 @@ final class UserAmountMonth extends AbstractUserRevenuePeriod
return 'UserAmountMonth'; return 'UserAmountMonth';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return $this->getRevenue('first day of this month 00:00:00', 'last day of this month 23:59:59', $options); return $this->getRevenue($this->createMonthStartDate(), $this->createMonthEndDate(), $options);
} }
} }

View File

@@ -13,6 +13,10 @@ use App\Widget\WidgetInterface;
final class UserAmountToday extends AbstractUserRevenuePeriod final class UserAmountToday extends AbstractUserRevenuePeriod
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
@@ -23,8 +27,11 @@ final class UserAmountToday extends AbstractUserRevenuePeriod
return 'UserAmountToday'; return 'UserAmountToday';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return $this->getRevenue('00:00:00', '23:59:59', $options); return $this->getRevenue($this->createTodayStartDate(), $this->createTodayEndDate(), $options);
} }
} }

View File

@@ -13,6 +13,10 @@ use App\Widget\WidgetInterface;
final class UserAmountTotal extends AbstractUserRevenuePeriod final class UserAmountTotal extends AbstractUserRevenuePeriod
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
@@ -23,6 +27,9 @@ final class UserAmountTotal extends AbstractUserRevenuePeriod
return 'UserAmountTotal'; return 'UserAmountTotal';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return $this->getRevenue(null, null, $options); return $this->getRevenue(null, null, $options);

View File

@@ -13,6 +13,10 @@ use App\Widget\WidgetInterface;
final class UserAmountWeek extends AbstractUserRevenuePeriod final class UserAmountWeek extends AbstractUserRevenuePeriod
{ {
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
@@ -23,8 +27,11 @@ final class UserAmountWeek extends AbstractUserRevenuePeriod
return 'UserAmountWeek'; return 'UserAmountWeek';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
return $this->getRevenue('monday this week 00:00:00', 'sunday this week 23:59:59', $options); return $this->getRevenue($this->createWeekStartDate(), $this->createWeekEndDate(), $options);
} }
} }

View File

@@ -13,14 +13,15 @@ use App\Configuration\SystemConfiguration;
use App\Event\UserRevenueStatisticEvent; use App\Event\UserRevenueStatisticEvent;
use App\Model\Revenue; use App\Model\Revenue;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class UserAmountYear extends AbstractCounterYear final class UserAmountYear extends AbstractCounterYear
{ {
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher) public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher)
{ {
parent::__construct($repository, $systemConfiguration); parent::__construct($systemConfiguration);
} }
public function getTemplateName(): string public function getTemplateName(): string
@@ -43,6 +44,10 @@ final class UserAmountYear extends AbstractCounterYear
return 'UserAmountYear'; return 'UserAmountYear';
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -51,20 +56,26 @@ final class UserAmountYear extends AbstractCounterYear
], parent::getOptions($options)); ], parent::getOptions($options));
} }
public function getData(array $options = []): mixed /**
* @param array<string, string|bool|int|null> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
{ {
$this->setQuery(TimesheetRepository::STATS_QUERY_RATE); try {
$this->setQueryWithUser(true);
/** @var array<Revenue> $data */ /** @var array<Revenue> $data */
$data = parent::getData($options); $data = $this->repository->getRevenue($begin, $end, $this->getUser());
$event = new UserRevenueStatisticEvent($this->getUser(), $this->getBegin(), $this->getEnd()); $event = new UserRevenueStatisticEvent($this->getUser(), $begin, $end);
foreach ($data as $row) { foreach ($data as $row) {
$event->addRevenue($row->getCurrency(), $row->getAmount()); $event->addRevenue($row->getCurrency(), $row->getAmount());
} }
$this->dispatcher->dispatch($event); $this->dispatcher->dispatch($event);
return $event->getRevenue(); return $event->getRevenue();
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class UserDurationMonth extends AbstractCounterDuration final class UserDurationMonth extends AbstractCounterDuration
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
@@ -28,12 +38,17 @@ final class UserDurationMonth extends AbstractCounterDuration
return 'userDurationMonth'; return 'userDurationMonth';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(true); try {
$this->setBegin('first day of this month 00:00:00'); return $this->repository->getDurationForTimeRange($this->createMonthStartDate(), $this->createMonthEndDate(), $this->getUser());
$this->setEnd('last day of this month 23:59:59'); } catch (\Exception $ex) {
throw new WidgetException(
return parent::getData($options); 'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class UserDurationToday extends AbstractCounterDuration final class UserDurationToday extends AbstractCounterDuration
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
@@ -28,12 +38,17 @@ final class UserDurationToday extends AbstractCounterDuration
return 'userDurationToday'; return 'userDurationToday';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(true); try {
$this->setBegin('00:00:00'); return $this->repository->getDurationForTimeRange($this->createTodayStartDate(), $this->createTodayEndDate(), $this->getUser());
$this->setEnd('23:59:59'); } catch (\Exception $ex) {
throw new WidgetException(
return parent::getData($options); 'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class UserDurationTotal extends AbstractCounterDuration final class UserDurationTotal extends AbstractCounterDuration
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
@@ -28,10 +38,17 @@ final class UserDurationTotal extends AbstractCounterDuration
return 'userDurationTotal'; return 'userDurationTotal';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(true); try {
return $this->repository->getDurationForTimeRange(null, null, $this->getUser());
return parent::getData($options); } catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,10 +9,20 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class UserDurationWeek extends AbstractCounterDuration final class UserDurationWeek extends AbstractCounterDuration
{ {
public function __construct(private TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options)); return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
@@ -28,12 +38,17 @@ final class UserDurationWeek extends AbstractCounterDuration
return 'userDurationWeek'; return 'userDurationWeek';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$this->setQueryWithUser(true); try {
$this->setBegin('monday this week 00:00:00'); return $this->repository->getDurationForTimeRange($this->createWeekStartDate(), $this->createWeekEndDate(), $this->getUser());
$this->setEnd('sunday this week 23:59:59'); } catch (\Exception $ex) {
throw new WidgetException(
return parent::getData($options); 'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
} }

View File

@@ -9,11 +9,18 @@
namespace App\Widget\Type; namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface; use App\Widget\WidgetInterface;
final class UserDurationYear extends AbstractCounterYear final class UserDurationYear extends AbstractCounterYear
{ {
public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
{
parent::__construct($systemConfiguration);
}
public function getId(): string public function getId(): string
{ {
return 'userDurationYear'; return 'userDurationYear';
@@ -24,6 +31,10 @@ final class UserDurationYear extends AbstractCounterYear
return 'widget/widget-counter-duration.html.twig'; return 'widget/widget-counter-duration.html.twig';
} }
/**
* @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null>
*/
public function getOptions(array $options = []): array public function getOptions(array $options = []): array
{ {
return array_merge([ return array_merge([
@@ -32,12 +43,18 @@ final class UserDurationYear extends AbstractCounterYear
], parent::getOptions($options)); ], parent::getOptions($options));
} }
public function getData(array $options = []): mixed /**
* @param array<string, string|bool|int|null> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
{ {
$this->setQuery(TimesheetRepository::STATS_QUERY_DURATION); try {
$this->setQueryWithUser(true); return $this->repository->getDurationForTimeRange($begin, $end, $this->getUser());
} catch (\Exception $ex) {
return parent::getData($options); throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
} }
protected function getFinancialYearTitle(): string protected function getFinancialYearTitle(): string

View File

@@ -59,6 +59,9 @@ final class UserTeamProjects extends AbstractWidget
return 'UserTeamProjects'; return 'UserTeamProjects';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$user = $this->getUser(); $user = $this->getUser();

View File

@@ -52,6 +52,9 @@ final class UserTeams extends AbstractWidget
return 'UserTeams'; return 'UserTeams';
} }
/**
* @param array<string, string|bool|int|null> $options
*/
public function getData(array $options = []): mixed public function getData(array $options = []): mixed
{ {
$user = $this->getUser(); $user = $this->getUser();

View File

@@ -71,10 +71,10 @@ interface WidgetInterface
* make sure that the given $options will overwrite the internal option for * make sure that the given $options will overwrite the internal option for
* this one call. * this one call.
* *
* @param array $options * @param array<string, string|bool|int|null> $options
* @return mixed|null * @return mixed|null
*/ */
public function getData(array $options = []); public function getData(array $options = []): mixed;
/** /**
* Returns all widget options to be used in the frontend. * Returns all widget options to be used in the frontend.
@@ -86,7 +86,7 @@ interface WidgetInterface
* return array_merge($this->options, $options); * return array_merge($this->options, $options);
* *
* @param array<string, string|bool|int|null> $options * @param array<string, string|bool|int|null> $options
* @return array<string, string|bool|int|null|null> * @return array<string, string|bool|int|null>
*/ */
public function getOptions(array $options = []): array; public function getOptions(array $options = []): array;

View File

@@ -12,10 +12,9 @@
{% block box_body_class %}p-0{% endblock %} {% block box_body_class %}p-0{% endblock %}
{% block box_body %} {% block box_body %}
{% set columns = { {% set columns = {
'name': {'class': 'alwaysVisible'}, 'name': {'class': 'alwaysVisible w-25'},
'description': {'class': 'd-none d-md-table-cell'}, 'description': {'class': 'd-none d-md-table-cell'},
'version': {'class': 'text-center w-min'}, 'version': {'class': 'text-center w-min'},
'actions': {'class': 'actions alwaysVisible'},
} %} } %}
{% set tableName = 'plugins' %} {% set tableName = 'plugins' %}
@@ -26,17 +25,13 @@
{% for plugin in plugins|sort((p1, p2) => p1.name|lower <=> p2.name|lower) %} {% for plugin in plugins|sort((p1, p2) => p1.name|lower <=> p2.name|lower) %}
<tr> <tr>
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}"> <td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">
{% if plugin.id != plugin.name %} <a href="{{ plugin.metadata.homepage }}" title="{{ 'homepage'|trans }}" target="_blank">{{ plugin.name }}</a>
<span data-toggle="tooltip" data-placement="top" title="{{ plugin.id }}">{{ plugin.name }}</span>
{% else %}
{{ plugin.name }}
{% endif %}
</td> </td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ plugin.metadata.description }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'version') }}">{{ widgets.label(plugin.metadata.version, 'primary') }}</td> {{ plugin.metadata.description }}
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}"> </td>
{% set event = actions(app.user, 'plugin', 'index', {'plugin': plugin}) %} <td class="{{ tables.data_table_column_class(tableName, columns, 'version') }}">
{{ widgets.table_actions(event.actions) }} {{ widgets.label(plugin.metadata.version, 'primary', plugin.id) }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -55,9 +50,9 @@
{% endblock %} {% endblock %}
{% block box_body %} {% block box_body %}
{% set columns = { {% set columns = {
'name': {'class': 'alwaysVisible'}, 'name': {'class': 'alwaysVisible w-25'},
'description': {'class': 'd-none d-md-table-cell'}, 'description': {'class': 'd-none d-md-table-cell'},
'actions': {'class': 'actions alwaysVisible'}, 'shop': {'class': 'w-min alwaysVisible'},
} %} } %}
{% set tableName = 'extensions' %} {% set tableName = 'extensions' %}
@@ -71,7 +66,7 @@
<a href="{{ extension.url }}" title="{{ 'homepage'|trans }}" target="_blank">{{ extension.name }}</a> <a href="{{ extension.url }}" title="{{ 'homepage'|trans }}" target="_blank">{{ extension.name }}</a>
</td> </td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ extension.description }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ extension.description }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}"> <td class="{{ tables.data_table_column_class(tableName, columns, 'shop') }}">
{% if extension.buy is defined and extension.buy is not empty and (extension.minimum_version is not defined or (extension.minimum_version is not empty and constant('App\\Constants::VERSION') >= extension.minimum_version)) %} {% if extension.buy is defined and extension.buy is not empty and (extension.minimum_version is not defined or (extension.minimum_version is not empty and constant('App\\Constants::VERSION') >= extension.minimum_version)) %}
<a class="btn btn-icon btn-outline-secondary" href="{{ extension.buy }}" title="{{ 'buy'|trans({}, 'plugins') }}" target="_blank">{{ icon('shop') }}</a> <a class="btn btn-icon btn-outline-secondary" href="{{ extension.buy }}" title="{{ 'buy'|trans({}, 'plugins') }}" target="_blank">{{ icon('shop') }}</a>
{% endif %} {% endif %}

View File

@@ -3,8 +3,8 @@
{% block main %} {% block main %}
<div class="row"> <div class="row">
<div class="col-12 col-md-3 mb-3 d-none d-md-block"> <div class="col-12 col-xxl-2 col-md-3 mb-3 d-none d-md-block">
<div class="card"> <div class="card sticky-xxl-top">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% set sorted = [] %} {% set sorted = [] %}
{% for section in sections %} {% for section in sections %}
@@ -15,7 +15,7 @@
{% set sorted = sorted|merge([{'name': title, 'id': section.model.section, 'counter': section.form.children.configuration|length}]) %} {% set sorted = sorted|merge([{'name': title, 'id': section.model.section, 'counter': section.form.children.configuration|length}]) %}
{% endfor %} {% endfor %}
{% for section in sorted|sort((a, b) => a.name <=> b.name) %} {% for section in sorted|sort((a, b) => a.name <=> b.name) %}
<a class="list-group-item d-flex" href="#conf_{{ section.id }}"> <a class="list-group-item d-flex ps-4 pe-4 p-lg-3 pb-md-2 pt-md-2" href="#conf_{{ section.id }}">
{{ section.name }} {{ section.name }}
<small class="text-body-secondary ms-auto">{{ section.counter }}</small> <small class="text-body-secondary ms-auto">{{ section.counter }}</small>
</a> </a>
@@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<div class="col-12 col-md-9"> <div class="col-12 col-xxl-10 col-md-9">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %} {% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}

View File

@@ -25,23 +25,21 @@ use Symfony\Component\Console\Tester\CommandTester;
*/ */
class BundleInstallerCommandTest extends KernelTestCase class BundleInstallerCommandTest extends KernelTestCase
{ {
/** private Application $application;
* @var Application
*/
protected $application;
/**
* @param class-string $className
*/
protected function getCommand(string $className): Command protected function getCommand(string $className): Command
{ {
$kernel = self::bootKernel(); $kernel = self::bootKernel();
$this->application = new Application($kernel); $this->application = new Application($kernel);
$container = self::$kernel->getContainer();
$this->application->add(new $className()); $this->application->add(new $className());
return $this->application->find('kimai:bundle:test:install'); return $this->application->find('kimai:bundle:test:install');
} }
public function testFullRun() public function testFullRun(): void
{ {
$command = $this->getCommand(TestBundleInstallerCommand::class); $command = $this->getCommand(TestBundleInstallerCommand::class);
$commandTester = new CommandTester($command); $commandTester = new CommandTester($command);
@@ -53,7 +51,7 @@ class BundleInstallerCommandTest extends KernelTestCase
self::assertEquals(0, $commandTester->getStatusCode()); self::assertEquals(0, $commandTester->getStatusCode());
} }
public function testMissingMigrationThrowsException() public function testMissingMigrationThrowsException(): void
{ {
$command = $this->getCommand(InstallerWithMissingMigrationsCommand::class); $command = $this->getCommand(InstallerWithMissingMigrationsCommand::class);
$commandTester = new CommandTester($command); $commandTester = new CommandTester($command);
@@ -64,7 +62,7 @@ class BundleInstallerCommandTest extends KernelTestCase
self::assertEquals(1, $commandTester->getStatusCode()); self::assertEquals(1, $commandTester->getStatusCode());
} }
public function testAssetsInstallationFailure() public function testAssetsInstallationFailure(): void
{ {
$command = $this->getCommand(AssetsInstallerFailureCommand::class); $command = $this->getCommand(AssetsInstallerFailureCommand::class);
$commandTester = new CommandTester($command); $commandTester = new CommandTester($command);
@@ -75,7 +73,7 @@ class BundleInstallerCommandTest extends KernelTestCase
self::assertEquals(1, $commandTester->getStatusCode()); self::assertEquals(1, $commandTester->getStatusCode());
} }
public function testInvalidNamespaceWillRaiseException() public function testInvalidNamespaceWillRaiseException(): void
{ {
$this->expectException(LogicException::class); $this->expectException(LogicException::class);
$this->expectExceptionMessage('Unsupported namespace given, expected "KimaiPlugin" but received "App". Please overwrite getBundleName() and return the correct bundle name.'); $this->expectExceptionMessage('Unsupported namespace given, expected "KimaiPlugin" but received "App". Please overwrite getBundleName() and return the correct bundle name.');
@@ -85,7 +83,7 @@ class BundleInstallerCommandTest extends KernelTestCase
$commandTester->execute(['command' => $command->getName()]); $commandTester->execute(['command' => $command->getName()]);
} }
public function testAssetsInstallIsOk() public function testAssetsInstallIsOk(): void
{ {
$command = $this->getCommand(InstallerWithAssetsCommand::class); $command = $this->getCommand(InstallerWithAssetsCommand::class);
@@ -99,7 +97,7 @@ class BundleInstallerCommandTest extends KernelTestCase
self::assertEquals(0, $commandTester->getStatusCode()); self::assertEquals(0, $commandTester->getStatusCode());
} }
public function testAssetsInstallReturnsNonZeroExitCode() public function testAssetsInstallReturnsNonZeroExitCode(): void
{ {
$command = $this->getCommand(InstallerWithAssetsCommand::class); $command = $this->getCommand(InstallerWithAssetsCommand::class);
@@ -171,7 +169,7 @@ class AssetsInstallerFailureCommand extends TestBundleInstallerCommand
return true; return true;
} }
protected function installAssets(SymfonyStyle $io, OutputInterface $output) protected function installAssets(SymfonyStyle $io, OutputInterface $output): void
{ {
throw new \Exception('Problem occurred while installing assets.'); throw new \Exception('Problem occurred while installing assets.');
} }

View File

@@ -172,21 +172,14 @@ class SystemConfigurationTest extends TestCase
(new Configuration())->setName('timesheet.foo')->setValue('hello'), (new Configuration())->setName('timesheet.foo')->setValue('hello'),
]); ]);
$this->assertEquals('hello', $sut->find('timesheet.foo')); $this->assertEquals('hello', $sut->find('timesheet.foo'));
$this->assertEquals('hello', $sut->offsetGet('timesheet.foo'));
$this->assertTrue($sut->has('timesheet.foo')); $this->assertTrue($sut->has('timesheet.foo'));
$this->assertTrue($sut->offsetExists('timesheet.foo'));
$this->assertFalse($sut->has('timesheet.yyyyyyyyy')); $this->assertFalse($sut->has('timesheet.yyyyyyyyy'));
$this->assertFalse($sut->offsetExists('timesheet.yyyyyyyyy'));
$this->assertFalse($sut->has('xxxxxxxx.yyyyyyyyy')); $this->assertFalse($sut->has('xxxxxxxx.yyyyyyyyy'));
$this->assertFalse($sut->offsetExists('xxxxxxxx.yyyyyyyyy'));
$this->assertNull($sut->find('xxxxxxxx.yyyyyyyyy')); $this->assertNull($sut->find('xxxxxxxx.yyyyyyyyy'));
$this->assertNull($sut->offsetGet('xxxxxxxx.yyyyyyyyy'));
$sut->offsetSet('xxxxxxxx.yyyyyyyyy', 'foooo-bar!'); $sut->set('xxxxxxxx.yyyyyyyyy', 'foooo-bar!');
$this->assertTrue($sut->has('xxxxxxxx.yyyyyyyyy')); $this->assertTrue($sut->has('xxxxxxxx.yyyyyyyyy'));
$this->assertTrue($sut->offsetExists('xxxxxxxx.yyyyyyyyy'));
$this->assertEquals('foooo-bar!', $sut->find('xxxxxxxx.yyyyyyyyy')); $this->assertEquals('foooo-bar!', $sut->find('xxxxxxxx.yyyyyyyyy'));
$this->assertEquals('foooo-bar!', $sut->offsetGet('xxxxxxxx.yyyyyyyyy'));
} }
public function testCalendarWithoutLoader() public function testCalendarWithoutLoader()

View File

@@ -18,29 +18,29 @@ use App\Tests\Plugin\Fixtures\TestPlugin\TestPlugin;
*/ */
class PluginControllerTest extends ControllerBaseTest class PluginControllerTest extends ControllerBaseTest
{ {
public function testIsSecure() public function testIsSecure(): void
{ {
$this->assertUrlIsSecured('/admin/plugins/'); $this->assertUrlIsSecured('/admin/plugins/');
} }
public function testIsSecureForRole() public function testIsSecureForRole(): void
{ {
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/plugins/'); $this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/plugins/');
} }
public function testIndexAction() public function testIndexAction(): void
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->assertAccessIsGranted($client, '/admin/plugins/'); $this->assertAccessIsGranted($client, '/admin/plugins/');
$this->assertCalloutWidgetWithMessage($client, 'You have no plugins installed yet'); $this->assertCalloutWidgetWithMessage($client, 'You have no plugins installed yet');
} }
public function testIndexActionWithInstalledPlugins() public function testIndexActionWithInstalledPlugins(): void
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
/** @var PluginManager $manager */ $manager = new PluginManager([new TestPlugin()]);
$manager = self::getContainer()->set(PluginManager::class, new PluginManager([new TestPlugin()])); self::getContainer()->set(PluginManager::class, $manager);
$this->assertAccessIsGranted($client, '/admin/plugins/'); $this->assertAccessIsGranted($client, '/admin/plugins/');
$this->assertHasDataTable($client); $this->assertHasDataTable($client);

View File

@@ -30,10 +30,10 @@ class DebugRendererTest extends TestCase
/** /**
* @dataProvider getTestModel * @dataProvider getTestModel
*/ */
public function testRender(InvoiceModel $model, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3, $hasProject, $metaFields = []) public function testRender(InvoiceModel $model, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3, $hasProject, $metaFields = []): void
{ {
$itemHydrator = new class() implements InvoiceItemHydrator { $itemHydrator = new class() implements InvoiceItemHydrator {
public function setInvoiceModel(InvoiceModel $model) public function setInvoiceModel(InvoiceModel $model): void
{ {
} }
@@ -75,7 +75,7 @@ class DebugRendererTest extends TestCase
self::assertEquals('21.03.12', $data['model']['invoice.last']); self::assertEquals('21.03.12', $data['model']['invoice.last']);
} }
protected function assertModelStructure(array $model, int $projectCounter = 0, int $activityCounter = 0) protected function assertModelStructure(array $model, int $projectCounter = 0, int $activityCounter = 0): void
{ {
$keys = [ $keys = [
'invoice.due_date', 'invoice.due_date',
@@ -243,7 +243,7 @@ class DebugRendererTest extends TestCase
$this->assertEquals($keys, $givenKeys); $this->assertEquals($keys, $givenKeys);
} }
protected function assertEntryStructure(array $model, array $metaFields) protected function assertEntryStructure(array $model, array $metaFields): void
{ {
$keys = [ $keys = [
'entry.row', 'entry.row',

View File

@@ -26,6 +26,7 @@ use App\Invoice\DefaultInvoiceFormatter;
use App\Invoice\InvoiceFormatter; use App\Invoice\InvoiceFormatter;
use App\Invoice\InvoiceModel; use App\Invoice\InvoiceModel;
use App\Invoice\NumberGenerator\DateNumberGenerator; use App\Invoice\NumberGenerator\DateNumberGenerator;
use App\Invoice\NumberGeneratorInterface;
use App\Invoice\Renderer\AbstractRenderer; use App\Invoice\Renderer\AbstractRenderer;
use App\Model\InvoiceDocument; use App\Model\InvoiceDocument;
use App\Repository\InvoiceRepository; use App\Repository\InvoiceRepository;
@@ -255,7 +256,7 @@ trait RendererTestTrait
return $model; return $model;
} }
private function getNumberGeneratorSut() private function getNumberGeneratorSut(): NumberGeneratorInterface
{ {
$repository = $this->createMock(InvoiceRepository::class); $repository = $this->createMock(InvoiceRepository::class);
$repository $repository

View File

@@ -23,11 +23,11 @@ class TestUserEntity implements UserInterface
return 'foo'; return 'foo';
} }
public function eraseCredentials() public function eraseCredentials(): void
{ {
} }
public function getTimeFormat() public function getTimeFormat(): string
{ {
return 'H:i'; return 'H:i';
} }

View File

@@ -10,8 +10,8 @@
namespace App\Tests\Twig\Runtime; namespace App\Tests\Twig\Runtime;
use App\Entity\User; use App\Entity\User;
use App\Tests\Widget\Type\More;
use App\Twig\Runtime\WidgetExtension; use App\Twig\Runtime\WidgetExtension;
use App\Widget\Type\More;
use App\Widget\WidgetService; use App\Widget\WidgetService;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;

View File

@@ -28,7 +28,10 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
return new DurationValidator(); return new DurationValidator();
} }
public function getValidData() /**
* @return array<array<string|int|null>>
*/
public function getValidData(): array
{ {
return [ return [
['2h'], ['2h'],
@@ -55,7 +58,7 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
]; ];
} }
public function testConstraintIsInvalid() public function testConstraintIsInvalid(): void
{ {
$this->expectException(UnexpectedTypeException::class); $this->expectException(UnexpectedTypeException::class);
@@ -65,7 +68,7 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
/** /**
* @dataProvider getValidData * @dataProvider getValidData
*/ */
public function testConstraintWithValidData(string|int|null $input) public function testConstraintWithValidData(string|int|null $input): void
{ {
$constraint = new Duration(); $constraint = new Duration();
$this->validator->validate($input, $constraint); $this->validator->validate($input, $constraint);
@@ -75,7 +78,10 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
$this->assertNoViolation(); $this->assertNoViolation();
} }
public function getInvalidData() /**
* @return array<array<string>>
*/
public function getInvalidData(): array
{ {
return [ return [
['13-13'], ['13-13'],
@@ -96,7 +102,7 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
/** /**
* @dataProvider getInvalidData * @dataProvider getInvalidData
*/ */
public function testValidationError(string $input) public function testValidationError(string $input): void
{ {
$constraint = new Duration([ $constraint = new Duration([
'message' => 'myMessage', 'message' => 'myMessage',
@@ -106,6 +112,7 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
$this->buildViolation('myMessage') $this->buildViolation('myMessage')
->setParameter('{{ value }}', '"' . $input . '"') ->setParameter('{{ value }}', '"' . $input . '"')
->setParameter('{{ pattern }}', '/^-?[0-9]{1,}$|^-?[0-9]{1,}[,.]{1}[0-9]{1,}$|^-?[0-9]{1,}:[0-9]{1,}:[0-9]{1,}$|^-?[0-9]{1,}:[0-9]{1,}$|^[0-9]{1,}[hHmMsS]{1}$|^[0-9]{1,}[hH]{1}[0-9]{1,}[mM]{1}$|^[0-9]{1,}[hHmM]{1}[0-9]{1,}[sS]{1}$|^[0-9]{1,}[mM]{1}[0-9]{1,}[sS]{1}$|^[0-9]{1,}[hH]{1}[0-9]{1,}[mM]{1}[0-9]{1,}[sS]{1}$/')
->setCode(Regex::REGEX_FAILED_ERROR) ->setCode(Regex::REGEX_FAILED_ERROR)
->assertRaised(); ->assertRaised();
} }
@@ -113,7 +120,7 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
/** /**
* @dataProvider getInvalidData * @dataProvider getInvalidData
*/ */
public function testValidationErrorUpperCase(string $input) public function testValidationErrorUpperCase(string $input): void
{ {
$input = strtoupper($input); $input = strtoupper($input);
$constraint = new Duration([ $constraint = new Duration([
@@ -124,6 +131,7 @@ class DurationValidatorTest extends ConstraintValidatorTestCase
$this->buildViolation('myMessage') $this->buildViolation('myMessage')
->setParameter('{{ value }}', '"' . $input . '"') ->setParameter('{{ value }}', '"' . $input . '"')
->setParameter('{{ pattern }}', '/^-?[0-9]{1,}$|^-?[0-9]{1,}[,.]{1}[0-9]{1,}$|^-?[0-9]{1,}:[0-9]{1,}:[0-9]{1,}$|^-?[0-9]{1,}:[0-9]{1,}$|^[0-9]{1,}[hHmMsS]{1}$|^[0-9]{1,}[hH]{1}[0-9]{1,}[mM]{1}$|^[0-9]{1,}[hHmM]{1}[0-9]{1,}[sS]{1}$|^[0-9]{1,}[mM]{1}[0-9]{1,}[sS]{1}$|^[0-9]{1,}[hH]{1}[0-9]{1,}[mM]{1}[0-9]{1,}[sS]{1}$/')
->setCode(Regex::REGEX_FAILED_ERROR) ->setCode(Regex::REGEX_FAILED_ERROR)
->assertRaised(); ->assertRaised();
} }

View File

@@ -1,17 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Widget\Type;
/**
* @covers \App\Widget\Type\AbstractSimpleStatisticChart
*/
abstract class AbstractSimpleStatisticsWidgetTypeTest extends AbstractWidgetTypeTest
{
}

View File

@@ -20,12 +20,12 @@ abstract class AbstractWidgetTypeTest extends AbstractWidgetTest
abstract public function getDefaultOptions(): array; abstract public function getDefaultOptions(): array;
protected function assertDefaultData(AbstractWidgetType $sut) protected function assertDefaultData(AbstractWidgetType $sut): void
{ {
self::assertNull($sut->getData()); self::assertNull($sut->getData());
} }
public function testDefaultData() public function testDefaultData(): void
{ {
$sut = $this->createSut(); $sut = $this->createSut();
self::assertInstanceOf(AbstractWidgetType::class, $sut); self::assertInstanceOf(AbstractWidgetType::class, $sut);
@@ -33,15 +33,7 @@ abstract class AbstractWidgetTypeTest extends AbstractWidgetTest
$this->assertDefaultData($sut); $this->assertDefaultData($sut);
} }
public function testFluentInterface() public function testSetter(): void
{
$sut = $this->createSut();
self::assertInstanceOf(AbstractWidgetType::class, $sut->setOptions([]));
self::assertInstanceOf(AbstractWidgetType::class, $sut->setId(''));
self::assertInstanceOf(AbstractWidgetType::class, $sut->setTitle(''));
}
public function testSetter()
{ {
$sut = $this->createSut(); $sut = $this->createSut();

View File

@@ -13,6 +13,7 @@ use App\Entity\User;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Tests\Mocks\SystemConfigurationFactory; use App\Tests\Mocks\SystemConfigurationFactory;
use App\Widget\Type\AbstractCounterYear; use App\Widget\Type\AbstractCounterYear;
use App\Widget\Type\AbstractWidget;
use App\Widget\Type\AbstractWidgetType; use App\Widget\Type\AbstractWidgetType;
use App\Widget\Type\ActiveUsersYear; use App\Widget\Type\ActiveUsersYear;
@@ -36,6 +37,11 @@ class ActiveUsersYearTest extends AbstractWidgetTypeTest
return $sut; return $sut;
} }
protected function assertDefaultData(AbstractWidget $sut): void
{
self::assertEquals(0, $sut->getData());
}
public function getDefaultOptions(): array public function getDefaultOptions(): array
{ {
return [ return [
@@ -44,7 +50,7 @@ class ActiveUsersYearTest extends AbstractWidgetTypeTest
]; ];
} }
public function testSettings() public function testSettings(): void
{ {
$sut = $this->createSut(); $sut = $this->createSut();

Some files were not shown because too many files have changed in this diff Show More