Release 2.15 (#4749)
This commit is contained in:
@@ -84,6 +84,7 @@
|
||||
"fakerphp/faker": "^1.15",
|
||||
"friendsofphp/php-cs-fixer": "^3.3",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||
"phpstan/phpstan-doctrine": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.0",
|
||||
|
||||
147
composer.lock
generated
147
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "24f571304052968a6a7f49cac1109c5b",
|
||||
"content-hash": "e857da36ba58dbba786a711ffe4f6320",
|
||||
"packages": [
|
||||
{
|
||||
"name": "azuyalabs/yasumi",
|
||||
@@ -2374,16 +2374,16 @@
|
||||
},
|
||||
{
|
||||
"name": "kevinpapst/tabler-bundle",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kevinpapst/TablerBundle.git",
|
||||
"reference": "b4519deb2cb9fff3493c108c73a13af54401b5e8"
|
||||
"reference": "d957405e18589a351856e4b7b8670731a5c34896"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/b4519deb2cb9fff3493c108c73a13af54401b5e8",
|
||||
"reference": "b4519deb2cb9fff3493c108c73a13af54401b5e8",
|
||||
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/d957405e18589a351856e4b7b8670731a5c34896",
|
||||
"reference": "d957405e18589a351856e4b7b8670731a5c34896",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2431,7 +2431,7 @@
|
||||
"description": "Admin/Backend theme bundle for Symfony based on Tabler.io",
|
||||
"support": {
|
||||
"issues": "https://github.com/kevinpapst/TablerBundle/issues",
|
||||
"source": "https://github.com/kevinpapst/TablerBundle/tree/1.3.0"
|
||||
"source": "https://github.com/kevinpapst/TablerBundle/tree/1.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2443,7 +2443,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-02-12T16:13:20+00:00"
|
||||
"time": "2024-04-12T10:09:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/csv",
|
||||
@@ -3110,16 +3110,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nelmio/api-doc-bundle",
|
||||
"version": "v4.25.1",
|
||||
"version": "4.25.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nelmio/NelmioApiDocBundle.git",
|
||||
"reference": "f594e600bf5943008562be35c4acd60957434d22"
|
||||
"reference": "fc5d60d74b63806f7d6bb80b8393db1a12ae2dbc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/f594e600bf5943008562be35c4acd60957434d22",
|
||||
"reference": "f594e600bf5943008562be35c4acd60957434d22",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/fc5d60d74b63806f7d6bb80b8393db1a12ae2dbc",
|
||||
"reference": "fc5d60d74b63806f7d6bb80b8393db1a12ae2dbc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3153,8 +3153,11 @@
|
||||
"friendsofsymfony/rest-bundle": "^2.8|^3.0",
|
||||
"jms/serializer": "^1.14|^3.0",
|
||||
"jms/serializer-bundle": "^2.3|^3.0|^4.0|^5.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpstan/phpstan-phpunit": "^1.3",
|
||||
"phpstan/phpstan-strict-rules": "^1.5",
|
||||
"phpstan/phpstan-symfony": "^1.3",
|
||||
"phpunit/phpunit": "^8.5|^9.6",
|
||||
"sensio/framework-extra-bundle": "^5.4|^6.0",
|
||||
"symfony/asset": "^5.4|^6.0|^7.0",
|
||||
"symfony/browser-kit": "^5.4|^6.0|^7.0",
|
||||
"symfony/cache": "^5.4|^6.0|^7.0",
|
||||
@@ -3216,9 +3219,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nelmio/NelmioApiDocBundle/issues",
|
||||
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.25.1"
|
||||
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/4.25.2"
|
||||
},
|
||||
"time": "2024-04-04T08:16:08+00:00"
|
||||
"time": "2024-04-06T12:08:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nelmio/cors-bundle",
|
||||
@@ -3613,28 +3616,35 @@
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
"version": "5.3.0",
|
||||
"version": "5.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
|
||||
"reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
|
||||
"reference": "298d2febfe79d03fe714eb871d5538da55205b1a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
|
||||
"reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/298d2febfe79d03fe714eb871d5538da55205b1a",
|
||||
"reference": "298d2febfe79d03fe714eb871d5538da55205b1a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/deprecations": "^1.1",
|
||||
"ext-filter": "*",
|
||||
"php": "^7.2 || ^8.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpdocumentor/reflection-common": "^2.2",
|
||||
"phpdocumentor/type-resolver": "^1.3",
|
||||
"phpdocumentor/type-resolver": "^1.7",
|
||||
"phpstan/phpdoc-parser": "^1.7",
|
||||
"webmozart/assert": "^1.9.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "~1.3.2",
|
||||
"psalm/phar": "^4.8"
|
||||
"mockery/mockery": "~1.3.5",
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"phpstan/phpstan-mockery": "^1.1",
|
||||
"phpstan/phpstan-webmozart-assert": "^1.2",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"vimeo/psalm": "^5.13"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -3658,15 +3668,15 @@
|
||||
},
|
||||
{
|
||||
"name": "Jaap van Otterdijk",
|
||||
"email": "account@ijaap.nl"
|
||||
"email": "opensource@ijaap.nl"
|
||||
}
|
||||
],
|
||||
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
|
||||
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
|
||||
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.0"
|
||||
},
|
||||
"time": "2021-10-19T17:43:47+00:00"
|
||||
"time": "2024-04-09T21:13:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/type-resolver",
|
||||
@@ -10769,16 +10779,16 @@
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.52.1",
|
||||
"version": "v3.53.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc"
|
||||
"reference": "69a19093a9ded8d1baac62ed6c009b8bc148d008"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/6e77207f0d851862ceeb6da63e6e22c01b1587bc",
|
||||
"reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/69a19093a9ded8d1baac62ed6c009b8bc148d008",
|
||||
"reference": "69a19093a9ded8d1baac62ed6c009b8bc148d008",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10802,6 +10812,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"facile-it/paraunit": "^1.3 || ^2.0",
|
||||
"infection/infection": "^0.27.11",
|
||||
"justinrainbow/json-schema": "^5.2",
|
||||
"keradus/cli-executor": "^2.1",
|
||||
"mikey179/vfsstream": "^1.6.11",
|
||||
@@ -10849,7 +10860,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.52.1"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.53.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10857,7 +10868,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-19T21:02:43+00:00"
|
||||
"time": "2024-04-08T15:03:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
@@ -11164,6 +11175,54 @@
|
||||
],
|
||||
"time": "2024-03-28T16:17:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-deprecation-rules",
|
||||
"version": "1.1.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-deprecation-rules.git",
|
||||
"reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa",
|
||||
"reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0",
|
||||
"phpstan/phpstan": "^1.10.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/phpstan-php-parser": "^1.1",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"rules.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPStan\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.1.4"
|
||||
},
|
||||
"time": "2023-08-05T09:02:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
"version": "1.3.65",
|
||||
@@ -11290,21 +11349,21 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
|
||||
"reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542"
|
||||
"reference": "568210bd301f94a0d4b1e5a0808c374c1b9cf11b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/7a50e9662ee9f3942e4aaaf3d603653f60282542",
|
||||
"reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/568210bd301f94a0d4b1e5a0808c374c1b9cf11b",
|
||||
"reference": "568210bd301f94a0d4b1e5a0808c374c1b9cf11b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0",
|
||||
"phpstan/phpstan": "^1.10.34"
|
||||
"phpstan/phpstan": "^1.10.60"
|
||||
},
|
||||
"require-dev": {
|
||||
"nikic/php-parser": "^4.13.0",
|
||||
@@ -11333,22 +11392,22 @@
|
||||
"description": "Extra strict and opinionated rules for PHPStan",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.2"
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.3"
|
||||
},
|
||||
"time": "2023-10-30T14:35:06+00:00"
|
||||
"time": "2024-04-06T07:43:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-symfony",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-symfony.git",
|
||||
"reference": "a32bc86da24495025d7aafd1ba62444d4a364a98"
|
||||
"reference": "3fbf634b2495adaa1dc4b7300293f8abf74128d8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a32bc86da24495025d7aafd1ba62444d4a364a98",
|
||||
"reference": "a32bc86da24495025d7aafd1ba62444d4a364a98",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/3fbf634b2495adaa1dc4b7300293f8abf74128d8",
|
||||
"reference": "3fbf634b2495adaa1dc4b7300293f8abf74128d8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11405,9 +11464,9 @@
|
||||
"description": "Symfony Framework extensions and rules for PHPStan",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-symfony/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.9"
|
||||
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.11"
|
||||
},
|
||||
"time": "2024-03-16T16:50:20+00:00"
|
||||
"time": "2024-04-12T07:42:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
|
||||
@@ -23,6 +23,8 @@ doctrine:
|
||||
types:
|
||||
datetime: App\Doctrine\UTCDateTimeType
|
||||
orm:
|
||||
controller_resolver:
|
||||
auto_mapping: true
|
||||
auto_generate_proxy_classes: '%kernel.debug%'
|
||||
default_entity_manager: default
|
||||
enable_lazy_ghost_objects: true
|
||||
|
||||
@@ -20,6 +20,7 @@ security:
|
||||
api:
|
||||
access_token:
|
||||
token_handler: App\API\Authentication\AccessTokenHandler
|
||||
remember_me: false
|
||||
request_matcher: App\API\Authentication\ApiRequestMatcher
|
||||
user_checker: App\Security\UserChecker
|
||||
stateless: true
|
||||
@@ -92,7 +93,7 @@ security:
|
||||
- { path: '^/(%app_locales%)/register', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/resetting', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/', roles: ROLE_USER }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED }
|
||||
|
||||
when@test:
|
||||
# this configuration simplifies testing URLs protected by the security mechanism
|
||||
|
||||
105
phpstan.neon
105
phpstan.neon
@@ -4,6 +4,7 @@ includes:
|
||||
- %rootDir%/../phpstan-doctrine/extension.neon
|
||||
- %rootDir%/../phpstan-doctrine/rules.neon
|
||||
- %rootDir%/../phpstan-strict-rules/rules.neon
|
||||
- %rootDir%/../phpstan-deprecation-rules/rules.neon
|
||||
- %rootDir%/../phpstan/conf/bleedingEdge.neon
|
||||
|
||||
parameters:
|
||||
@@ -3447,11 +3448,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Invoice/Calculator/AbstractMergedCalculator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method format\\(\\) on DateTime\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getCurrency\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -3462,26 +3458,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceItemDefaultHydrator\\:\\:hydrate\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$date of method App\\\\Invoice\\\\InvoiceFormatter\\:\\:getFormattedDateTime\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#"
|
||||
count: 3
|
||||
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$date of method App\\\\Invoice\\\\InvoiceFormatter\\:\\:getFormattedTime\\(\\) expects DateTimeInterface, DateTime\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceModelActivityHydrator\\:\\:getBudgetValues\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -5572,95 +5553,15 @@ parameters:
|
||||
count: 1
|
||||
path: src/Voter/TimesheetVoter.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'billable' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'day' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'details' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'duration' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'month' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'rate' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'year' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getCustomer\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Activity\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
path: src/Widget/Type/DailyWorkingTimeChart.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot cast mixed to float\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Cannot cast mixed to int\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$details of method App\\\\Model\\\\Statistic\\\\Day\\:\\:setDetails\\(\\) expects array, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$seconds of method App\\\\Model\\\\Statistic\\\\Day\\:\\:setTotalDurationBillable\\(\\) expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$year of method DateTime\\:\\:setDate\\(\\) expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$month of method DateTime\\:\\:setDate\\(\\) expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$day of method DateTime\\:\\:setDate\\(\\) expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/DataProvider/DailyWorkingTimeChartProvider.php
|
||||
path: src/Widget/Type/DailyWorkingTimeChart.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Widget\\\\Type\\\\AbstractWidget\\:\\:getUser\\(\\) should return App\\\\Entity\\\\User but returns App\\\\Entity\\\\User\\|null\\.$#"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# See https://github.com/symfony/recipes-contrib/blob/main/symfony/apache-pack/1.0/public/.htaccess#L38
|
||||
|
||||
# Use the front controller as index file. It serves as a fallback solution when
|
||||
# every other rewrite/redirect fails (e.g. in an aliased environment without
|
||||
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||
@@ -5,30 +7,41 @@
|
||||
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||
DirectoryIndex index.php
|
||||
|
||||
# Do NOT use Kimai with http, this is a huge security risk!
|
||||
# Activate these lines if you configured SSL and certificates:
|
||||
# By default, Apache does not evaluate symbolic links if you did not enable this
|
||||
# feature in your server configuration. Uncomment the following line if you
|
||||
# install assets as symlinks or if you experience problems related to symlinks
|
||||
# when compiling LESS/Sass/CoffeScript assets.
|
||||
# Options +SymLinksIfOwnerMatch
|
||||
|
||||
#<IfModule mod_rewrite.c>
|
||||
# RewriteEngine On
|
||||
# RewriteCond %{SERVER_PORT} 80
|
||||
# RewriteRule ^(.*)$ https://kimai.example.com/$1 [R,L]
|
||||
#</IfModule>
|
||||
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
|
||||
# to the front controller "/index.php" but be rewritten to "/index.php/index".
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
# This Option needs to be enabled for RewriteRule, otherwise it will show an error like
|
||||
# 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden'
|
||||
Options +SymLinksIfOwnerMatch
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Determine the RewriteBase automatically and set it as environment variable.
|
||||
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||
# project in a subdirectory, the base path will be prepended to allow proper
|
||||
# resolution of the app.php file and to redirect to the correct URI. It will
|
||||
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||
# work in environments without path prefix as well, providing a safe, one-size
|
||||
# fits all solution. But as you do not need it in this case, you can comment
|
||||
# the following 2 lines to eliminate the overhead.
|
||||
RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
|
||||
RewriteRule ^(.*) - [E=BASE:%1]
|
||||
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
|
||||
RewriteRule .* - [E=BASE:%1]
|
||||
|
||||
# Sets the HTTP_AUTHORIZATION header removed by Apache
|
||||
RewriteCond %{HTTP:Authorization} .+
|
||||
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
|
||||
|
||||
# Redirect to URI without front controller to prevent duplicate content
|
||||
# (with and without `/app.php`). Only do this redirect on the initial
|
||||
# (with and without `/index.php`). Only do this redirect on the initial
|
||||
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
|
||||
# endless redirect loop (request -> rewrite to front controller ->
|
||||
# redirect -> request -> ...).
|
||||
@@ -38,16 +51,14 @@ DirectoryIndex index.php
|
||||
# - disable this feature by commenting the following 2 lines or
|
||||
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||
# following RewriteCond (best solution)
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} ^$
|
||||
RewriteRule ^index\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||
|
||||
# If the requested filename exists, simply serve it.
|
||||
# We only want to let Apache serve files and not directories.
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule .? - [L]
|
||||
|
||||
# Rewrite all other queries to the front controller.
|
||||
RewriteRule .? %{ENV:BASE}/index.php [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_rewrite.c>
|
||||
@@ -55,7 +66,7 @@ DirectoryIndex index.php
|
||||
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||
# the start page to the front controller explicitly so that the website
|
||||
# and the generated links can still be used.
|
||||
RedirectMatch 302 ^/$ /index.php/
|
||||
RedirectMatch 307 ^/$ /index.php/
|
||||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
@@ -26,7 +26,7 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/actions')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Actions')]
|
||||
final class ActionsController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/activities')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Activity')]
|
||||
final class ActivityController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Default')]
|
||||
final class ConfigurationController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/customers')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Customer')]
|
||||
final class CustomerController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
|
||||
#[Route(path: '/projects')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Project')]
|
||||
final class ProjectController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Default')]
|
||||
final class StatusController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/tags')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Tag')]
|
||||
final class TagController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/teams')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Team')]
|
||||
final class TeamController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
|
||||
#[Route(path: '/timesheets')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'Timesheet')]
|
||||
final class TimesheetController extends BaseApiController
|
||||
{
|
||||
@@ -85,14 +85,14 @@ final class TimesheetController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'tags', map: true, strict: true, nullable: true, default: [], description: 'List of tag names, e.g. tags[]=bar&tags[]=foo')]
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|begin|end|rate', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)')]
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: DESC)')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5)')]
|
||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5)')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
|
||||
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
|
||||
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
|
||||
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.')]
|
||||
#[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||
{
|
||||
$query = new TimesheetQuery(false);
|
||||
@@ -398,7 +398,7 @@ final class TimesheetController extends BaseApiController
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
#[OA\Response(response: 200, description: 'Returns the collection of recent user activities (always the latest entry of a unique working set grouped by customer, project and activity)', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TimesheetCollectionExpanded')))]
|
||||
#[Route(methods: ['GET'], path: '/recent', name: 'recent_timesheet')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included. Default: today - 1 year (format: HTML5)')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included. Default: today - 1 year (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries (default: 10)')]
|
||||
public function recentAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/users')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
#[IsGranted('IS_AUTHENTICATED')]
|
||||
#[OA\Tag(name: 'User')]
|
||||
final class UserController extends BaseApiController
|
||||
{
|
||||
|
||||
@@ -127,6 +127,8 @@ final class SystemConfiguration
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
@trigger_error('The method "SystemConfiguration::offsetExists()" is deprecated, use "has()" instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->has($offset);
|
||||
}
|
||||
|
||||
@@ -135,6 +137,8 @@ final class SystemConfiguration
|
||||
*/
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
@trigger_error('The method "SystemConfiguration::offsetGet()" is deprecated, use "find()" instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->find($offset);
|
||||
}
|
||||
|
||||
@@ -143,16 +147,9 @@ final class SystemConfiguration
|
||||
*/
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$this->set($offset, $value);
|
||||
}
|
||||
@trigger_error('The method "SystemConfiguration::offsetSet()" is deprecated, use "set()" instead', E_USER_DEPRECATED);
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0.35
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
throw new \BadMethodCallException('SystemBundleConfiguration does not support offsetUnset()');
|
||||
$this->set($offset, $value);
|
||||
}
|
||||
|
||||
// ========== Authentication configurations ==========
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.14.0';
|
||||
public const VERSION = '2.15.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 21400;
|
||||
public const VERSION_ID = 21500;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -199,7 +199,7 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
}
|
||||
}
|
||||
|
||||
protected function logException(\Exception $ex): void
|
||||
protected function logException(\Throwable $ex): void
|
||||
{
|
||||
$this->container->get('logger')->critical($ex->getMessage());
|
||||
}
|
||||
|
||||
@@ -265,6 +265,7 @@ final class DoctorController extends AbstractController
|
||||
'sys_temp_dir',
|
||||
'date.timezone',
|
||||
'session.gc_maxlifetime',
|
||||
'disable_functions'
|
||||
];
|
||||
|
||||
$settings = [];
|
||||
|
||||
@@ -61,7 +61,7 @@ final class PluginController extends AbstractController
|
||||
$item->expiresAfter(86400); // one day
|
||||
|
||||
return $response->toArray();
|
||||
} catch (\Exception $exception) {
|
||||
} catch (\Throwable $exception) {
|
||||
$this->logException($exception);
|
||||
$this->flashError('Could not download plugin information');
|
||||
}
|
||||
|
||||
@@ -88,12 +88,12 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
if ($canSeeRate) {
|
||||
$table->addColumn('hourlyRate', ['class' => 'text-end d-none text-nowrap']);
|
||||
$table->addColumn('internalRate', ['class' => 'text-end text-nowrap d-none d-md-table-cell']);
|
||||
$table->addColumn('internalRate', ['class' => 'text-end text-nowrap d-none d-xxl-table-cell']);
|
||||
$table->addColumn('rate', ['class' => 'text-end text-nowrap']);
|
||||
}
|
||||
|
||||
$table->addColumn('customer', ['class' => 'd-none d-md-table-cell']);
|
||||
$table->addColumn('project', ['class' => 'd-none d-lg-table-cell']);
|
||||
$table->addColumn('project', ['class' => 'd-none d-xl-table-cell']);
|
||||
$table->addColumn('activity', ['class' => 'd-none d-xl-table-cell']);
|
||||
$table->addColumn('description', ['class' => 'd-none']);
|
||||
$table->addColumn('tags', ['class' => 'd-none badges', 'orderBy' => false]);
|
||||
@@ -103,7 +103,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
|
||||
if ($canSeeUsername) {
|
||||
$table->addColumn('username', ['class' => 'd-none d-sm-table-cell', 'orderBy' => false]);
|
||||
$table->addColumn('username', ['class' => 'd-none d-md-table-cell', 'orderBy' => false]);
|
||||
}
|
||||
|
||||
$table->addColumn('billable', ['class' => 'text-center d-none w-min', 'orderBy' => false]);
|
||||
|
||||
@@ -11,9 +11,9 @@ namespace App\Doctrine\Extensions;
|
||||
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\AST\Node;
|
||||
use Doctrine\ORM\Query\Lexer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
final class Date extends FunctionNode
|
||||
{
|
||||
@@ -26,9 +26,9 @@ final class Date extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->value = $parser->ArithmeticPrimary();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ namespace App\Doctrine\Extensions;
|
||||
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\AST\Node;
|
||||
use Doctrine\ORM\Query\Lexer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
final class Day extends FunctionNode
|
||||
{
|
||||
@@ -26,9 +26,9 @@ final class Day extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->value = $parser->ArithmeticPrimary();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ namespace App\Doctrine\Extensions;
|
||||
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\AST\Node;
|
||||
use Doctrine\ORM\Query\Lexer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
final class Month extends FunctionNode
|
||||
{
|
||||
@@ -26,9 +26,9 @@ final class Month extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->value = $parser->ArithmeticPrimary();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ namespace App\Doctrine\Extensions;
|
||||
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\AST\Node;
|
||||
use Doctrine\ORM\Query\Lexer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
final class Year extends FunctionNode
|
||||
{
|
||||
@@ -26,9 +26,9 @@ final class Year extends FunctionNode
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(Lexer::T_IDENTIFIER);
|
||||
$parser->match(Lexer::T_OPEN_PARENTHESIS);
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->value = $parser->ArithmeticPrimary();
|
||||
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Doctrine;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\DateTimeType;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
||||
final class UTCDateTimeType extends DateTimeType
|
||||
{
|
||||
@@ -67,7 +68,7 @@ final class UTCDateTimeType extends DateTimeType
|
||||
if (!$converted) {
|
||||
throw ConversionException::conversionFailedFormat(
|
||||
$value,
|
||||
$this->getName(),
|
||||
Types::DATETIME_MUTABLE,
|
||||
$platform->getDateTimeFormatString()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ use App\Invoice\InvoiceModel;
|
||||
|
||||
final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
||||
{
|
||||
private const DATE_PROCESS_FORMAT = 'Y-m-d h:i:s';
|
||||
|
||||
private InvoiceModel $model;
|
||||
|
||||
public function setInvoiceModel(InvoiceModel $model): void
|
||||
@@ -75,17 +77,24 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
||||
'entry.duration_format' => $formatter->getFormattedDuration($item->getDuration()),
|
||||
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($item->getDuration()),
|
||||
'entry.duration_minutes' => (int) ($item->getDuration() / 60),
|
||||
'entry.begin' => $formatter->getFormattedDateTime($begin),
|
||||
'entry.begin_time' => $formatter->getFormattedTime($begin),
|
||||
'entry.begin_timestamp' => $begin->getTimestamp(),
|
||||
'entry.end' => $formatter->getFormattedDateTime($end),
|
||||
'entry.end_time' => $formatter->getFormattedTime($end),
|
||||
'entry.end_timestamp' => $end->getTimestamp(),
|
||||
'entry.date' => $formatter->getFormattedDateTime($begin),
|
||||
'entry.week' => \intval($begin->format('W')),
|
||||
'entry.weekyear' => $begin->format('o'),
|
||||
];
|
||||
|
||||
if ($begin !== null) {
|
||||
$values['entry.begin'] = $formatter->getFormattedDateTime($begin);
|
||||
$values['entry.begin_time'] = $formatter->getFormattedTime($begin);
|
||||
$values['entry.begin_timestamp'] = $begin->getTimestamp();
|
||||
$values['entry.date'] = $formatter->getFormattedDateTime($begin);
|
||||
$values['entry.date_process'] = $begin->format(self::DATE_PROCESS_FORMAT); // since 2.14
|
||||
$values['entry.week'] = \intval($begin->format('W'));
|
||||
$values['entry.weekyear'] = $begin->format('o');
|
||||
}
|
||||
|
||||
if ($end !== null) {
|
||||
$values['entry.end'] = $formatter->getFormattedDateTime($end);
|
||||
$values['entry.end_time'] = $formatter->getFormattedTime($end);
|
||||
$values['entry.end_timestamp'] = $end->getTimestamp();
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$values = array_merge($values, [
|
||||
'entry.user_id' => $user->getId(),
|
||||
|
||||
@@ -66,6 +66,8 @@ final class InvoiceModelActivityHydrator implements InvoiceModelHydrator
|
||||
$prefix . 'id' => $activity->getId(),
|
||||
$prefix . 'name' => $activity->getName() ?? '',
|
||||
$prefix . 'comment' => $activity->getComment() ?? '',
|
||||
$prefix . 'number' => $activity->getNumber() ?? '',
|
||||
$prefix . 'invoice_text' => $activity->getInvoiceText() ?? '',
|
||||
];
|
||||
|
||||
if ($model->getQuery()?->getEnd() !== null) {
|
||||
|
||||
@@ -79,6 +79,8 @@ final class InvoiceModelProjectHydrator implements InvoiceModelHydrator
|
||||
$prefix . 'budget_time' => $project->getTimeBudget(),
|
||||
$prefix . 'budget_time_decimal' => $formatter->getFormattedDecimalDuration($project->getTimeBudget()),
|
||||
$prefix . 'budget_time_minutes' => (int) ($project->getTimeBudget() / 60),
|
||||
$prefix . 'number' => $project->getNumber() ?? '',
|
||||
$prefix . 'invoice_text' => $project->getInvoiceText() ?? '',
|
||||
];
|
||||
|
||||
if ($model->getQuery()?->getEnd() !== null) {
|
||||
|
||||
@@ -17,7 +17,14 @@ use App\Entity\Project;
|
||||
class ActivityQuery extends ProjectQuery
|
||||
{
|
||||
public const ACTIVITY_ORDER_ALLOWED = [
|
||||
'name', 'description' => 'comment', 'customer', 'project', 'budget', 'timeBudget', 'visible'
|
||||
'name',
|
||||
'description' => 'comment',
|
||||
'activity_number' => 'number',
|
||||
'customer',
|
||||
'project',
|
||||
'budget',
|
||||
'timeBudget',
|
||||
'visible'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,8 +16,17 @@ class ProjectQuery extends BaseQuery implements VisibilityInterface
|
||||
use VisibilityTrait;
|
||||
|
||||
public const PROJECT_ORDER_ALLOWED = [
|
||||
'name', 'description' => 'comment', 'customer', 'orderNumber', 'orderDate',
|
||||
'project_start', 'project_end', 'budget', 'timeBudget', 'visible'
|
||||
'name',
|
||||
'description' => 'comment',
|
||||
'project_number' => 'number',
|
||||
'customer',
|
||||
'orderNumber',
|
||||
'orderDate',
|
||||
'project_start',
|
||||
'project_end',
|
||||
'budget',
|
||||
'timeBudget',
|
||||
'visible'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,7 +62,11 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
|
||||
|
||||
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||
{
|
||||
if (!($user instanceof User)) {
|
||||
if (!($user instanceof User) || !$user->isInternalUser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->getPassword() === $newHashedPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ final class SamlAuthenticationSuccessHandler extends DefaultAuthenticationSucces
|
||||
}
|
||||
|
||||
$relayState = $request->get('RelayState');
|
||||
if (null !== $relayState && $relayState !== $this->httpUtils->generateUri($request, $this->options['login_path'])) {
|
||||
$loginUrl = $this->httpUtils->generateUri($request, $this->options['login_path']);
|
||||
|
||||
if ($relayState !== null && $relayState !== '' && $relayState !== $loginUrl) {
|
||||
return $relayState;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,9 @@ final class TimesheetStatisticService
|
||||
->addSelect('DAY(t.date) as day')
|
||||
->addSelect('MONTH(t.date) as month')
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
@@ -124,9 +124,9 @@ final class TimesheetStatisticService
|
||||
->addSelect('IDENTITY(t.project) as project')
|
||||
->addSelect('IDENTITY(t.activity) as activity')
|
||||
->addSelect('DATE(t.date) as date')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
@@ -200,9 +200,9 @@ final class TimesheetStatisticService
|
||||
->addSelect('IDENTITY(t.activity) as activity')
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->addSelect('MONTH(t.date) as month')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
@@ -292,9 +292,9 @@ final class TimesheetStatisticService
|
||||
->addSelect('MONTH(t.date) as month')
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->addSelect('IDENTITY(t.user) as user')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
|
||||
@@ -1,206 +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\DataProvider;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Day;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* This class should really be deleted and replaced by TimesheetStatisticService::getDailyStatistics()
|
||||
* @deprecated since 2.0
|
||||
* @codeCoverageIgnore
|
||||
* @internal
|
||||
* @final
|
||||
*/
|
||||
class DailyWorkingTimeChartProvider
|
||||
{
|
||||
public function __construct(private readonly TimesheetRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* In case this method is called with one timezone and the results are from another timezone,
|
||||
* it might return rows outside the time-range.
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
protected function getDailyData(DateTimeInterface $begin, DateTimeInterface $end, ?User $user = null): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('t');
|
||||
|
||||
$or = $qb->expr()->orX();
|
||||
$or->add($qb->expr()->between(':begin', 't.begin', 't.end'));
|
||||
$or->add($qb->expr()->between(':end', 't.begin', 't.end'));
|
||||
$or->add($qb->expr()->between('t.begin', ':begin', ':end'));
|
||||
$or->add($qb->expr()->between('t.end', ':begin', ':end'));
|
||||
|
||||
$qb->select('t, p, a, c')
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($or)
|
||||
->orderBy('t.begin', 'DESC')
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->leftJoin('t.activity', 'a')
|
||||
->leftJoin('t.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
;
|
||||
|
||||
if (null !== $user) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->eq('t.user', ':user'))
|
||||
->setParameter('user', $user)
|
||||
;
|
||||
}
|
||||
|
||||
$timesheets = $qb->getQuery()->getResult();
|
||||
|
||||
$results = [];
|
||||
/** @var Timesheet $result */
|
||||
foreach ($timesheets as $result) {
|
||||
/** @var DateTime $beginTmp */
|
||||
$beginTmp = $result->getBegin();
|
||||
/** @var DateTime $endTmp */
|
||||
$endTmp = $result->getEnd();
|
||||
$dateKeyEnd = $endTmp->format('Ymd');
|
||||
|
||||
do {
|
||||
$dateKey = $beginTmp->format('Ymd');
|
||||
|
||||
if ($dateKey !== $dateKeyEnd) {
|
||||
$newDateBegin = clone $beginTmp;
|
||||
$newDateBegin->add(new \DateInterval('P1D'));
|
||||
// overlapping records should always start at midnight
|
||||
$newDateBegin->setTime(0, 0, 0);
|
||||
} else {
|
||||
$newDateBegin = clone $endTmp;
|
||||
}
|
||||
|
||||
// make sure to exclude entries that are outside the requested time-range:
|
||||
// these entries can exist if you have long running entries that started before $begin
|
||||
// for statistical reasons we have to include everything between $begin and $end while
|
||||
// excluding everything that is outside of that range
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Be aware that this will NOT filter every record, in case there is a timezone mismatch between the
|
||||
// begin/end dates and the ones from the database (eg. recorded in UTC) - which might actually be
|
||||
// before $begin (which happens thanks to the timezone conversion when querying the database)
|
||||
if ($newDateBegin > $begin && $beginTmp < $end) {
|
||||
if (!isset($results[$dateKey])) {
|
||||
$results[$dateKey] = [
|
||||
'rate' => 0,
|
||||
'duration' => 0,
|
||||
'billable' => 0, // duration
|
||||
'month' => $beginTmp->format('n'),
|
||||
'year' => $beginTmp->format('Y'),
|
||||
'day' => $beginTmp->format('j'),
|
||||
'details' => []
|
||||
];
|
||||
}
|
||||
$duration = $newDateBegin->getTimestamp() - $beginTmp->getTimestamp();
|
||||
$durationPercent = 0;
|
||||
if ($result->getDuration() !== null && $result->getDuration() > 0) {
|
||||
$durationPercent = $duration / $result->getDuration();
|
||||
}
|
||||
$rate = $result->getRate() * $durationPercent;
|
||||
|
||||
$results[$dateKey]['rate'] += $rate;
|
||||
$results[$dateKey]['duration'] += $duration;
|
||||
if ($result->isBillable()) {
|
||||
$results[$dateKey]['billable'] += $duration;
|
||||
}
|
||||
$detailsId =
|
||||
$result->getProject()->getCustomer()->getId()
|
||||
. '_' . $result->getProject()->getId()
|
||||
. '_' . $result->getActivity()->getId()
|
||||
;
|
||||
|
||||
if (!isset($results[$dateKey]['details'][$detailsId])) {
|
||||
$results[$dateKey]['details'][$detailsId] = [
|
||||
'project' => $result->getProject(),
|
||||
'activity' => $result->getActivity(),
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
'billable' => 0, // duration
|
||||
];
|
||||
}
|
||||
|
||||
$results[$dateKey]['details'][$detailsId]['duration'] += $duration;
|
||||
$results[$dateKey]['details'][$detailsId]['rate'] += $rate;
|
||||
if ($result->isBillable()) {
|
||||
$results[$dateKey]['details'][$detailsId]['billable'] += $duration;
|
||||
}
|
||||
}
|
||||
|
||||
$beginTmp = $newDateBegin;
|
||||
|
||||
// yes, we only want to compare the day, not the time
|
||||
if ((int) $end->format('Ymd') < (int) $newDateBegin->format('Ymd')) {
|
||||
break;
|
||||
}
|
||||
} while ($dateKey !== $dateKeyEnd);
|
||||
}
|
||||
|
||||
ksort($results);
|
||||
|
||||
foreach ($results as $key => $value) {
|
||||
$results[$key]['details'] = array_values($results[$key]['details']);
|
||||
}
|
||||
|
||||
return array_values($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0 - use TimesheetStatisticService::getDailyStatistics() instead
|
||||
*
|
||||
* @param User|null $user
|
||||
* @param DateTimeInterface $begin
|
||||
* @param DateTimeInterface $end
|
||||
* @return Day[]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getData(?User $user, DateTimeInterface $begin, DateTimeInterface $end): array
|
||||
{
|
||||
/** @var Day[] $days */
|
||||
$days = [];
|
||||
|
||||
// prefill the array
|
||||
$tmp = DateTime::createFromInterface($end);
|
||||
$until = (int) $begin->format('Ymd');
|
||||
while ((int) $tmp->format('Ymd') >= $until) {
|
||||
$last = clone $tmp;
|
||||
$days[$last->format('Ymd')] = new Day($last, 0, 0.00);
|
||||
$tmp->modify('-1 day');
|
||||
}
|
||||
|
||||
$results = $this->getDailyData($begin, $end, $user);
|
||||
|
||||
foreach ($results as $statRow) {
|
||||
$dateTime = DateTime::createFromInterface($begin);
|
||||
$dateTime->setDate($statRow['year'], $statRow['month'], $statRow['day']);
|
||||
$dateTime->setTime(0, 0, 0);
|
||||
$day = new Day($dateTime, (int) $statRow['duration'], (float) $statRow['rate']);
|
||||
$day->setTotalDurationBillable($statRow['billable']);
|
||||
$day->setDetails($statRow['details']);
|
||||
$dateKey = $dateTime->format('Ymd');
|
||||
// make sure entries from other timezones are filtered
|
||||
if (!\array_key_exists($dateKey, $days)) {
|
||||
continue;
|
||||
}
|
||||
$days[$dateKey] = $day;
|
||||
}
|
||||
|
||||
ksort($days);
|
||||
|
||||
return array_values($days);
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,21 @@ namespace App\Widget\Type;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Day;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Widget\DataProvider\DailyWorkingTimeChartProvider;
|
||||
use App\Widget\WidgetInterface;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* This is rendered inside the PaginatedWorkingTimeChart.
|
||||
*/
|
||||
final class DailyWorkingTimeChart extends AbstractWidget
|
||||
{
|
||||
public function __construct(private DailyWorkingTimeChartProvider $dailyWorkingTimeChartProvider)
|
||||
public function __construct(private readonly TimesheetRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -47,7 +51,7 @@ final class DailyWorkingTimeChart extends AbstractWidget
|
||||
|
||||
/**
|
||||
* @param array<string, string|bool|int|null|array<string, mixed>> $options
|
||||
@return array<string, string|bool|int|null|array<string, mixed>>
|
||||
* @return array<string, string|bool|int|null|array<string, mixed>>
|
||||
*/
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
@@ -71,7 +75,7 @@ final class DailyWorkingTimeChart extends AbstractWidget
|
||||
$begin = $options['begin'];
|
||||
if (!($begin instanceof \DateTimeInterface)) {
|
||||
if (\is_string($begin)) {
|
||||
$begin = new DateTime($begin, new \DateTimeZone($user->getTimezone()));
|
||||
$begin = new \DateTimeImmutable($begin, new \DateTimeZone($user->getTimezone()));
|
||||
} else {
|
||||
$begin = $dateTimeFactory->getStartOfWeek();
|
||||
}
|
||||
@@ -80,14 +84,14 @@ final class DailyWorkingTimeChart extends AbstractWidget
|
||||
$end = $options['end'];
|
||||
if (!($end instanceof \DateTimeInterface)) {
|
||||
if (\is_string($end)) {
|
||||
$end = new DateTime($end, new \DateTimeZone($user->getTimezone()));
|
||||
$end = new \DateTimeImmutable($end, new \DateTimeZone($user->getTimezone()));
|
||||
} else {
|
||||
$end = $dateTimeFactory->getEndOfWeek($begin);
|
||||
}
|
||||
}
|
||||
|
||||
$activities = [];
|
||||
$statistics = $this->dailyWorkingTimeChartProvider->getData($user, $begin, $end);
|
||||
$statistics = $this->getPreparedData($user, $begin, $end);
|
||||
|
||||
foreach ($statistics as $day) {
|
||||
foreach ($day->getDetails() as $entry) {
|
||||
@@ -125,4 +129,120 @@ final class DailyWorkingTimeChart extends AbstractWidget
|
||||
{
|
||||
return 'widget/widget-dailyworkingtimechart.html.twig';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{'duration': int, 'billable': int, 'month': numeric-string, 'year': numeric-string, 'day': numeric-string, 'details': array<int|string, array<mixed>>}>
|
||||
*/
|
||||
private function getDailyData(DateTimeInterface $begin, DateTimeInterface $end, User $user): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('t');
|
||||
|
||||
$qb->select('t, p, a, c')
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->eq('t.user', ':user'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $user)
|
||||
->leftJoin('t.activity', 'a')
|
||||
->leftJoin('t.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
;
|
||||
|
||||
$timesheets = $qb->getQuery()->getResult();
|
||||
|
||||
$results = [];
|
||||
/** @var Timesheet $result */
|
||||
foreach ($timesheets as $result) {
|
||||
/** @var DateTime $beginTmp */
|
||||
$beginTmp = $result->getBegin();
|
||||
|
||||
$dateKey = $beginTmp->format('Ymd');
|
||||
|
||||
if (!isset($results[$dateKey])) {
|
||||
$results[$dateKey] = [
|
||||
'duration' => 0,
|
||||
'billable' => 0, // duration
|
||||
'month' => $beginTmp->format('n'),
|
||||
'year' => $beginTmp->format('Y'),
|
||||
'day' => $beginTmp->format('j'),
|
||||
'details' => []
|
||||
];
|
||||
}
|
||||
$duration = $result->getDuration() ?? 0;
|
||||
|
||||
$results[$dateKey]['duration'] += $duration;
|
||||
if ($result->isBillable()) {
|
||||
$results[$dateKey]['billable'] += $duration;
|
||||
}
|
||||
$detailsId =
|
||||
$result->getProject()->getCustomer()->getId()
|
||||
. '_' . ($result->getProject()?->getId() ?? '')
|
||||
. '_' . ($result->getActivity()?->getId() ?? '')
|
||||
;
|
||||
|
||||
if (!isset($results[$dateKey]['details'][$detailsId])) {
|
||||
$results[$dateKey]['details'][$detailsId] = [
|
||||
'project' => $result->getProject(),
|
||||
'activity' => $result->getActivity(),
|
||||
'duration' => 0,
|
||||
'billable' => 0, // duration
|
||||
];
|
||||
}
|
||||
|
||||
$results[$dateKey]['details'][$detailsId]['duration'] += $duration;
|
||||
if ($result->isBillable()) {
|
||||
$results[$dateKey]['details'][$detailsId]['billable'] += $duration;
|
||||
}
|
||||
}
|
||||
|
||||
ksort($results);
|
||||
|
||||
foreach ($results as $key => $value) {
|
||||
$results[$key]['details'] = array_values($value['details']);
|
||||
}
|
||||
|
||||
return array_values($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Day[]
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getPreparedData(User $user, DateTimeInterface $begin, DateTimeInterface $end): array
|
||||
{
|
||||
/** @var Day[] $days */
|
||||
$days = [];
|
||||
|
||||
// prefill the array
|
||||
$tmp = DateTime::createFromInterface($end);
|
||||
$until = (int) $begin->format('Ymd');
|
||||
while ((int) $tmp->format('Ymd') >= $until) {
|
||||
$last = clone $tmp;
|
||||
$days[$last->format('Ymd')] = new Day($last, 0, 0.00);
|
||||
$tmp->modify('-1 day');
|
||||
}
|
||||
|
||||
// TODO replace with TimesheetStatisticService::getDailyStatistics()
|
||||
$results = $this->getDailyData($begin, $end, $user);
|
||||
|
||||
foreach ($results as $statRow) {
|
||||
$dateTime = DateTime::createFromInterface($begin);
|
||||
$dateTime->setDate((int) $statRow['year'], (int) $statRow['month'], (int) $statRow['day']);
|
||||
$dateTime->setTime(0, 0, 0);
|
||||
$day = new Day($dateTime, (int) $statRow['duration'], 0.00); // rate is not used in frontend
|
||||
$day->setTotalDurationBillable($statRow['billable']);
|
||||
$day->setDetails($statRow['details']);
|
||||
$dateKey = $dateTime->format('Ymd');
|
||||
// make sure entries from other timezones are filtered
|
||||
if (!\array_key_exists($dateKey, $days)) {
|
||||
continue;
|
||||
}
|
||||
$days[$dateKey] = $day;
|
||||
}
|
||||
|
||||
ksort($days);
|
||||
|
||||
return array_values($days);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,6 @@
|
||||
{{- get_title() -}}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_actions_class %}col-auto ms-auto d-print-none{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{% if 'dashboard' in app.request.attributes.get('_route') %}
|
||||
{{ parent() }}
|
||||
|
||||
@@ -11,54 +11,35 @@
|
||||
<img onclick="document.getElementById('totp_secret').classList.toggle('d-none');" data-toggle="tooltip" title="Click to show code" alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" src="{{ qr_code.dataUri }}" />
|
||||
<span id="totp_secret" style="display: block" class="d-none">{{ secret }}</span>
|
||||
</p>
|
||||
{% if user.totpAuthenticationEnabled %}
|
||||
<div class="row mb-3">
|
||||
<div class="form-selectgroup form-selectgroup-boxes d-flex">
|
||||
<label class="form-selectgroup-item flex-column">
|
||||
<input type="radio" id="user_two_factor_enable" name="user_two_factor[enabled]" class="form-selectgroup-input" value="1" checked="checked">
|
||||
<div class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<div class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-check-label" for="user_two_factor_enable">{{ 'activated'|trans }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="form-selectgroup-item flex-column">
|
||||
<input type="radio" id="user_two_factor_disable" name="user_two_factor[enabled]" class="form-selectgroup-input" value="0">
|
||||
<div class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<div class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-check-label" for="user_two_factor_disable">{{ 'deactivated'|trans }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_start(deactivate, {'attr': {'id': 'user_two_factor_form'}}) }}
|
||||
{{ form_end(deactivate) }}
|
||||
{% else %}
|
||||
{% if not user.totpAuthenticationEnabled %}
|
||||
{{ form_start(form, {'attr': {'id': 'user_two_factor_form'}}) }}
|
||||
<p>{{ 'profile.2fa_confirmation'|trans }}</p>
|
||||
{{ form_widget(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% else %}
|
||||
<strong><i class="{{ 'success'|icon(true) }} text-success"></i> {{ 'activated'|trans }}</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" id="user_two_factor_button" />
|
||||
{% if user.totpAuthenticationEnabled %}
|
||||
{{ form_start(deactivate, {'attr': {'id': 'user_two_factor_form'}}) }}
|
||||
<input type="submit" value="{{ 'deactivate'|trans }}" class="btn btn-warning" id="user_two_factor_button" />
|
||||
{{ form_end(deactivate) }}
|
||||
{% else %}
|
||||
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" id="user_two_factor_button" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% if not user.totpAuthenticationEnabled %}
|
||||
<script>
|
||||
document.getElementById('user_two_factor_button').addEventListener('click', function(){
|
||||
document.getElementById('user_two_factor_form').submit();
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>
|
||||
{{ 'api_password.intro'|trans }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>URL</strong>: {{ url('api.swagger_ui', {}, false)|replace({'/doc': ''}) }}</li>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p>
|
||||
{{ 'api_password.intro'|trans }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>URL</strong>: {{ url('api.swagger_ui', {}, false)|replace({'/doc': ''}) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 text-md-end">
|
||||
<a class="btn action-create modal-ajax-form" href="{{ path('user_profile_access_token', {'username': user.userIdentifier}) }}">{{ icon('create', true) }} {{ 'create'|trans }}</a>
|
||||
<a class="btn" target="_blank" href="{{ path('api.swagger_ui') }}" data-toggle="tooltip" title="Swagger API Docs">{{ icon('documentation', true) }} Swagger Docs</a>
|
||||
<a class="btn" target="_blank" href="{{ 'rest-api.html'|docu_link }}" data-toggle="tooltip" title="{{ 'help'|trans }}">{{ icon('help', true) }} {{ 'help'|trans }}</a>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 text-md-end">
|
||||
<a class="btn action-create modal-ajax-form" href="{{ path('user_profile_access_token', {'username': user.userIdentifier}) }}">{{ icon('create', true) }} {{ 'create'|trans }}</a>
|
||||
<a class="btn" target="_blank" href="{{ path('api.swagger_ui') }}">{{ icon('documentation', true) }} Swagger Docs</a>
|
||||
<a class="btn" target="_blank" href="{{ 'rest-api.html'|docu_link }}">{{ icon('help', true) }} {{ 'help'|trans }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if created_token is not null %}
|
||||
@@ -39,7 +39,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if access_tokens|length > 0 %}
|
||||
<table class="table">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'name'|trans }}</th>
|
||||
@@ -81,6 +81,7 @@
|
||||
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'activity_invoice_settings'} %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block title %}{{ 'password'|trans }}{% endblock %}
|
||||
{% block box_class %}mt-3{% endblock %}
|
||||
{% block body %}
|
||||
{{ widgets.alert('danger', 'api_password_deprecated'|trans) }}
|
||||
{{ form_start(form) }}
|
||||
|
||||
@@ -163,15 +163,6 @@ class SystemConfigurationTest extends TestCase
|
||||
$this->assertEquals('2020-03-27', $sut->getFinancialYearStart());
|
||||
}
|
||||
|
||||
public function testOffsetUnsetThrowsException(): void
|
||||
{
|
||||
$this->expectException(\BadMethodCallException::class);
|
||||
$this->expectExceptionMessage('SystemBundleConfiguration does not support offsetUnset()');
|
||||
|
||||
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||
$sut->offsetUnset('dfsdf');
|
||||
}
|
||||
|
||||
public function testUnknownConfigs(): void
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||
|
||||
@@ -18,11 +18,11 @@ use App\Tests\Mocks\Saml\SamlAuthFactoryFactory;
|
||||
use App\Tests\Mocks\SystemConfigurationFactory;
|
||||
use OneLogin\Saml2\Auth;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
@@ -127,7 +127,7 @@ class SamlControllerTest extends TestCase
|
||||
|
||||
$request = new Request();
|
||||
$request->setSession($this->createMock(SessionInterface::class));
|
||||
$request->attributes->set(Security::AUTHENTICATION_ERROR, new \Exception('My test error'));
|
||||
$request->attributes->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, new \Exception('My test error'));
|
||||
|
||||
$factory = $this->getMockBuilder(SamlAuthFactory::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ class InvoiceTemplateTest extends TestCase
|
||||
self::assertEquals('default', $sut->getNumberGenerator());
|
||||
self::assertEquals('default', $sut->getRenderer());
|
||||
self::assertEquals(30, $sut->getDueDays());
|
||||
self::assertTrue($sut->isDecimalDuration());
|
||||
}
|
||||
|
||||
public function testSetNullForOptionalValues(): void
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Tests\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Style;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -42,7 +43,7 @@ abstract class AbstractFormatterTest extends TestCase
|
||||
$sut->setFormattedValue($worksheet, 1, 1, $this->getActualValue());
|
||||
$cell = $worksheet->getCell([1, 1]);
|
||||
$this->assertCellValue($cell);
|
||||
$this->assertCellStyle($worksheet->getStyleByColumnAndRow(1, 1));
|
||||
$this->assertCellStyle($worksheet->getStyle(CellAddress::fromColumnAndRow(1, 1)));
|
||||
}
|
||||
|
||||
public function testSetNull(): void
|
||||
|
||||
@@ -52,6 +52,6 @@ class DateFormatterTest extends AbstractFormatterTest
|
||||
|
||||
public function assertCellStyle(Style $style): void
|
||||
{
|
||||
self::assertEquals(NumberFormat::FORMAT_DATE_YYYYMMDD2, $style->getNumberFormat()->getFormatCode());
|
||||
self::assertEquals(NumberFormat::FORMAT_DATE_YYYYMMDD, $style->getNumberFormat()->getFormatCode());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ class InvoiceItemDefaultHydratorTest extends TestCase
|
||||
'entry.end_time',
|
||||
'entry.end_timestamp',
|
||||
'entry.date',
|
||||
'entry.date_process',
|
||||
'entry.week',
|
||||
'entry.weekyear',
|
||||
'entry.user_id',
|
||||
|
||||
@@ -37,6 +37,8 @@ class InvoiceModelActivityHydratorTest extends TestCase
|
||||
'activity.id',
|
||||
'activity.name',
|
||||
'activity.comment',
|
||||
'activity.number',
|
||||
'activity.invoice_text',
|
||||
'activity.meta.foo-activity',
|
||||
'activity.budget_open',
|
||||
'activity.budget_open_plain',
|
||||
@@ -45,6 +47,8 @@ class InvoiceModelActivityHydratorTest extends TestCase
|
||||
'activity.1.id',
|
||||
'activity.1.name',
|
||||
'activity.1.comment',
|
||||
'activity.1.number',
|
||||
'activity.1.invoice_text',
|
||||
'activity.1.meta.foo-activity',
|
||||
'activity.1.budget_open',
|
||||
'activity.1.budget_open_plain',
|
||||
|
||||
@@ -37,6 +37,8 @@ class InvoiceModelProjectHydratorTest extends TestCase
|
||||
'project.id',
|
||||
'project.name',
|
||||
'project.comment',
|
||||
'project.number',
|
||||
'project.invoice_text',
|
||||
'project.order_date',
|
||||
'project.order_number',
|
||||
'project.meta.foo-project',
|
||||
@@ -55,6 +57,8 @@ class InvoiceModelProjectHydratorTest extends TestCase
|
||||
'project.1.id',
|
||||
'project.1.name',
|
||||
'project.1.comment',
|
||||
'project.1.number',
|
||||
'project.1.invoice_text',
|
||||
'project.1.order_date',
|
||||
'project.1.order_number',
|
||||
'project.1.meta.foo-project',
|
||||
|
||||
@@ -152,6 +152,8 @@ class DebugRendererTest extends TestCase
|
||||
'activity.id',
|
||||
'activity.name',
|
||||
'activity.comment',
|
||||
'activity.number',
|
||||
'activity.invoice_text',
|
||||
'activity.meta.foo-activity',
|
||||
'activity.budget_open',
|
||||
'activity.budget_open_plain',
|
||||
@@ -184,6 +186,8 @@ class DebugRendererTest extends TestCase
|
||||
'activity.1.id',
|
||||
'activity.1.name',
|
||||
'activity.1.comment',
|
||||
'activity.1.number',
|
||||
'activity.1.invoice_text',
|
||||
'activity.1.meta.foo-activity',
|
||||
]);
|
||||
}
|
||||
@@ -201,6 +205,8 @@ class DebugRendererTest extends TestCase
|
||||
'project.id',
|
||||
'project.name',
|
||||
'project.comment',
|
||||
'project.number',
|
||||
'project.invoice_text',
|
||||
'project.order_date',
|
||||
'project.order_number',
|
||||
'project.meta.foo-project',
|
||||
@@ -223,6 +229,8 @@ class DebugRendererTest extends TestCase
|
||||
'project.1.id',
|
||||
'project.1.name',
|
||||
'project.1.comment',
|
||||
'project.1.number',
|
||||
'project.1.invoice_text',
|
||||
'project.1.order_date',
|
||||
'project.1.order_number',
|
||||
'project.1.meta.foo-project',
|
||||
@@ -276,6 +284,7 @@ class DebugRendererTest extends TestCase
|
||||
'entry.end_time',
|
||||
'entry.end_timestamp',
|
||||
'entry.date',
|
||||
'entry.date_process',
|
||||
'entry.week',
|
||||
'entry.weekyear',
|
||||
'entry.user_id',
|
||||
|
||||
@@ -116,9 +116,9 @@ class PdfRendererTest extends KernelTestCase
|
||||
|
||||
$dirs = [
|
||||
__DIR__ . '/../../../templates/invoice/renderer/',
|
||||
__DIR__ . '/../../../var/invoices/',
|
||||
__DIR__ . '/../../../var/invoices_customer/',
|
||||
__DIR__ . '/../../../var/invoices_old/',
|
||||
//__DIR__ . '/../../../var/invoices/',
|
||||
//__DIR__ . '/../../../var/invoices_customer/',
|
||||
//__DIR__ . '/../../../var/invoices_old/',
|
||||
];
|
||||
|
||||
$files = [];
|
||||
|
||||
@@ -200,8 +200,8 @@ class ServiceInvoiceTest extends TestCase
|
||||
self::assertNotNull($query->getBegin());
|
||||
self::assertNotNull($query->getEnd());
|
||||
|
||||
self::assertEquals('2010-01-27T00:00:00+0100', $query->getBegin()->format(DATE_ISO8601));
|
||||
self::assertEquals('2020-11-27T23:59:59+0100', $query->getEnd()->format(DATE_ISO8601));
|
||||
self::assertEquals('2010-01-27T00:00:00+01:00', $query->getBegin()->format(DATE_ATOM));
|
||||
self::assertEquals('2020-11-27T23:59:59+01:00', $query->getEnd()->format(DATE_ATOM));
|
||||
}
|
||||
|
||||
public function testCreateModelsIncludesModelsWithNegativeTotal(): void
|
||||
|
||||
@@ -271,7 +271,7 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
|
||||
$user->setTimezone('Asia/Dubai');
|
||||
$date = $sut->createDate('2019-08-27 16:30:45', $user);
|
||||
$this->assertEquals('2019-08-27T16:30:45+0400', $date->format(DATE_ISO8601));
|
||||
$this->assertEquals('2019-08-27T16:30:45+04:00', $date->format(DATE_ATOM));
|
||||
$this->assertEquals('Asia/Dubai', $date->getTimezone()->getName());
|
||||
|
||||
$date = $sut->createDate('2019-08-27 16:30:45', null);
|
||||
|
||||
@@ -9,11 +9,8 @@
|
||||
|
||||
namespace App\Tests\Widget\Type;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Day;
|
||||
use App\Widget\DataProvider\DailyWorkingTimeChartProvider;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\DailyWorkingTimeChart;
|
||||
use App\Widget\WidgetInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -27,7 +24,7 @@ class DailyWorkingTimeChartTest extends TestCase
|
||||
{
|
||||
public function createSut(): DailyWorkingTimeChart
|
||||
{
|
||||
$repository = $this->createMock(DailyWorkingTimeChartProvider::class);
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
|
||||
$sut = new DailyWorkingTimeChart($repository);
|
||||
$sut->setUser(new User());
|
||||
@@ -65,54 +62,4 @@ class DailyWorkingTimeChartTest extends TestCase
|
||||
self::assertStringStartsWith('DailyWorkingTimeChart_', $options['id']);
|
||||
self::assertEquals('xxx', $options['type']);
|
||||
}
|
||||
|
||||
public function testGetData(): void
|
||||
{
|
||||
$activity = $this->createMock(Activity::class);
|
||||
$activity->method('getId')->willReturn(42);
|
||||
|
||||
$project = $this->createMock(Project::class);
|
||||
$project->method('getId')->willReturn(4711);
|
||||
|
||||
$repository = $this->getMockBuilder(DailyWorkingTimeChartProvider::class)->disableOriginalConstructor()->onlyMethods(['getDailyData'])->getMock();
|
||||
$repository->expects($this->once())->method('getDailyData')->willReturnCallback(function ($begin, $end, $user) use ($activity, $project) {
|
||||
return [
|
||||
[
|
||||
'year' => $begin->format('Y'),
|
||||
'month' => $begin->format('n'),
|
||||
'day' => $begin->format('j'),
|
||||
'rate' => 13.75,
|
||||
'duration' => 1234,
|
||||
'billable' => 1234,
|
||||
'details' => [
|
||||
[
|
||||
'activity' => $activity,
|
||||
'project' => $project,
|
||||
'billable' => 1234,
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
$sut = new DailyWorkingTimeChart($repository);
|
||||
$sut->setUser(new User());
|
||||
$data = $sut->getData($sut->getOptions());
|
||||
self::assertCount(2, $data);
|
||||
self::assertArrayHasKey('activities', $data);
|
||||
self::assertArrayHasKey('data', $data);
|
||||
|
||||
self::assertCount(1, $data['activities']);
|
||||
self::assertArrayHasKey('4711_42', $data['activities']);
|
||||
self::assertCount(2, $data['activities']['4711_42']);
|
||||
self::assertArrayHasKey('activity', $data['activities']['4711_42']);
|
||||
self::assertArrayHasKey('project', $data['activities']['4711_42']);
|
||||
self::assertSame($activity, $data['activities']['4711_42']['activity']);
|
||||
self::assertSame($project, $data['activities']['4711_42']['project']);
|
||||
|
||||
self::assertCount(7, $data['data']);
|
||||
foreach ($data['data'] as $statObj) {
|
||||
self::assertInstanceOf(Day::class, $statObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ includes:
|
||||
- %rootDir%/../phpstan-doctrine/rules.neon
|
||||
- %rootDir%/../phpstan-phpunit/extension.neon
|
||||
- %rootDir%/../phpstan-phpunit/rules.neon
|
||||
- %rootDir%/../phpstan-deprecation-rules/rules.neon
|
||||
|
||||
parameters:
|
||||
level: 9
|
||||
@@ -4132,16 +4133,6 @@ parameters:
|
||||
count: 1
|
||||
path: Widget/Type/AmountYearTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$array of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) expects array\\|ArrayAccess, mixed given\\.$#"
|
||||
count: 1
|
||||
path: Widget/Type/DailyWorkingTimeChartTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$haystack of static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertCount\\(\\) expects Countable\\|iterable, mixed given\\.$#"
|
||||
count: 1
|
||||
path: Widget/Type/DailyWorkingTimeChartTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Widget\\\\Type\\\\DurationYearTest\\:\\:getDefaultOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -1544,10 +1544,18 @@
|
||||
<source>Deactivated</source>
|
||||
<target>Deaktiviert</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="WwesQPU" resname="deactivate">
|
||||
<source>Deactivate</source>
|
||||
<target>Deaktivieren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3WHV.yf" resname="activated">
|
||||
<source>Activated</source>
|
||||
<target>Aktiviert</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="IAx_YiA" resname="activate">
|
||||
<source>Activate</source>
|
||||
<target>Aktivieren</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3Clo55j" resname="about.title">
|
||||
<source>about.title</source>
|
||||
<target>Über Kimai</target>
|
||||
|
||||
@@ -1544,10 +1544,18 @@
|
||||
<source>Deactivated</source>
|
||||
<target>Deactivated</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="WwesQPU" resname="deactivate">
|
||||
<source>Deactivate</source>
|
||||
<target>Deactivate</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3WHV.yf" resname="activated">
|
||||
<source>Activated</source>
|
||||
<target>Activated</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="IAx_YiA" resname="activate">
|
||||
<source>Activate</source>
|
||||
<target>Activate</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="3Clo55j" resname="about.title">
|
||||
<source>about.title</source>
|
||||
<target>About Kimai</target>
|
||||
|
||||
Reference in New Issue
Block a user