Compare commits
12 Commits
2.21.0
...
api-pagina
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233c0e792e | ||
|
|
896e8b42c1 | ||
|
|
bf288bf115 | ||
|
|
13649e146c | ||
|
|
0f9b964b11 | ||
|
|
4076e1c3d3 | ||
|
|
8de54e1fa7 | ||
|
|
1965c35b43 | ||
|
|
3fd770047e | ||
|
|
036b297a76 | ||
|
|
537c120ad9 | ||
|
|
3e3cdc5e69 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: [kevinpapst]
|
||||
custom: ["https://www.kimai.org/", "https://www.kimai.tw/", "https://www.kimai.co.il/"]
|
||||
custom: ["https://www.kimai.org/", "https://www.kimai.cloud/", "https://www.kevinpapst.de/"]
|
||||
|
||||
12
UPGRADING.md
12
UPGRADING.md
@@ -8,6 +8,18 @@ you can upgrade your Kimai installation to the latest stable release.
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [2.14.0](https://github.com/kimai/kimai/releases/tag/2.13.0)
|
||||
|
||||
**DEVELOPERS ONLY**
|
||||
|
||||
The API now uses pagination globally for all collection endpoints. This might break current implementations
|
||||
|
||||
All collections are limited to 50 entries by default (changes in Customer, Project, Activity, User).
|
||||
|
||||
The parameters to influence pagination, page size and page number are `size` (and `pageSize`) and `page`.
|
||||
|
||||
Read the [API pagination docs](https://www.kimai.org/documentation/api-pagination.html) for more infos.
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
|
||||
@@ -4,7 +4,7 @@ fieldset legend.col-form-label {
|
||||
}
|
||||
|
||||
/* See project assignment to teams */
|
||||
fieldset.form-fieldset legend {
|
||||
fieldset.form-fieldset > legend {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ table.dataTable {
|
||||
}
|
||||
th.weekend,
|
||||
td.weekend {
|
||||
background-color: var(--tblr-bg-surface-secondary);
|
||||
background-color: var(--tblr-gray-50);
|
||||
}
|
||||
/* order is important, "today” should overwrite "weekend" therefor later in the file */
|
||||
th.today {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^1.2",
|
||||
"kevinpapst/tabler-bundle": "1.4.2",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
|
||||
350
composer.lock
generated
350
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": "6e2a1e12230f06cb810120b7d1cc64d2",
|
||||
"content-hash": "30e0cd4d74c85f6523a08a515a2b40f5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "azuyalabs/yasumi",
|
||||
@@ -184,24 +184,24 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
"version": "3.4.2",
|
||||
"version": "3.4.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/semver.git",
|
||||
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6"
|
||||
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6",
|
||||
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
|
||||
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.3.2 || ^7.0 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.4",
|
||||
"symfony/phpunit-bridge": "^4.2 || ^5"
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"symfony/phpunit-bridge": "^3 || ^7"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -245,7 +245,7 @@
|
||||
"support": {
|
||||
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||
"issues": "https://github.com/composer/semver/issues",
|
||||
"source": "https://github.com/composer/semver/tree/3.4.2"
|
||||
"source": "https://github.com/composer/semver/tree/3.4.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -261,7 +261,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-07-12T11:35:52+00:00"
|
||||
"time": "2024-09-19T14:15:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dasprid/enum",
|
||||
@@ -585,16 +585,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/dbal",
|
||||
"version": "3.9.0",
|
||||
"version": "3.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/dbal.git",
|
||||
"reference": "d8f68ea6cc00912e5313237130b8c8decf4d28c6"
|
||||
"reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/dbal/zipball/d8f68ea6cc00912e5313237130b8c8decf4d28c6",
|
||||
"reference": "d8f68ea6cc00912e5313237130b8c8decf4d28c6",
|
||||
"url": "https://api.github.com/repos/doctrine/dbal/zipball/d7dc08f98cba352b2bab5d32c5e58f7e745c11a7",
|
||||
"reference": "d7dc08f98cba352b2bab5d32c5e58f7e745c11a7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -610,7 +610,7 @@
|
||||
"doctrine/coding-standard": "12.0.0",
|
||||
"fig/log-test": "^1",
|
||||
"jetbrains/phpstorm-stubs": "2023.1",
|
||||
"phpstan/phpstan": "1.11.7",
|
||||
"phpstan/phpstan": "1.12.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.6",
|
||||
"phpunit/phpunit": "9.6.20",
|
||||
"psalm/plugin-phpunit": "0.18.4",
|
||||
@@ -678,7 +678,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/dbal/issues",
|
||||
"source": "https://github.com/doctrine/dbal/tree/3.9.0"
|
||||
"source": "https://github.com/doctrine/dbal/tree/3.9.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -694,7 +694,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-15T07:34:42+00:00"
|
||||
"time": "2024-09-01T13:49:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
@@ -745,16 +745,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/doctrine-bundle",
|
||||
"version": "2.12.0",
|
||||
"version": "2.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/DoctrineBundle.git",
|
||||
"reference": "5418e811a14724068e95e0ba43353b903ada530f"
|
||||
"reference": "ca59d84b8e63143ce1aed90cdb333ba329d71563"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5418e811a14724068e95e0ba43353b903ada530f",
|
||||
"reference": "5418e811a14724068e95e0ba43353b903ada530f",
|
||||
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca59d84b8e63143ce1aed90cdb333ba329d71563",
|
||||
"reference": "ca59d84b8e63143ce1aed90cdb333ba329d71563",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -845,7 +845,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/DoctrineBundle/issues",
|
||||
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.12.0"
|
||||
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -861,7 +861,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-19T07:20:37+00:00"
|
||||
"time": "2024-09-01T09:46:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/doctrine-migrations-bundle",
|
||||
@@ -3055,16 +3055,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nelmio/api-doc-bundle",
|
||||
"version": "v4.29.3",
|
||||
"version": "v4.30.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nelmio/NelmioApiDocBundle.git",
|
||||
"reference": "61a3f8bb95111fade6eace55c071c43da0cc75d9"
|
||||
"reference": "277fa17b912be31b5170e49f0924b1028058c938"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/61a3f8bb95111fade6eace55c071c43da0cc75d9",
|
||||
"reference": "61a3f8bb95111fade6eace55c071c43da0cc75d9",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/277fa17b912be31b5170e49f0924b1028058c938",
|
||||
"reference": "277fa17b912be31b5170e49f0924b1028058c938",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3165,7 +3165,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nelmio/NelmioApiDocBundle/issues",
|
||||
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.29.3"
|
||||
"source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.30.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3173,7 +3173,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-17T13:03:07+00:00"
|
||||
"time": "2024-09-14T10:54:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nelmio/cors-bundle",
|
||||
@@ -3698,16 +3698,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/math",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/Math.git",
|
||||
"reference": "f0f8cad98624459c540cdd61d2a174d834471773"
|
||||
"reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/Math/zipball/f0f8cad98624459c540cdd61d2a174d834471773",
|
||||
"reference": "f0f8cad98624459c540cdd61d2a174d834471773",
|
||||
"url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc2eb6d1a61b058d5dac77197059db30ee3c8329",
|
||||
"reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3722,8 +3722,7 @@
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpOffice\\Math\\": "src/Math/",
|
||||
"Tests\\PhpOffice\\Math\\": "tests/Math/"
|
||||
"PhpOffice\\Math\\": "src/Math/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
@@ -3745,9 +3744,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/Math/issues",
|
||||
"source": "https://github.com/PHPOffice/Math/tree/0.1.0"
|
||||
"source": "https://github.com/PHPOffice/Math/tree/0.2.0"
|
||||
},
|
||||
"time": "2023-09-25T12:08:20+00:00"
|
||||
"time": "2024-08-12T07:30:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
@@ -3855,16 +3854,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpword",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PHPWord.git",
|
||||
"reference": "e76b701ef538cb749641514fcbc31a68078550fa"
|
||||
"reference": "8392134ce4b5dba65130ba956231a1602b848b7f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/e76b701ef538cb749641514fcbc31a68078550fa",
|
||||
"reference": "e76b701ef538cb749641514fcbc31a68078550fa",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/8392134ce4b5dba65130ba956231a1602b848b7f",
|
||||
"reference": "8392134ce4b5dba65130ba956231a1602b848b7f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3872,7 +3871,7 @@
|
||||
"ext-json": "*",
|
||||
"ext-xml": "*",
|
||||
"php": "^7.1|^8.0",
|
||||
"phpoffice/math": "^0.1"
|
||||
"phpoffice/math": "^0.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"dompdf/dompdf": "^2.0",
|
||||
@@ -3958,22 +3957,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PHPWord/issues",
|
||||
"source": "https://github.com/PHPOffice/PHPWord/tree/1.2.0"
|
||||
"source": "https://github.com/PHPOffice/PHPWord/tree/1.3.0"
|
||||
},
|
||||
"time": "2023-11-30T11:22:23+00:00"
|
||||
"time": "2024-08-30T18:03:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "1.30.0",
|
||||
"version": "1.30.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||
"reference": "5ceb0e384997db59f38774bf79c2a6134252c08f"
|
||||
"reference": "51b95ec8670af41009e2b2b56873bad96682413e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/5ceb0e384997db59f38774bf79c2a6134252c08f",
|
||||
"reference": "5ceb0e384997db59f38774bf79c2a6134252c08f",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51b95ec8670af41009e2b2b56873bad96682413e",
|
||||
"reference": "51b95ec8670af41009e2b2b56873bad96682413e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4005,9 +4004,9 @@
|
||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.0"
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.1"
|
||||
},
|
||||
"time": "2024-08-29T09:54:52+00:00"
|
||||
"time": "2024-09-07T20:13:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
@@ -4371,16 +4370,16 @@
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "79dff0b268932c640297f5208d6298f71855c03e"
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
|
||||
"reference": "79dff0b268932c640297f5208d6298f71855c03e",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4415,9 +4414,9 @@
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.1"
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2024-08-21T13:31:24+00:00"
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/simple-cache",
|
||||
@@ -4682,16 +4681,16 @@
|
||||
},
|
||||
{
|
||||
"name": "setasign/fpdi",
|
||||
"version": "v2.6.0",
|
||||
"version": "v2.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Setasign/FPDI.git",
|
||||
"reference": "a6db878129ec6c7e141316ee71872923e7f1b7ad"
|
||||
"reference": "09a816004fcee9ed3405bd164147e3fdbb79a56f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/a6db878129ec6c7e141316ee71872923e7f1b7ad",
|
||||
"reference": "a6db878129ec6c7e141316ee71872923e7f1b7ad",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/09a816004fcee9ed3405bd164147e3fdbb79a56f",
|
||||
"reference": "09a816004fcee9ed3405bd164147e3fdbb79a56f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4742,7 +4741,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||
"source": "https://github.com/Setasign/FPDI/tree/v2.6.0"
|
||||
"source": "https://github.com/Setasign/FPDI/tree/v2.6.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -4750,7 +4749,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-11T16:03:32+00:00"
|
||||
"time": "2024-09-02T10:17:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spomky-labs/otphp",
|
||||
@@ -7359,20 +7358,20 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
|
||||
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
|
||||
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
|
||||
"reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
@@ -7417,7 +7416,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7433,24 +7432,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-icu",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-icu.git",
|
||||
"reference": "e76343c631b453088e2260ac41dfebe21954de81"
|
||||
"reference": "d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e76343c631b453088e2260ac41dfebe21954de81",
|
||||
"reference": "e76343c631b453088e2260ac41dfebe21954de81",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78",
|
||||
"reference": "d80a05e9904d2c2b9b95929f3e4b5d3a8f418d78",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance and support of other locales than \"en\""
|
||||
@@ -7501,7 +7500,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7517,26 +7516,25 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
"reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c"
|
||||
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c",
|
||||
"reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
|
||||
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"symfony/polyfill-intl-normalizer": "^1.10",
|
||||
"symfony/polyfill-php72": "^1.10"
|
||||
"php": ">=7.2",
|
||||
"symfony/polyfill-intl-normalizer": "^1.10"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
@@ -7585,7 +7583,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7601,24 +7599,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
|
||||
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
@@ -7666,7 +7664,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7682,24 +7680,24 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-31T15:07:36+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
"version": "v1.30.0",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9"
|
||||
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
|
||||
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
|
||||
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -7742,7 +7740,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0"
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7758,7 +7756,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-19T12:35:24+00:00"
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/property-access",
|
||||
@@ -9659,23 +9657,23 @@
|
||||
},
|
||||
{
|
||||
"name": "twig/cssinliner-extra",
|
||||
"version": "v3.12.0",
|
||||
"version": "v3.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/cssinliner-extra.git",
|
||||
"reference": "31fd1f7bc79bf3b7c188316e02d78fdd0fc153e3"
|
||||
"reference": "cef36c444b1cce4c0978d7aebd20427671a918f4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/31fd1f7bc79bf3b7c188316e02d78fdd0fc153e3",
|
||||
"reference": "31fd1f7bc79bf3b7c188316e02d78fdd0fc153e3",
|
||||
"url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/cef36c444b1cce4c0978d7aebd20427671a918f4",
|
||||
"reference": "cef36c444b1cce4c0978d7aebd20427671a918f4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"tijsverkoyen/css-to-inline-styles": "^2.0",
|
||||
"twig/twig": "^3.0"
|
||||
"twig/twig": "^3.13|^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^6.4|^7.0"
|
||||
@@ -9712,7 +9710,7 @@
|
||||
"twig"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/twigphp/cssinliner-extra/tree/v3.12.0"
|
||||
"source": "https://github.com/twigphp/cssinliner-extra/tree/v3.13.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9724,27 +9722,27 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-26T15:29:01+00:00"
|
||||
"time": "2024-09-03T13:08:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/extra-bundle",
|
||||
"version": "v3.12.0",
|
||||
"version": "v3.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/twig-extra-bundle.git",
|
||||
"reference": "a5427976a23c50b98d034d2f4c215ffaaaf5875f"
|
||||
"reference": "21a9a7aa9f79d4493bb6fed4eb2794339f9551f5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/a5427976a23c50b98d034d2f4c215ffaaaf5875f",
|
||||
"reference": "a5427976a23c50b98d034d2f4c215ffaaaf5875f",
|
||||
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/21a9a7aa9f79d4493bb6fed4eb2794339f9551f5",
|
||||
"reference": "21a9a7aa9f79d4493bb6fed4eb2794339f9551f5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.2",
|
||||
"symfony/framework-bundle": "^5.4|^6.4|^7.0",
|
||||
"symfony/twig-bundle": "^5.4|^6.4|^7.0",
|
||||
"twig/twig": "^3.0"
|
||||
"twig/twig": "^3.0|^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"league/commonmark": "^1.0|^2.0",
|
||||
@@ -9786,7 +9784,7 @@
|
||||
"twig"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.12.0"
|
||||
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.13.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9798,27 +9796,27 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-10T10:32:24+00:00"
|
||||
"time": "2024-09-01T20:39:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/inky-extra",
|
||||
"version": "v3.12.0",
|
||||
"version": "v3.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/inky-extra.git",
|
||||
"reference": "cf3abb00b329af9a3732d3bd6168a4e16ae8cd9f"
|
||||
"reference": "60c92c2a435ccd95d7a852229f01098aaf7fbced"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/inky-extra/zipball/cf3abb00b329af9a3732d3bd6168a4e16ae8cd9f",
|
||||
"reference": "cf3abb00b329af9a3732d3bd6168a4e16ae8cd9f",
|
||||
"url": "https://api.github.com/repos/twigphp/inky-extra/zipball/60c92c2a435ccd95d7a852229f01098aaf7fbced",
|
||||
"reference": "60c92c2a435ccd95d7a852229f01098aaf7fbced",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"lorenzo/pinky": "^1.0.5",
|
||||
"php": ">=8.0.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"twig/twig": "^3.0"
|
||||
"twig/twig": "^3.13|^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^6.4|^7.0"
|
||||
@@ -9856,7 +9854,7 @@
|
||||
"twig"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/twigphp/inky-extra/tree/v3.12.0"
|
||||
"source": "https://github.com/twigphp/inky-extra/tree/v3.13.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9868,26 +9866,26 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-10T10:32:24+00:00"
|
||||
"time": "2024-09-03T13:08:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/intl-extra",
|
||||
"version": "v3.12.0",
|
||||
"version": "v3.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/intl-extra.git",
|
||||
"reference": "61e1189333120a475d2b67b93664b8002668fc27"
|
||||
"reference": "1b8d78c5db08bdc61015fd55009d2e84b3aa7e38"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/61e1189333120a475d2b67b93664b8002668fc27",
|
||||
"reference": "61e1189333120a475d2b67b93664b8002668fc27",
|
||||
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/1b8d78c5db08bdc61015fd55009d2e84b3aa7e38",
|
||||
"reference": "1b8d78c5db08bdc61015fd55009d2e84b3aa7e38",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.2",
|
||||
"symfony/intl": "^5.4|^6.4|^7.0",
|
||||
"twig/twig": "^3.10"
|
||||
"twig/twig": "^3.13|^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^6.4|^7.0"
|
||||
@@ -9920,7 +9918,7 @@
|
||||
"twig"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/twigphp/intl-extra/tree/v3.12.0"
|
||||
"source": "https://github.com/twigphp/intl-extra/tree/v3.13.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9932,27 +9930,27 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-10T10:32:24+00:00"
|
||||
"time": "2024-09-03T13:08:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/string-extra",
|
||||
"version": "v3.12.0",
|
||||
"version": "v3.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/string-extra.git",
|
||||
"reference": "0abea0e39f24db0bd847165533d77c93fcbb5025"
|
||||
"reference": "148e869d87cf4bea9d97896ab49e048e4add3310"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/string-extra/zipball/0abea0e39f24db0bd847165533d77c93fcbb5025",
|
||||
"reference": "0abea0e39f24db0bd847165533d77c93fcbb5025",
|
||||
"url": "https://api.github.com/repos/twigphp/string-extra/zipball/148e869d87cf4bea9d97896ab49e048e4add3310",
|
||||
"reference": "148e869d87cf4bea9d97896ab49e048e4add3310",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.2",
|
||||
"symfony/string": "^5.4|^6.4|^7.0",
|
||||
"symfony/translation-contracts": "^1.1|^2|^3",
|
||||
"twig/twig": "^3.0"
|
||||
"twig/twig": "^3.13|^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^6.4|^7.0"
|
||||
@@ -9987,7 +9985,7 @@
|
||||
"unicode"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/twigphp/string-extra/tree/v3.12.0"
|
||||
"source": "https://github.com/twigphp/string-extra/tree/v3.13.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9999,20 +9997,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-10T10:32:24+00:00"
|
||||
"time": "2024-09-03T13:08:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.12.0",
|
||||
"version": "v3.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "4d19472d4ac1838e0b1f0e029ce1fa4040eb34ea"
|
||||
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/4d19472d4ac1838e0b1f0e029ce1fa4040eb34ea",
|
||||
"reference": "4d19472d4ac1838e0b1f0e029ce1fa4040eb34ea",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
|
||||
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10066,7 +10064,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.12.0"
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.14.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10078,7 +10076,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-29T09:51:12+00:00"
|
||||
"time": "2024-09-09T17:55:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
@@ -10940,16 +10938,16 @@
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.63.2",
|
||||
"version": "v3.64.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "9d427f3f14984403a6ae9fc726b61765ca0c005e"
|
||||
"reference": "58dd9c931c785a79739310aef5178928305ffa67"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9d427f3f14984403a6ae9fc726b61765ca0c005e",
|
||||
"reference": "9d427f3f14984403a6ae9fc726b61765ca0c005e",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67",
|
||||
"reference": "58dd9c931c785a79739310aef5178928305ffa67",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11031,7 +11029,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.63.2"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -11039,7 +11037,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-28T10:47:21+00:00"
|
||||
"time": "2024-08-30T23:09:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
@@ -11110,16 +11108,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v5.1.0",
|
||||
"version": "v5.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nikic/PHP-Parser.git",
|
||||
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1"
|
||||
"reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1",
|
||||
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1",
|
||||
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb",
|
||||
"reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11162,9 +11160,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/nikic/PHP-Parser/issues",
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0"
|
||||
"source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0"
|
||||
},
|
||||
"time": "2024-07-01T20:03:41+00:00"
|
||||
"time": "2024-09-15T16:40:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
@@ -11286,16 +11284,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.12.0",
|
||||
"version": "1.12.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "384af967d35b2162f69526c7276acadce534d0e1"
|
||||
"reference": "ffa517cb918591b93acc9b95c0bebdcd0e4538bd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/384af967d35b2162f69526c7276acadce534d0e1",
|
||||
"reference": "384af967d35b2162f69526c7276acadce534d0e1",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/ffa517cb918591b93acc9b95c0bebdcd0e4538bd",
|
||||
"reference": "ffa517cb918591b93acc9b95c0bebdcd0e4538bd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11340,25 +11338,25 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-27T09:18:05+00:00"
|
||||
"time": "2024-09-19T07:58:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-deprecation-rules",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-deprecation-rules.git",
|
||||
"reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26"
|
||||
"reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/fa8cce7720fa782899a0aa97b6a41225d1bb7b26",
|
||||
"reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/f94d246cc143ec5a23da868f8f7e1393b50eaa82",
|
||||
"reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0",
|
||||
"phpstan/phpstan": "^1.11"
|
||||
"phpstan/phpstan": "^1.12"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
@@ -11385,22 +11383,22 @@
|
||||
"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.2.0"
|
||||
"source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.1"
|
||||
},
|
||||
"time": "2024-04-20T06:39:48+00:00"
|
||||
"time": "2024-09-11T15:52:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-doctrine.git",
|
||||
"reference": "4d17bed8a33aa8220c1f2a21a6b14fcdb0e5b02c"
|
||||
"reference": "38db3bad8f1567d7bf64806738d724261f8a2b5c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/4d17bed8a33aa8220c1f2a21a6b14fcdb0e5b02c",
|
||||
"reference": "4d17bed8a33aa8220c1f2a21a6b14fcdb0e5b02c",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/38db3bad8f1567d7bf64806738d724261f8a2b5c",
|
||||
"reference": "38db3bad8f1567d7bf64806738d724261f8a2b5c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11457,9 +11455,9 @@
|
||||
"description": "Doctrine extensions for PHPStan",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/1.5.2"
|
||||
"source": "https://github.com/phpstan/phpstan-doctrine/tree/1.5.3"
|
||||
},
|
||||
"time": "2024-08-23T11:07:15+00:00"
|
||||
"time": "2024-09-01T13:17:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-phpunit",
|
||||
@@ -11564,22 +11562,22 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-symfony",
|
||||
"version": "1.4.8",
|
||||
"version": "1.4.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-symfony.git",
|
||||
"reference": "14eec8c011b856eee4d744a2a3f709db1e1858bd"
|
||||
"reference": "51ab2438fb2695467cf96b58d2f8f28d4dd1e3e9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/14eec8c011b856eee4d744a2a3f709db1e1858bd",
|
||||
"reference": "14eec8c011b856eee4d744a2a3f709db1e1858bd",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/51ab2438fb2695467cf96b58d2f8f28d4dd1e3e9",
|
||||
"reference": "51ab2438fb2695467cf96b58d2f8f28d4dd1e3e9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-simplexml": "*",
|
||||
"php": "^7.2 || ^8.0",
|
||||
"phpstan/phpstan": "^1.11.7"
|
||||
"phpstan/phpstan": "^1.12"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/framework-bundle": "<3.0"
|
||||
@@ -11630,9 +11628,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.4.8"
|
||||
"source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.9"
|
||||
},
|
||||
"time": "2024-08-13T19:43:40+00:00"
|
||||
"time": "2024-09-05T16:15:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
|
||||
@@ -117,10 +117,10 @@ kimai:
|
||||
ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING', 'EVERYONE']
|
||||
# mapping a "role name" to an array of "permission names"
|
||||
roles:
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile']
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting','hours_own_profile']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile','hours_own_profile']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile','hours_own_profile']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile','hours_own_profile']
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ nelmio_api_doc:
|
||||
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
|
||||
- { alias: TeamMember, type: App\Entity\TeamMember, groups: [Team_Entity] }
|
||||
- { alias: TeamMembership, type: App\Entity\TeamMember, groups: [User_Entity] }
|
||||
- { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] }
|
||||
- { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] }
|
||||
areas:
|
||||
path_patterns:
|
||||
- ^/api(?!/doc)
|
||||
|
||||
@@ -17,9 +17,10 @@ nelmio_cors:
|
||||
hosts: []
|
||||
origin_regex: true
|
||||
forced_allow_origin_value: ~
|
||||
# allow_private_network: true
|
||||
paths:
|
||||
'^/api/':
|
||||
allow_origin: ['*']
|
||||
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type']
|
||||
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type', 'Authorization']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
max_age: 3600
|
||||
|
||||
@@ -23,12 +23,14 @@ services:
|
||||
- '../src/API/Model/'
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Event/'
|
||||
- '../src/Form/Model/'
|
||||
- '../src/Model/'
|
||||
- '../src/Repository/Loader/'
|
||||
- '../src/Repository/Paginator/'
|
||||
- '../src/Repository/Query/'
|
||||
- '../src/Repository/Result/'
|
||||
- '../src/Event/'
|
||||
- '../src/Model/'
|
||||
- '../src/WorkingTime/Calculator/'
|
||||
- '../src/Kernel.php'
|
||||
- '../src/Constants.php'
|
||||
|
||||
|
||||
@@ -24,11 +24,15 @@ final class <className> extends AbstractMigration
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->abortIf(true, 'Migration was auto-generated, adapt to your needs before running it.');
|
||||
|
||||
<up>
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->abortIf(true, 'Migration was auto-generated, adapt to your needs before running it.');
|
||||
|
||||
<down>
|
||||
}<override>
|
||||
}
|
||||
|
||||
61
migrations/Version20240920105524.php
Normal file
61
migrations/Version20240920105524.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* @version 2.22
|
||||
*/
|
||||
final class Version20240920105524 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Updates the user preferences for configurable work contract type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$ids = $this->connection->fetchFirstColumn("
|
||||
SELECT DISTINCT user_id
|
||||
FROM kimai2_user_preferences AS kp
|
||||
WHERE kp.value > 0
|
||||
AND kp.name IN ('work_monday', 'work_tuesday', 'work_wednesday', 'work_thursday', 'work_friday', 'work_saturday', 'work_sunday')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM kimai2_user_preferences AS kp2
|
||||
WHERE kp2.user_id = kp.user_id
|
||||
AND kp2.name = 'work_contract_type'
|
||||
);");
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$this->addSql('INSERT INTO kimai2_user_preferences (`user_id`, `name`, `value`) VALUES (:id, :name, :value)', [
|
||||
'id' => $id,
|
||||
'name' => 'work_contract_type',
|
||||
'value' => 'day',
|
||||
]);
|
||||
}
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
$this->preventEmptyMigrationWarning();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql("DELETE FROM kimai2_user_preferences WHERE `name` = 'work_contract_type'");
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
35
phpstan.neon
35
phpstan.neon
@@ -154,16 +154,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/API/UserController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Activity\\\\ActivityStatisticService\\:\\:createStatisticQueryBuilder\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Activity/ActivityStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Activity\\\\ActivityStatisticService\\:\\:getBudgetStatistic\\(\\) should return array\\<int, App\\\\Model\\\\ActivityStatistic\\> but returns array\\<int\\|string, App\\\\Model\\\\ActivityStatistic\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Activity/ActivityStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$statistic of class App\\\\Event\\\\ActivityStatisticEvent constructor expects App\\\\Model\\\\ActivityStatistic, App\\\\Model\\\\ActivityStatistic\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -2094,36 +2084,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/DataTransformer/StringToArrayTransformer.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Extension\\\\DocumentationLinkExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/DocumentationLinkExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Extension\\\\EnhancedChoiceTypeExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/EnhancedChoiceTypeExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Extension\\\\IconExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/IconExtension.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getName\\(\\) on Symfony\\\\Component\\\\Form\\\\FormInterface\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/SelectWithApiDataExtension.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getParent\\(\\) on Symfony\\\\Component\\\\Form\\\\FormInterface\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/SelectWithApiDataExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Extension\\\\SelectWithApiDataExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/SelectWithApiDataExtension.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of method Symfony\\\\Component\\\\Routing\\\\Generator\\\\UrlGeneratorInterface\\:\\:generate\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/build/app.3611eec9.js
Normal file
2
public/build/app.3611eec9.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/app.13d5a0d9.js"
|
||||
"/build/app.3611eec9.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.a7ba9fd6.css"
|
||||
"/build/app.c4fc7cc8.css"
|
||||
]
|
||||
},
|
||||
"app-rtl": {
|
||||
@@ -15,7 +15,7 @@
|
||||
"/build/app-rtl.97153087.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.6516df2d.css"
|
||||
"/build/app-rtl.83358c8a.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -54,7 +54,7 @@
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/calendar.bb629e51.js"
|
||||
"/build/calendar.e52c9d34.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.bb473428.css"
|
||||
@@ -72,10 +72,10 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG",
|
||||
"/build/app.13d5a0d9.js": "sha384-1g1GY1GLRW3qbMZV2ZywyP9MR1Z8N65ayJqb+hyV2xyrhzY3JoVPyFF9IWozaEK4",
|
||||
"/build/app.a7ba9fd6.css": "sha384-GfMYf1vYI9zkkywMoxYk6wpC4UyK185zjDBi16qa0237oztrtOuWdBZsKjWOkyeY",
|
||||
"/build/app.3611eec9.js": "sha384-uBLSZpUnOLWU2TLPGZ0gZDqmiAEa2xrKhWVkHheU+vBjcStIq8ZQg3/iqxLS9Su0",
|
||||
"/build/app.c4fc7cc8.css": "sha384-icbl3mA5CyeJXjF1YzLObStpJHF5qZ5GdqALtvDYN4KPOCqREwGdFWDu27GYkI+U",
|
||||
"/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr",
|
||||
"/build/app-rtl.6516df2d.css": "sha384-5hF5akg98Hnj/bihpbsiTjMyZ3BsAC4+Z1kzqi1O3/U+tgpnQ+JmwsOADDpMld6d",
|
||||
"/build/app-rtl.83358c8a.css": "sha384-eKnlBDnocNJ68nIc2l4AWCLn3oUEEbsRJoZ1TUP3mii+ToGt9xs08ih8wEmvO+Dq",
|
||||
"/build/export-pdf.1442bee7.js": "sha384-C6agvjJnQUsMCxaZ/J7dUZU3cpC7uHKI9ZtnfGpRqYbjyw91YLy1oV3iNYgwYyCF",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.7ef8a0c8.js": "sha384-z4lZ1Ig3+NPigrRyGPZoff0gG3n5PnCjaDJ73ATnzdUYk0lOCEtNV9fg35VRD0vG",
|
||||
@@ -83,7 +83,7 @@
|
||||
"/build/invoice-pdf.6091a528.js": "sha384-yJz/MDBgBpNHNxF7DuSyFbJN9EL7oDJjraoPRB7VYlHMdb1T0ZfTcI0L8VMDB46U",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.80ee190c.js": "sha384-KrQszq859PR0u+KHpGy+RO6K1rcMqCFZx+sNiLVNVZHwEi7RwTXqnYbu8zvfWzUI",
|
||||
"/build/calendar.bb629e51.js": "sha384-uihkC7VSrD8l4gSiveJxM2g8dLFCR9+VQwVmcIb2LlNmA2PKDQ/uV0TN5gzmgpo+",
|
||||
"/build/calendar.e52c9d34.js": "sha384-a6P/nyvUkmPZglNqIfSKEyon791oFKB2+zgfUZHpy1awKi/FBchr5TqeRYupRDQn",
|
||||
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
||||
"/build/dashboard.10118fb0.js": "sha384-pasZR1GYf8/7lMFS9OrsxX8nJ9tSlRYr32fWKUhGoF2mNAGrokLEWdgtbYMRHgFB",
|
||||
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"build/app.css": "/build/app.a7ba9fd6.css",
|
||||
"build/app.js": "/build/app.13d5a0d9.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.6516df2d.css",
|
||||
"build/app.css": "/build/app.c4fc7cc8.css",
|
||||
"build/app.js": "/build/app.3611eec9.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.83358c8a.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.97153087.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.1442bee7.js",
|
||||
@@ -11,7 +11,7 @@
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.6091a528.js",
|
||||
"build/chart.js": "/build/chart.80ee190c.js",
|
||||
"build/calendar.css": "/build/calendar.bb473428.css",
|
||||
"build/calendar.js": "/build/calendar.bb629e51.js",
|
||||
"build/calendar.js": "/build/calendar.e52c9d34.js",
|
||||
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
||||
"build/dashboard.js": "/build/dashboard.10118fb0.js",
|
||||
"build/runtime.js": "/build/runtime.74179306.js",
|
||||
|
||||
@@ -62,14 +62,13 @@ final class ActivityController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name|project', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name, project (default: name)')]
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, ProjectRepository $projectRepository): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ActivityQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($user);
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
if (\is_string($order) && $order !== '') {
|
||||
@@ -110,9 +109,9 @@ final class ActivityController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getActivitiesForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
@@ -52,6 +53,46 @@ abstract class BaseApiController extends AbstractController
|
||||
return DateTimeFactory::createByUser($user);
|
||||
}
|
||||
|
||||
protected function prepareQuery(BaseQuery $query, ParamFetcherInterface $paramFetcher): void
|
||||
{
|
||||
$query->setIsApiCall(true);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
// there is no function has() in ParamFetcherInterface, so we need to use all() and check for the key
|
||||
$all = $paramFetcher->all(true);
|
||||
|
||||
if (\array_key_exists('page', $all)) {
|
||||
$page = $all['page'];
|
||||
if (is_numeric($page)) {
|
||||
$query->setPage((int) $page);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('size', $all)) {
|
||||
$size = $all['size'];
|
||||
if (is_numeric($size)) {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('pageSize', $all)) {
|
||||
$size = $all['pageSize'];
|
||||
if (is_numeric($size)) {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createPaginatedView(Pagination $pagination): View
|
||||
{
|
||||
$results = (array) $pagination->getCurrentPageResults();
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $pagination);
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
protected function addPagination(View $view, Pagination $pagination): void
|
||||
{
|
||||
$view->setHeader('X-Page', (string) $pagination->getCurrentPage());
|
||||
|
||||
@@ -59,14 +59,13 @@ final class CustomerController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name (default: name)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new CustomerQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($user);
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
if (\is_string($order) && $order !== '') {
|
||||
@@ -88,9 +87,9 @@ final class CustomerController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getCustomersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
106
src/API/InvoiceController.php
Normal file
106
src/API/InvoiceController.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?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\API;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\InvoiceRepository;
|
||||
use App\Repository\Query\InvoiceArchiveQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
|
||||
#[Route(path: '/invoices')]
|
||||
#[IsGranted('API')]
|
||||
#[OA\Tag(name: 'Invoice')]
|
||||
final class InvoiceController extends BaseApiController
|
||||
{
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Invoice', 'Invoice_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Invoice'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly InvoiceRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a paginated collection of invoices.
|
||||
*
|
||||
* Needs permission: view_invoice
|
||||
*/
|
||||
#[IsGranted('view_invoice')]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of invoices', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/InvoiceCollection')))]
|
||||
#[Route(methods: ['GET'], path: '', name: 'get_invoices')]
|
||||
#[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: 'customers', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of customer IDs to filter, e.g.: customers[]=1&customers[]=2')]
|
||||
#[Rest\QueryParam(name: 'status', map: true, requirements: 'pending|paid|canceled|new', strict: true, nullable: true, default: [], description: 'Invoice status: pending, paid, canceled, new. Default: all')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository): Response
|
||||
{
|
||||
$query = new InvoiceArchiveQuery();
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
$factory = $this->getDateTimeFactory();
|
||||
|
||||
$begin = $paramFetcher->get('begin');
|
||||
if (\is_string($begin) && $begin !== '') {
|
||||
$query->setBegin($factory->createDateTime($begin));
|
||||
}
|
||||
|
||||
$end = $paramFetcher->get('end');
|
||||
if (\is_string($end) && $end !== '') {
|
||||
$query->setEnd($factory->createDateTime($end));
|
||||
}
|
||||
|
||||
/** @var array<string> $status */
|
||||
$status = $paramFetcher->get('status');
|
||||
if (\is_array($status)) {
|
||||
foreach ($status as $s) {
|
||||
$query->addStatus($s);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<int> $customers */
|
||||
$customers = $paramFetcher->get('customers');
|
||||
foreach ($customerRepository->findByIds(array_unique($customers)) as $customer) {
|
||||
$query->addCustomer($customer);
|
||||
}
|
||||
|
||||
$data = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($data);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one invoice.
|
||||
*
|
||||
* Needs permission: view_invoice
|
||||
*/
|
||||
#[IsGranted('view_invoice')]
|
||||
#[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))]
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_invoice', requirements: ['id' => '\d+'])]
|
||||
public function getAction(Invoice $invoice): Response
|
||||
{
|
||||
$view = new View($invoice, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
@@ -68,14 +68,13 @@ final class ProjectController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name|customer', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name, customer (default: name)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($user);
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
if (\is_string($order) && $order !== '') {
|
||||
@@ -142,9 +141,9 @@ final class ProjectController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getProjectsForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
@@ -96,7 +96,7 @@ final class TimesheetController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||
{
|
||||
$query = new TimesheetQuery(false);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
$seeAll = false;
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
@@ -168,16 +168,6 @@ final class TimesheetController extends BaseApiController
|
||||
$query->addActivity($activity);
|
||||
}
|
||||
|
||||
$page = $paramFetcher->get('page');
|
||||
if (\is_string($page) && $page !== '') {
|
||||
$query->setPage((int) $page);
|
||||
}
|
||||
|
||||
$size = $paramFetcher->get('size');
|
||||
if (\is_string($size) && $size !== '') {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
|
||||
/** @var array<string> $tags */
|
||||
$tags = $paramFetcher->get('tags');
|
||||
if (\is_array($tags) && \count($tags) > 0) {
|
||||
@@ -248,12 +238,8 @@ final class TimesheetController extends BaseApiController
|
||||
$query->setModifiedAfter($factory->createDateTime($modifiedAfter));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getPagerfantaForQuery($query);
|
||||
$results = (array) $data->getCurrentPageResults();
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $data);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
|
||||
@@ -57,10 +57,12 @@ final class UserController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[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: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$visible = $paramFetcher->get('visible');
|
||||
if (\is_string($visible) && $visible !== '') {
|
||||
@@ -82,9 +84,8 @@ final class UserController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getUsersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
|
||||
@@ -117,7 +117,7 @@ class ActivityStatisticService
|
||||
|
||||
/**
|
||||
* @param Activity[] $activities
|
||||
* @return array<int, ActivityStatistic>
|
||||
* @return array<int|string, ActivityStatistic>
|
||||
*/
|
||||
private function getBudgetStatistic(array $activities, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): array
|
||||
{
|
||||
@@ -159,6 +159,9 @@ class ActivityStatisticService
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity[] $activities
|
||||
*/
|
||||
private function createStatisticQueryBuilder(array $activities, \DateTimeInterface $begin = null, ?\DateTimeInterface $end = null): QueryBuilder
|
||||
{
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.21.0';
|
||||
public const VERSION = '2.22.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 22100;
|
||||
public const VERSION_ID = 22200;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -53,7 +53,7 @@ final class ContractController extends AbstractController
|
||||
|
||||
/** @var User $profile */
|
||||
$profile = $values->getUser();
|
||||
if ($this->getUser() !== $profile && !$canChangeUser) {
|
||||
if (!$this->isGranted('hours', $profile)) {
|
||||
throw $this->createAccessDeniedException('Cannot access user contract settings');
|
||||
}
|
||||
|
||||
@@ -79,9 +79,10 @@ final class ContractController extends AbstractController
|
||||
|
||||
$boxConfiguration = new BoxConfiguration();
|
||||
$boxConfiguration->setDecimal(false);
|
||||
$boxConfiguration->setCollapsed($profile->hasWorkHourConfiguration() && $summary->count() > 0);
|
||||
$boxConfiguration->setCollapsed($summary->count() > 0);
|
||||
|
||||
return $this->render('contract/status.html.twig', [
|
||||
'withWorkHourConfiguration' => $profile->hasWorkHourConfiguration(),
|
||||
'box_configuration' => $boxConfiguration,
|
||||
'page_setup' => $page,
|
||||
'decimal' => $boxConfiguration->isDecimal(),
|
||||
|
||||
@@ -32,7 +32,7 @@ final class FavoriteController extends AbstractController
|
||||
{
|
||||
$favoriteRecordService->addFavorite($timesheet);
|
||||
|
||||
return $this->render('favorite/index.html.twig');
|
||||
return $this->redirectToRoute('favorites_timesheets');
|
||||
}
|
||||
|
||||
#[Route(path: '/timesheet/remove/{id}', name: 'favorites_timesheets_remove', methods: ['GET'])]
|
||||
@@ -41,6 +41,6 @@ final class FavoriteController extends AbstractController
|
||||
{
|
||||
$favoriteRecordService->removeFavorite($timesheet);
|
||||
|
||||
return $this->render('favorite/index.html.twig');
|
||||
return $this->redirectToRoute('favorites_timesheets');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Form\AccessTokenForm;
|
||||
use App\Form\Model\TotpActivation;
|
||||
use App\Form\Model\UserContractModel;
|
||||
use App\Form\UserApiPasswordType;
|
||||
use App\Form\UserContractType;
|
||||
use App\Form\UserEditType;
|
||||
@@ -267,7 +268,7 @@ final class ProfileController extends AbstractController
|
||||
#[IsGranted('contract', 'profile')]
|
||||
public function contractAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$form = $this->createForm(UserContractType::class, $profile, [
|
||||
$form = $this->createForm(UserContractType::class, new UserContractModel($profile), [
|
||||
'action' => $this->generateUrl('user_profile_contract', ['username' => $profile->getUserIdentifier()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
@@ -61,9 +61,8 @@ final class TeamController extends AbstractController
|
||||
$table->setReloadEvents('kimai.teamUpdate');
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('teamlead', ['class' => 'd-none badges', 'orderBy' => false]);
|
||||
$table->addColumn('teamlead_avatar', ['title' => 'team.member', 'translation_domain' => 'teams', 'class' => 'd-none d-lg-table-cell avatars avatar-list avatar-list-stacked', 'orderBy' => false]);
|
||||
$table->addColumn('user', ['class' => 'd-none badges', 'orderBy' => false, 'title' => 'user']);
|
||||
$table->addColumn('avatar', ['title' => 'team.member', 'translation_domain' => 'teams', 'class' => 'd-none d-sm-table-cell avatars avatar-list avatar-list-stacked', 'orderBy' => false]);
|
||||
$table->addColumn('amount', ['title' => 'amount', 'class' => 'd-sm-none text-center', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = new PageSetup('teams');
|
||||
|
||||
@@ -25,6 +25,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('invoiceNumber')]
|
||||
#[UniqueEntity('invoiceFilename')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Exporter\Order(['id', 'createdAt', 'invoiceNumber', 'status', 'customer', 'subtotal', 'total', 'tax', 'currency', 'vat', 'dueDays', 'dueDate', 'paymentDate', 'user', 'invoiceFilename', 'customerNumber', 'comment'])]
|
||||
#[Exporter\Expose(name: 'customer', label: 'customer', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()')]
|
||||
#[Exporter\Expose(name: 'customerNumber', label: 'number', exp: 'object.getCustomer() === null ? null : object.getCustomer().getNumber()')]
|
||||
@@ -44,56 +45,78 @@ class Invoice implements EntityWithMetaFields
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\Column(name: 'invoice_number', type: 'string', length: 50, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'invoice.number', type: 'string')]
|
||||
private ?string $invoiceNumber = null;
|
||||
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?Customer $customer = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'date', type: 'datetime')]
|
||||
private ?\DateTime $createdAt = null;
|
||||
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)]
|
||||
private ?string $timezone = null;
|
||||
#[ORM\Column(name: 'total', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'total_rate', type: 'float')]
|
||||
private float $total = 0.00;
|
||||
#[ORM\Column(name: 'tax', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'invoice.tax', type: 'float')]
|
||||
private float $tax = 0.00;
|
||||
#[ORM\Column(name: 'currency', type: 'string', length: 3, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(max: 3)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'currency', type: 'string')]
|
||||
private ?string $currency = null;
|
||||
#[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Range(min: 0, max: 999)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Exporter\Expose(label: 'due_days', type: 'integer')]
|
||||
private int $dueDays = 30;
|
||||
#[ORM\Column(name: 'vat', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Range(min: 0.0, max: 99.99)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'tax_rate', type: 'float')]
|
||||
private float $vat = 0.00;
|
||||
#[ORM\Column(name: 'status', type: 'string', length: 20, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'status', type: 'string')]
|
||||
private string $status = self::STATUS_NEW;
|
||||
#[ORM\Column(name: 'invoice_filename', type: 'string', length: 150, nullable: false)]
|
||||
@@ -103,6 +126,8 @@ class Invoice implements EntityWithMetaFields
|
||||
private ?string $invoiceFilename = null;
|
||||
private bool $localized = false;
|
||||
#[ORM\Column(name: 'payment_date', type: 'date', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?\DateTime $paymentDate = null;
|
||||
/**
|
||||
* Meta fields registered with the invoice
|
||||
@@ -191,7 +216,6 @@ class Invoice implements EntityWithMetaFields
|
||||
public function setModel(InvoiceModel $model): Invoice
|
||||
{
|
||||
$template = $model->getTemplate();
|
||||
|
||||
if ($template === null) {
|
||||
throw new \InvalidArgumentException('Missing invoice template');
|
||||
}
|
||||
@@ -200,19 +224,25 @@ class Invoice implements EntityWithMetaFields
|
||||
throw new \InvalidArgumentException('Missing due-days or vat setting');
|
||||
}
|
||||
|
||||
$this->customer = $model->getCustomer();
|
||||
$this->user = $model->getUser();
|
||||
$this->total = $model->getCalculator()->getTotal();
|
||||
$this->tax = $model->getCalculator()->getTax();
|
||||
$this->invoiceNumber = $model->getInvoiceNumber();
|
||||
$this->currency = $model->getCurrency();
|
||||
$customer = $model->getCustomer();
|
||||
if ($customer === null) {
|
||||
throw new \InvalidArgumentException('Missing invoice customer');
|
||||
}
|
||||
|
||||
$createdAt = $model->getInvoiceDate();
|
||||
$this->createdAt = \DateTime::createFromInterface($createdAt);
|
||||
$this->timezone = $createdAt->getTimezone()->getName();
|
||||
$user = $model->getUser();
|
||||
if ($user === null) {
|
||||
throw new \InvalidArgumentException('Missing invoice user');
|
||||
}
|
||||
|
||||
$this->dueDays = $template->getDueDays();
|
||||
$this->vat = $template->getVat();
|
||||
$this->setCustomer($customer);
|
||||
$this->setUser($user);
|
||||
$this->setTotal($model->getCalculator()->getTotal());
|
||||
$this->setTax($model->getCalculator()->getTax());
|
||||
$this->setInvoiceNumber($model->getInvoiceNumber());
|
||||
$this->setCurrency($model->getCurrency());
|
||||
$this->setCreatedAt($model->getInvoiceDate());
|
||||
$this->setDueDays($template->getDueDays());
|
||||
$this->setVat($template->getVat());
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -380,6 +410,52 @@ class Invoice implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setVat(float $vat): void
|
||||
{
|
||||
$this->vat = $vat;
|
||||
}
|
||||
|
||||
public function setInvoiceNumber(string $invoiceNumber): void
|
||||
{
|
||||
$this->invoiceNumber = $invoiceNumber;
|
||||
}
|
||||
|
||||
public function setCustomer(Customer $customer): void
|
||||
{
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTimeInterface $createdAt): void
|
||||
{
|
||||
$this->createdAt = \DateTime::createFromInterface($createdAt);
|
||||
$this->timezone = $createdAt->getTimezone()->getName();
|
||||
}
|
||||
|
||||
public function setTotal(float $total): void
|
||||
{
|
||||
$this->total = $total;
|
||||
}
|
||||
|
||||
public function setTax(float $tax): void
|
||||
{
|
||||
$this->tax = $tax;
|
||||
}
|
||||
|
||||
public function setCurrency(string $currency): void
|
||||
{
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
public function setDueDays(int $dueDays): void
|
||||
{
|
||||
$this->dueDays = $dueDays;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->id) {
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Entity;
|
||||
use App\Export\Annotation as Exporter;
|
||||
use App\Utils\StringHelper;
|
||||
use App\Validator\Constraints as Constraints;
|
||||
use App\WorkingTime\Mode\WorkingTimeModeNone;
|
||||
use DateTime;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -1117,9 +1118,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
$initial = mb_substr($initial, 0, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
$initial = mb_strtoupper($initial);
|
||||
|
||||
return $initial;
|
||||
return mb_strtoupper($initial);
|
||||
}
|
||||
|
||||
public function getAccountNumber(): ?string
|
||||
@@ -1234,36 +1233,57 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursMonday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_MONDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursTuesday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursWednesday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursThursday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursFriday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursSaturday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursSunday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, 0);
|
||||
@@ -1304,39 +1324,60 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this->getFormattedHoliday(is_numeric($holidays) ? $holidays : 0.0);
|
||||
}
|
||||
|
||||
public function setWorkHoursMonday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursMonday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_MONDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_MONDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursTuesday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursTuesday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursWednesday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursWednesday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursThursday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursThursday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursFriday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursFriday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursSaturday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursSaturday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursSunday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursSunday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setPublicHolidayGroup(null|string $group = null): void
|
||||
@@ -1363,6 +1404,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return (float) number_format((round($holidays * 2) / 2), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function hasContractSettings(): bool
|
||||
{
|
||||
return $this->hasWorkHourConfiguration() || $this->getHolidaysPerYear() !== 0.0;
|
||||
@@ -1370,15 +1414,12 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
|
||||
public function hasWorkHourConfiguration(): bool
|
||||
{
|
||||
return $this->getWorkHoursMonday() !== 0 ||
|
||||
$this->getWorkHoursTuesday() !== 0 ||
|
||||
$this->getWorkHoursWednesday() !== 0 ||
|
||||
$this->getWorkHoursThursday() !== 0 ||
|
||||
$this->getWorkHoursFriday() !== 0 ||
|
||||
$this->getWorkHoursSaturday() !== 0 ||
|
||||
$this->getWorkHoursSunday() !== 0;
|
||||
return $this->getWorkContractMode() !== WorkingTimeModeNone::ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
|
||||
{
|
||||
return match ($dateTime->format('N')) {
|
||||
@@ -1393,6 +1434,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function isWorkDay(\DateTimeInterface $dateTime): bool
|
||||
{
|
||||
return $this->getWorkHoursForDay($dateTime) > 0;
|
||||
@@ -1412,4 +1456,14 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
{
|
||||
$this->supervisor = $supervisor;
|
||||
}
|
||||
|
||||
public function getWorkContractMode(): string
|
||||
{
|
||||
return (string) $this->getPreferenceValue(UserPreference::WORK_CONTRACT_TYPE, WorkingTimeModeNone::ID);
|
||||
}
|
||||
|
||||
public function setWorkContractMode(string $mode): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_CONTRACT_TYPE, $mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
@@ -32,16 +33,24 @@ class UserPreference
|
||||
public const LOCALE = 'locale';
|
||||
public const TIMEZONE = 'timezone';
|
||||
public const FIRST_WEEKDAY = 'first_weekday';
|
||||
public const WORK_HOURS_MONDAY = 'work_monday';
|
||||
public const WORK_HOURS_TUESDAY = 'work_tuesday';
|
||||
public const WORK_HOURS_WEDNESDAY = 'work_wednesday';
|
||||
public const WORK_HOURS_THURSDAY = 'work_thursday';
|
||||
public const WORK_HOURS_FRIDAY = 'work_friday';
|
||||
public const WORK_HOURS_SATURDAY = 'work_saturday';
|
||||
public const WORK_HOURS_SUNDAY = 'work_sunday';
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_MONDAY = WorkingTimeCalculatorDay::WORK_HOURS_MONDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_TUESDAY = WorkingTimeCalculatorDay::WORK_HOURS_TUESDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_WEDNESDAY = WorkingTimeCalculatorDay::WORK_HOURS_WEDNESDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_THURSDAY = WorkingTimeCalculatorDay::WORK_HOURS_THURSDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_FRIDAY = WorkingTimeCalculatorDay::WORK_HOURS_FRIDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_SATURDAY = WorkingTimeCalculatorDay::WORK_HOURS_SATURDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_SUNDAY = WorkingTimeCalculatorDay::WORK_HOURS_SUNDAY;
|
||||
public const WORK_STARTING_DAY = 'work_start_day';
|
||||
public const PUBLIC_HOLIDAY_GROUP = 'public_holiday_group';
|
||||
public const HOLIDAYS_PER_YEAR = 'holidays';
|
||||
public const WORK_CONTRACT_TYPE = 'work_contract_type';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
|
||||
@@ -71,6 +71,9 @@ final class ConfigureMainMenuEvent extends Event
|
||||
return $this->menu->getChild('reporting');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22 - use getMenu() or getAdminMenu() instead
|
||||
*/
|
||||
public function getAppsMenu(): MenuItemModel
|
||||
{
|
||||
return $this->apps;
|
||||
|
||||
@@ -17,7 +17,11 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class UserSubscriber extends AbstractActionsSubscriber
|
||||
{
|
||||
public function __construct(AuthorizationCheckerInterface $auth, UrlGeneratorInterface $urlGenerator, private EventDispatcherInterface $eventDispatcher)
|
||||
public function __construct(
|
||||
AuthorizationCheckerInterface $auth,
|
||||
UrlGeneratorInterface $urlGenerator,
|
||||
private readonly EventDispatcherInterface $eventDispatcher
|
||||
)
|
||||
{
|
||||
parent::__construct($auth, $urlGenerator);
|
||||
}
|
||||
@@ -47,6 +51,10 @@ final class UserSubscriber extends AbstractActionsSubscriber
|
||||
$event->addActionToSubmenu('edit', $id, $action);
|
||||
}
|
||||
|
||||
if ($this->isGranted('hours', $user)) {
|
||||
$event->addActionToSubmenu('report', 'work_times', ['url' => $this->path('user_contract', ['user' => $user->getId()]), 'title' => 'work_times']);
|
||||
}
|
||||
|
||||
if (($event->getUser()->getId() === $user->getId() && $this->isGranted('report:user')) || $this->isGranted('report:other')) {
|
||||
$event->addActionToSubmenu('report', 'weekly', ['url' => $this->path('report_user_week', ['user' => $user->getId()]), 'translation_domain' => 'reporting', 'title' => 'report_user_week']);
|
||||
$event->addActionToSubmenu('report', 'monthly', ['url' => $this->path('report_user_month', ['user' => $user->getId()]), 'translation_domain' => 'reporting', 'title' => 'report_user_month']);
|
||||
|
||||
@@ -43,8 +43,8 @@ final class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
$event->addItem($child);
|
||||
}
|
||||
|
||||
if ($menuEvent->getAppsMenu()->hasChildren()) {
|
||||
$event->addItem($menuEvent->getAppsMenu());
|
||||
if ($menuEvent->getAppsMenu()->hasChildren()) { // @phpstan-ignore-line
|
||||
$event->addItem($menuEvent->getAppsMenu()); // @phpstan-ignore-line
|
||||
}
|
||||
if ($menuEvent->getAdminMenu()->hasChildren()) {
|
||||
$event->addItem($menuEvent->getAdminMenu());
|
||||
|
||||
@@ -94,7 +94,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
$contract = new MenuItemModel('contract', 'work_contract', null, [], 'contract');
|
||||
if ($user->hasContractSettings() || $auth->isGranted('hours_other_profile')) {
|
||||
if ($auth->isGranted('hours', $user)) {
|
||||
$contract->addChild(new MenuItemModel('contract_status', 'work_times', 'user_contract', [], 'work_times'));
|
||||
}
|
||||
|
||||
|
||||
@@ -25,19 +25,11 @@ final class DocumentationLinkExtension extends AbstractTypeExtension
|
||||
return [FormType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['docu_chapter'] = $options['docu_chapter'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsResolver $resolver
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined(['docu_chapter']);
|
||||
|
||||
@@ -26,11 +26,6 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
return [EntityType::class, ChoiceType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (isset($options['selectpicker']) && false === $options['selectpicker']) {
|
||||
@@ -59,7 +54,7 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
|
||||
// there is a very weird logic in vendor/symfony/twig-bridge/Resources/views/Form/form_div_layout.html.twig
|
||||
// in block "block choice_widget_collapsed" that resets "{% set required = false %}", so we fake it into the select
|
||||
if (true === $options['required'] && (!\array_key_exists('size', $options['attr']) || $options['attr']['size'] <= 1)) {
|
||||
if (true === $options['required'] && \is_array($options['attr']) && (!\array_key_exists('size', $options['attr']) || $options['attr']['size'] <= 1)) {
|
||||
$extendedOptions['required'] = 'required';
|
||||
$extendedOptions['placeholder'] = '';
|
||||
}
|
||||
|
||||
@@ -25,11 +25,6 @@ final class IconExtension extends AbstractTypeExtension
|
||||
return [TextType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['icon'] = $options['icon'] ?? null;
|
||||
|
||||
@@ -21,7 +21,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
*/
|
||||
final class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
{
|
||||
public function __construct(private UrlGeneratorInterface $router)
|
||||
public function __construct(private readonly UrlGeneratorInterface $router)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@ final class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
return [EntityType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (!isset($options['api_data'])) {
|
||||
@@ -69,7 +64,7 @@ final class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
$parent = $form->getParent();
|
||||
do {
|
||||
$formPrefixes[] = $parent->getName();
|
||||
} while (($parent = $parent->getParent()) !== null);
|
||||
} while (($parent = $parent?->getParent()) !== null);
|
||||
|
||||
$formPrefix = implode('_', array_reverse($formPrefixes));
|
||||
$formField = $apiData['select'];
|
||||
|
||||
52
src/Form/Model/UserContractModel.php
Normal file
52
src/Form/Model/UserContractModel.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\Form\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
final class UserContractModel
|
||||
{
|
||||
public function __construct(private readonly User $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __set(string $name, mixed $value): void
|
||||
{
|
||||
$method = 'set' . ucfirst($name);
|
||||
|
||||
if (method_exists($this->user, $method)) {
|
||||
$this->user->$method($value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_scalar($value) && $value !== null) {
|
||||
throw new \InvalidArgumentException('Invalid value passed');
|
||||
}
|
||||
|
||||
$this->user->setPreferenceValue($name, $value);
|
||||
}
|
||||
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
$method = 'get' . ucfirst($name);
|
||||
|
||||
if (method_exists($this->user, $method)) {
|
||||
return $this->user->$method();
|
||||
}
|
||||
|
||||
return $this->user->getPreferenceValue($name);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ final class MenuChoiceType extends AbstractType
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
$choices = $this->getChoicesFromMenu($event->getMenu(), $filter);
|
||||
$choices += $this->getChoicesFromMenu($event->getAppsMenu(), $filter);
|
||||
$choices += $this->getChoicesFromMenu($event->getAppsMenu(), $filter); // @phpstan-ignore-line
|
||||
$choices += $this->getChoicesFromMenu($event->getAdminMenu(), $filter);
|
||||
$choices += $this->getChoicesFromMenu($event->getSystemMenu(), $filter);
|
||||
|
||||
|
||||
@@ -9,42 +9,56 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Model\UserContractModel;
|
||||
use App\WorkingTime\Mode\WorkingTimeMode;
|
||||
use App\WorkingTime\Mode\WorkingTimeModeFactory;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<User>
|
||||
* @extends AbstractType<UserContractModel>
|
||||
*/
|
||||
final class UserContractType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly WorkingTimeModeFactory $contractModeService)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['workContractModes'] = $this->contractModeService->getAll();
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$dayOptions = [
|
||||
'translation_domain' => 'system-configuration',
|
||||
'constraints' => [
|
||||
new GreaterThanOrEqual(0)
|
||||
],
|
||||
];
|
||||
$sorted = $this->contractModeService->getAll();
|
||||
|
||||
$builder
|
||||
->add('workHoursMonday', DurationType::class, array_merge(['label' => 'Monday'], $dayOptions))
|
||||
->add('workHoursTuesday', DurationType::class, array_merge(['label' => 'Tuesday'], $dayOptions))
|
||||
->add('workHoursWednesday', DurationType::class, array_merge(['label' => 'Wednesday'], $dayOptions))
|
||||
->add('workHoursThursday', DurationType::class, array_merge(['label' => 'Thursday'], $dayOptions))
|
||||
->add('workHoursFriday', DurationType::class, array_merge(['label' => 'Friday'], $dayOptions))
|
||||
->add('workHoursSaturday', DurationType::class, array_merge(['label' => 'Saturday'], $dayOptions))
|
||||
->add('workHoursSunday', DurationType::class, array_merge(['label' => 'Sunday'], $dayOptions))
|
||||
;
|
||||
usort($sorted, function (WorkingTimeMode $a, WorkingTimeMode $b) {
|
||||
return $a->getOrder() <=> $b->getOrder();
|
||||
});
|
||||
|
||||
$modes = [];
|
||||
foreach ($sorted as $mode) {
|
||||
$modes[$mode->getName()] = $mode->getId();
|
||||
}
|
||||
|
||||
if (\count($modes) > 1) {
|
||||
$builder->add('workContractMode', ChoiceType::class, ['label' => 'work_hours_mode', 'choices' => $modes]);
|
||||
}
|
||||
|
||||
foreach ($modes as $mode) {
|
||||
$this->contractModeService->getMode($mode)->buildForm($builder, $options); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
'data_class' => UserContractModel::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'edit_user_contract',
|
||||
|
||||
@@ -19,9 +19,9 @@ use App\Repository\Query\InvoiceQuery;
|
||||
final class InvoiceModelFactory
|
||||
{
|
||||
public function __construct(
|
||||
private CustomerStatisticService $customerStatisticService,
|
||||
private ProjectStatisticService $projectStatisticService,
|
||||
private ActivityStatisticService $activityStatisticService
|
||||
private readonly CustomerStatisticService $customerStatisticService,
|
||||
private readonly ProjectStatisticService $projectStatisticService,
|
||||
private readonly ActivityStatisticService $activityStatisticService
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -82,12 +82,14 @@ final class ActivityLoader implements LoaderInterface
|
||||
}
|
||||
|
||||
// required on "Activity listing" page for non super-admins
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL a.{id}', 'teams')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('a.id', $activityIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
if (\count($activityIds) > 0) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL a.{id}', 'teams')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('a.id', $activityIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,13 +66,15 @@ final class CustomerLoader implements LoaderInterface
|
||||
$em = $this->entityManager;
|
||||
|
||||
// required where we need to check team permissions, e.g. "Customer listing"
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL c.{id}', 'teams')
|
||||
->from(Customer::class, 'c')
|
||||
->leftJoin('c.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('c.id', $customerIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
if (\count($customerIds) > 0) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL c.{id}', 'teams')
|
||||
->from(Customer::class, 'c')
|
||||
->leftJoin('c.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('c.id', $customerIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
|
||||
// do not load team members or leads by default, because they will only be used on detail pages
|
||||
if ($hydrateTeamMembers) {
|
||||
|
||||
@@ -37,6 +37,7 @@ final class ProjectLoader implements LoaderInterface
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var array<int> $projectIds */
|
||||
$projectIds = array_filter(array_unique(array_map(function (Project $project) {
|
||||
// make sure that this potential doctrine proxy is initialized and filled with all data
|
||||
$project->getName();
|
||||
@@ -48,7 +49,7 @@ final class ProjectLoader implements LoaderInterface
|
||||
|
||||
$em = $this->entityManager;
|
||||
|
||||
if ($this->hydrateTeams) {
|
||||
if ($this->hydrateTeams && \count($projectIds) > 0) {
|
||||
$customerIds = array_filter(array_unique(array_map(function (Project $project) {
|
||||
return $project->getCustomer()->getId();
|
||||
}, $results)), function ($value) { return $value !== null; });
|
||||
@@ -61,13 +62,15 @@ final class ProjectLoader implements LoaderInterface
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL customer.{id}', 'teams')
|
||||
->from(Customer::class, 'customer')
|
||||
->leftJoin('customer.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('customer.id', $customerIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
if (\count($customerIds) > 0) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL customer.{id}', 'teams')
|
||||
->from(Customer::class, 'customer')
|
||||
->leftJoin('customer.teams', 'teams')
|
||||
->andWhere($qb->expr()->in('customer.id', $customerIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
// do not load team members or leads by default, because they will only be used on detail pages
|
||||
|
||||
@@ -45,24 +45,26 @@ final class TeamLoader implements LoaderInterface
|
||||
$em = $this->entityManager;
|
||||
|
||||
// required wherever users are shown, e.g. on "Custom details" page
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL team.{id}', 'members', 'user')
|
||||
->from(Team::class, 'team')
|
||||
->leftJoin('team.members', 'members')
|
||||
->leftJoin('members.user', 'user')
|
||||
->andWhere($qb->expr()->in('team.id', $teamIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
if (\count($teamIds) > 0) {
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL team.{id}', 'members', 'user')
|
||||
->from(Team::class, 'team')
|
||||
->leftJoin('team.members', 'members')
|
||||
->leftJoin('members.user', 'user')
|
||||
->andWhere($qb->expr()->in('team.id', $teamIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
// used in UserTeamProjects widget
|
||||
$qb = $em->createQueryBuilder();
|
||||
/** @var array<Team> $teams */
|
||||
$teams = $qb->select('PARTIAL team.{id}', 'projects')
|
||||
->from(Team::class, 'team')
|
||||
->leftJoin('team.projects', 'projects')
|
||||
->andWhere($qb->expr()->in('team.id', $teamIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
// used in UserTeamProjects widget
|
||||
$qb = $em->createQueryBuilder();
|
||||
/** @var array<Team> $teams */
|
||||
$teams = $qb->select('PARTIAL team.{id}', 'projects')
|
||||
->from(Team::class, 'team')
|
||||
->leftJoin('team.projects', 'projects')
|
||||
->andWhere($qb->expr()->in('team.id', $teamIds))
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
|
||||
$projectIds = [];
|
||||
foreach ($results as $team) {
|
||||
@@ -71,7 +73,7 @@ final class TeamLoader implements LoaderInterface
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->loadCustomer) {
|
||||
if ($this->loadCustomer && \count($projectIds) > 0) {
|
||||
// used in UserTeamProjects widget
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('PARTIAL project.{id}', 'customer')
|
||||
|
||||
@@ -135,8 +135,9 @@ class TagRepository extends EntityRepository
|
||||
$qb1 = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb1->from(Timesheet::class, 't')->select('COUNT(tags)')->innerJoin('t.tags', 'tags')->where('tags.id = tag.id');
|
||||
|
||||
$dql = $qb1->getDQL(); // see https://github.com/phpstan/phpstan-doctrine/issues/606
|
||||
$qb->select('tag.id, tag.name, tag.color, tag.visible');
|
||||
$qb->addSelect('(' . $qb1->getDQL() . ') as amount');
|
||||
$qb->addSelect('(' . $dql . ') as amount');
|
||||
|
||||
$orderBy = $query->getOrderBy();
|
||||
$orderBy = match ($orderBy) {
|
||||
|
||||
@@ -112,11 +112,11 @@ final class FavoriteRecordService
|
||||
public function removeFavorite(Timesheet $timesheet): void
|
||||
{
|
||||
if ($timesheet->getUser() === null) {
|
||||
throw new \InvalidArgumentException('Cannot favorite timesheet without user');
|
||||
throw new \InvalidArgumentException('Cannot remove favorite without user');
|
||||
}
|
||||
|
||||
if ($timesheet->getId() === null) {
|
||||
throw new \InvalidArgumentException('Cannot favorite unsaved timesheet');
|
||||
throw new \InvalidArgumentException('Cannot remove unsaved favorite');
|
||||
}
|
||||
|
||||
$this->removeFavoriteById($timesheet->getUser(), $timesheet->getId());
|
||||
|
||||
@@ -19,11 +19,7 @@ final class Util
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the rate for a hourly rate and a given duration in seconds.
|
||||
*
|
||||
* @param float $hourlyRate
|
||||
* @param int $seconds
|
||||
* @return float
|
||||
* Calculates the rate by an hourly rate and a given duration in seconds.
|
||||
*/
|
||||
public static function calculateRate(float $hourlyRate, int $seconds): float
|
||||
{
|
||||
|
||||
@@ -18,7 +18,6 @@ use App\Utils\JavascriptFormatConverter;
|
||||
use App\Utils\LocaleFormatter;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
@@ -30,7 +29,7 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
||||
private ?LocaleFormatter $formatter = null;
|
||||
private ?string $locale = null;
|
||||
|
||||
public function __construct(private LocaleService $localeService, private Security $security)
|
||||
public function __construct(private readonly LocaleService $localeService)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -111,7 +110,7 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
public function isWeekend(\DateTimeInterface|string|null $dateTime, ?User $user = null): bool
|
||||
public function isWeekend(\DateTimeInterface|string|null $dateTime): bool
|
||||
{
|
||||
if (!$dateTime instanceof \DateTimeInterface) {
|
||||
return false;
|
||||
@@ -119,12 +118,6 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
||||
|
||||
$day = (int) $dateTime->format('N');
|
||||
|
||||
/** @var User|null $tmp */
|
||||
$tmp = $user ?? $this->security->getUser();
|
||||
if ($tmp !== null && $tmp->hasWorkHourConfiguration()) {
|
||||
return !$tmp->isWorkDay($dateTime);
|
||||
}
|
||||
|
||||
return ($day === 6 || $day === 7);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,10 +74,6 @@ final class UserVoter extends Voter
|
||||
return $this->permissionManager->hasRolePermission($user, 'contract_other_profile');
|
||||
}
|
||||
|
||||
if ($attribute === 'hours') {
|
||||
return $this->permissionManager->hasRolePermission($user, 'hours_other_profile');
|
||||
}
|
||||
|
||||
if ($attribute === 'access_user') {
|
||||
return $user->canSeeUser($subject);
|
||||
}
|
||||
|
||||
20
src/WorkingTime/Calculator/WorkingTimeCalculator.php
Normal file
20
src/WorkingTime/Calculator/WorkingTimeCalculator.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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\WorkingTime\Calculator;
|
||||
|
||||
interface WorkingTimeCalculator
|
||||
{
|
||||
/**
|
||||
* @return int seconds
|
||||
*/
|
||||
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int;
|
||||
|
||||
public function isWorkDay(\DateTimeInterface $dateTime): bool;
|
||||
}
|
||||
46
src/WorkingTime/Calculator/WorkingTimeCalculatorDay.php
Normal file
46
src/WorkingTime/Calculator/WorkingTimeCalculatorDay.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\WorkingTime\Calculator;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
final class WorkingTimeCalculatorDay implements WorkingTimeCalculator
|
||||
{
|
||||
public const WORK_HOURS_MONDAY = 'work_monday';
|
||||
public const WORK_HOURS_TUESDAY = 'work_tuesday';
|
||||
public const WORK_HOURS_WEDNESDAY = 'work_wednesday';
|
||||
public const WORK_HOURS_THURSDAY = 'work_thursday';
|
||||
public const WORK_HOURS_FRIDAY = 'work_friday';
|
||||
public const WORK_HOURS_SATURDAY = 'work_saturday';
|
||||
public const WORK_HOURS_SUNDAY = 'work_sunday';
|
||||
|
||||
public function __construct(private readonly User $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
|
||||
{
|
||||
return (int) match ($dateTime->format('N')) {
|
||||
'1' => $this->user->getPreferenceValue(self::WORK_HOURS_MONDAY, 0),
|
||||
'2' => $this->user->getPreferenceValue(self::WORK_HOURS_TUESDAY, 0),
|
||||
'3' => $this->user->getPreferenceValue(self::WORK_HOURS_WEDNESDAY, 0),
|
||||
'4' => $this->user->getPreferenceValue(self::WORK_HOURS_THURSDAY, 0),
|
||||
'5' => $this->user->getPreferenceValue(self::WORK_HOURS_FRIDAY, 0),
|
||||
'6' => $this->user->getPreferenceValue(self::WORK_HOURS_SATURDAY, 0),
|
||||
'7' => $this->user->getPreferenceValue(self::WORK_HOURS_SUNDAY, 0),
|
||||
default => throw new \Exception('Unknown day: ' . $dateTime->format('Y-m-d'))
|
||||
};
|
||||
}
|
||||
|
||||
public function isWorkDay(\DateTimeInterface $dateTime): bool
|
||||
{
|
||||
return $this->getWorkHoursForDay($dateTime) > 0;
|
||||
}
|
||||
}
|
||||
24
src/WorkingTime/Calculator/WorkingTimeCalculatorNone.php
Normal file
24
src/WorkingTime/Calculator/WorkingTimeCalculatorNone.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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\WorkingTime\Calculator;
|
||||
|
||||
final class WorkingTimeCalculatorNone implements WorkingTimeCalculator
|
||||
{
|
||||
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function isWorkDay(\DateTimeInterface $dateTime): bool
|
||||
{
|
||||
// we don't know it, so we must assume every day is a a working day
|
||||
return true;
|
||||
}
|
||||
}
|
||||
48
src/WorkingTime/Mode/WorkingTimeMode.php
Normal file
48
src/WorkingTime/Mode/WorkingTimeMode.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\WorkingTime\Mode;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\UserContractModel;
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculator;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
#[AutoconfigureTag]
|
||||
interface WorkingTimeMode
|
||||
{
|
||||
/**
|
||||
* Short and unique identifier for this mode.
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* @return int<0, 100>
|
||||
*/
|
||||
public function getOrder(): int;
|
||||
|
||||
/**
|
||||
* Translation key for the name of this mode.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface<UserContractModel> $builder
|
||||
* @param array<mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void;
|
||||
|
||||
public function getCalculator(User $user): WorkingTimeCalculator;
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getFormFields(): array;
|
||||
}
|
||||
77
src/WorkingTime/Mode/WorkingTimeModeDay.php
Normal file
77
src/WorkingTime/Mode/WorkingTimeModeDay.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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\WorkingTime\Mode;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculator;
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\LessThanOrEqual;
|
||||
|
||||
class WorkingTimeModeDay implements WorkingTimeMode
|
||||
{
|
||||
final public const ID = 'day';
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $fields = [
|
||||
'workHoursMonday' => 'Monday',
|
||||
'workHoursTuesday' => 'Tuesday',
|
||||
'workHoursWednesday' => 'Wednesday',
|
||||
'workHoursThursday' => 'Thursday',
|
||||
'workHoursFriday' => 'Friday',
|
||||
'workHoursSaturday' => 'Saturday',
|
||||
'workHoursSunday' => 'Sunday',
|
||||
];
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'hours_per_day';
|
||||
}
|
||||
|
||||
public function getFormFields(): array
|
||||
{
|
||||
return array_keys($this->fields);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$durationOptions = [
|
||||
'required' => false,
|
||||
'translation_domain' => 'system-configuration',
|
||||
'constraints' => [
|
||||
new GreaterThanOrEqual(0),
|
||||
new LessThanOrEqual(86400),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($this->fields as $field => $label) {
|
||||
$builder->add($field, DurationType::class, array_merge(['label' => $label], $durationOptions));
|
||||
}
|
||||
}
|
||||
|
||||
public function getCalculator(User $user): WorkingTimeCalculator
|
||||
{
|
||||
return new WorkingTimeCalculatorDay($user);
|
||||
}
|
||||
}
|
||||
55
src/WorkingTime/Mode/WorkingTimeModeFactory.php
Normal file
55
src/WorkingTime/Mode/WorkingTimeModeFactory.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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\WorkingTime\Mode;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
|
||||
|
||||
final class WorkingTimeModeFactory
|
||||
{
|
||||
/**
|
||||
* @param iterable<WorkingTimeMode> $modes
|
||||
*/
|
||||
public function __construct(
|
||||
#[TaggedIterator(WorkingTimeMode::class)]
|
||||
private readonly iterable $modes
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorkingTimeMode[]
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
$modes = [];
|
||||
foreach ($this->modes as $mode) {
|
||||
$modes[] = $mode;
|
||||
}
|
||||
|
||||
return $modes;
|
||||
}
|
||||
|
||||
public function getModeForUser(User $user): WorkingTimeMode
|
||||
{
|
||||
return $this->getMode($user->getWorkContractMode());
|
||||
}
|
||||
|
||||
public function getMode(string $contractMode): WorkingTimeMode
|
||||
{
|
||||
foreach ($this->modes as $mode) {
|
||||
if ($mode->getId() === $contractMode) {
|
||||
return $mode;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Unknown working contract mode: ' . $contractMode);
|
||||
}
|
||||
}
|
||||
50
src/WorkingTime/Mode/WorkingTimeModeNone.php
Normal file
50
src/WorkingTime/Mode/WorkingTimeModeNone.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?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\WorkingTime\Mode;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculator;
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculatorNone;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class WorkingTimeModeNone implements WorkingTimeMode
|
||||
{
|
||||
final public const ID = 'none';
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getFormFields(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
public function getCalculator(User $user): WorkingTimeCalculator
|
||||
{
|
||||
return new WorkingTimeCalculatorNone();
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,15 @@ use App\Event\WorkingTimeYearSummaryEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\WorkingTimeRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\WorkingTime\Mode\WorkingTimeMode;
|
||||
use App\WorkingTime\Mode\WorkingTimeModeFactory;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use App\WorkingTime\Model\Year;
|
||||
use App\WorkingTime\Model\YearPerUserSummary;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal this API and the entire namespace is instable: you should expect changes!
|
||||
* @internal this API and the entire namespace is experimental: expect changes!
|
||||
*/
|
||||
final class WorkingTimeService
|
||||
{
|
||||
@@ -33,11 +35,17 @@ final class WorkingTimeService
|
||||
public function __construct(
|
||||
private readonly TimesheetRepository $timesheetRepository,
|
||||
private readonly WorkingTimeRepository $workingTimeRepository,
|
||||
private readonly EventDispatcherInterface $eventDispatcher
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly WorkingTimeModeFactory $contractModeService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getContractMode(User $user): WorkingTimeMode
|
||||
{
|
||||
return $this->contractModeService->getModeForUser($user);
|
||||
}
|
||||
|
||||
public function getYearSummary(Year $year, \DateTimeInterface $until): YearPerUserSummary
|
||||
{
|
||||
$yearPerUserSummary = new YearPerUserSummary($year);
|
||||
@@ -94,6 +102,7 @@ final class WorkingTimeService
|
||||
|
||||
$stats = null;
|
||||
$firstDay = $user->getWorkStartingDay();
|
||||
$calculator = $this->getContractMode($user)->getCalculator($user);
|
||||
|
||||
foreach ($year->getMonths() as $month) {
|
||||
foreach ($month->getDays() as $day) {
|
||||
@@ -111,7 +120,7 @@ final class WorkingTimeService
|
||||
$result = new WorkingTime($user, $dayDate);
|
||||
|
||||
if ($firstDay === null || $firstDay <= $dayDate) {
|
||||
$result->setExpectedTime($user->getWorkHoursForDay($dayDate));
|
||||
$result->setExpectedTime($calculator->getWorkHoursForDay($dayDate));
|
||||
}
|
||||
|
||||
if (\array_key_exists($key, $stats)) {
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set withWorkHourConfiguration = user.hasWorkHourConfiguration() %}
|
||||
|
||||
{% if not withWorkHourConfiguration %}
|
||||
{% set warningMsg %}
|
||||
{{ 'work_times_should.none_configured'|trans }}
|
||||
@@ -127,8 +125,8 @@
|
||||
{% endif %}
|
||||
<th>{{ 'month'|trans }}</th>
|
||||
<th class="text-end total">{{ 'sum.total'|trans }}</th>
|
||||
<th class="text-end">{{ 'work_times_should'|trans }}</th>
|
||||
<th class="text-end">{{ 'work_times_is'|trans }}</th>
|
||||
<th class="text-end hw-min">{{ 'work_times_should'|trans }}</th>
|
||||
<th class="text-end hw-min">{{ 'work_times_is'|trans }}</th>
|
||||
{% for day in 1..31 %}
|
||||
<th class="text-end contractDay">{% if day < 10 %}0{% endif %}{{ day }}</th>
|
||||
{% endfor %}
|
||||
@@ -167,7 +165,7 @@
|
||||
{% set dayCount = 0 %}
|
||||
{% for day in month.days %}
|
||||
{% set class = 'text-end contractDay text-nowrap' %}
|
||||
{% if day.day is weekend(user) %}
|
||||
{% if day.workingTime.expectedTime == 0 %}
|
||||
{% set class = class ~ ' weekend' %}
|
||||
{% endif %}
|
||||
{% if day.workingTime.expectedTime > 0 and day.workingTime.actualTime == 0 and now > day.day %}
|
||||
@@ -230,51 +228,6 @@
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
{% if withWorkHourConfiguration %}
|
||||
{% set expectedWeekTimes = user.workHoursMonday + user.workHoursTuesday + user.workHoursWednesday + user.workHoursThursday + user.workHoursFriday + user.workHoursSaturday + user.workHoursSunday %}
|
||||
|
||||
{% embed '@theme/embeds/collapsible.html.twig' with {id: 'work_contract_should_preview', border: false, item: {options: {bodyExtraClass: 'border-top'}}} %}
|
||||
{% from "macros/status.html.twig" import status_duration %}
|
||||
{% block title %}
|
||||
{{ 'work_times_should'|trans }}
|
||||
|
||||
{{ status_duration(expectedWeekTimes|duration) }}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Monday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursMonday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Tuesday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursTuesday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Wednesday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursWednesday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Thursday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursThursday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Friday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursFriday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Saturday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursSaturday|duration }}</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">{{ 'Sunday'|trans({}, 'system-configuration') }}</div>
|
||||
<div class="datagrid-content">{{ user.workHoursSunday|duration }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endif %}
|
||||
|
||||
{% for controller in boxes %}
|
||||
{{ render(controller(controller, {'year': year, 'boxConfiguration': box_configuration})) }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
{% block report_content %}
|
||||
{% embed 'reporting/report_by_user_data.html.twig' with {with_links: true} %}
|
||||
{% block period_name %}
|
||||
<th class="text-center text-nowrap{% if column is weekend(user) %} weekend{% endif %}{% if column is today %} today{% endif %}">
|
||||
<th class="text-center text-nowrap{% if column is weekend %} weekend{% endif %}{% if column is today %} today{% endif %}">
|
||||
{{ column|date_weekday }}
|
||||
</th>
|
||||
{% endblock %}
|
||||
{% block upper_left %} {% endblock %}
|
||||
{% block column_classes_project -%}
|
||||
{% if column.date is weekend(user) %} weekend{% endif %}{% if column.date is today %} today{% endif %}
|
||||
{% if column.date is weekend %} weekend{% endif %}{% if column.date is today %} today{% endif %}
|
||||
{%- endblock %}
|
||||
{% block column_classes_activity -%}
|
||||
{% if column.date is weekend(user) %} weekend{% endif %}{% if column.date is today %} today{% endif %}
|
||||
{% if column.date is weekend %} weekend{% endif %}{% if column.date is today %} today{% endif %}
|
||||
{%- endblock %}
|
||||
{% block column_classes_total -%}
|
||||
{% if column is weekend(user) %} weekend{% endif %}
|
||||
{% if column is weekend %} weekend{% endif %}
|
||||
{%- endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block report_content %}
|
||||
{% embed 'reporting/user_list_period_data.html.twig' with {stats: stats, dataType: dataType, period_attribute: period_attribute, subReportRoute: subReportRoute, subReportDate: subReportDate, decimal: decimal} only %}
|
||||
{% block period_name %}
|
||||
<th class="text-center text-nowrap{% if column is today %} today{% endif %}">
|
||||
<th class="text-center text-nowrap{% if column is weekend %} weekend{% endif %}{% if column is today %} today{% endif %}">
|
||||
{{ column|date_weekday }}
|
||||
</th>
|
||||
{% endblock %}
|
||||
@@ -25,6 +25,6 @@
|
||||
{% block duration %}
|
||||
{{ period.totalDuration|duration(decimal) }}
|
||||
{% endblock %}
|
||||
{% block period_cell_class %}{% if period.date is weekend(userPeriod.user) %} weekend{% endif %}{% if period.date is today %} today{% endif %}{% endblock %}
|
||||
{% block period_cell_class %}{% if period.date is weekend %} weekend{% endif %}{% if period.date is today %} today{% endif %}{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,35 +2,23 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
|
||||
{% block datatable_row %}
|
||||
<tr{% if is_granted('edit', entry) %} class="open-edit alternative-link" data-href="{{ path('admin_team_edit', {'id': entry.id}) }}"{% endif %}>
|
||||
<td class="{{ tables.class(dataTable, 'name') }}">
|
||||
{{ widgets.label_name(entry.name, entry.color|colorize(entry.name)) }}
|
||||
</td>
|
||||
<td class="{{ tables.class(dataTable, 'teamlead') }}">
|
||||
{% for member in entry.members %}
|
||||
{% if member.teamlead %}
|
||||
{{ widgets.label_user(member.user) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="{{ tables.class(dataTable, 'teamlead_avatar') }}">
|
||||
{% for member in entry.members|sort((a, b) => b.teamlead <=> a.teamlead) %}
|
||||
{% set user = member.user %}
|
||||
{% set tooltip = member.teamlead ? ('teamlead'|trans ~ ': ' ~ user.displayName) : null %}
|
||||
{{ widgets.user_avatar(user, tooltip, 'avatar-rounded', (member.teamlead ? 'info' : null)) }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="{{ tables.class(dataTable, 'user') }}">
|
||||
{% for member in entry.members %}
|
||||
{% if not member.teamlead %}
|
||||
{{ widgets.label_user(member.user) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="{{ tables.class(dataTable, 'actions') }}">
|
||||
{% set event = actions(app.user, 'team', 'index', {'team': entry}) %}
|
||||
{{ widgets.table_actions(event.actions) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% block datatable_row_attr %}
|
||||
{%- if is_granted('edit', entry) %} class="open-edit alternative-link" data-href="{{ path('admin_team_edit', {'id': entry.id}) }}"{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block datatable_column_value %}
|
||||
{% if column == 'name' %}
|
||||
{{ widgets.label_name(entry.name, entry.color|colorize(entry.name)) }}
|
||||
{% elseif column == 'avatar' %}
|
||||
{% for member in entry.members|sort((a, b) => b.teamlead <=> a.teamlead) %}
|
||||
{% set user = member.user %}
|
||||
{% set tooltip = member.teamlead ? ('teamlead'|trans ~ ': ' ~ user.displayName) : null %}
|
||||
{{ widgets.user_avatar(user, tooltip, 'avatar-rounded', (member.teamlead ? 'info' : null)) }}
|
||||
{% endfor %}
|
||||
{% elseif column == 'amount' %}
|
||||
{{ widgets.badge_counter(entry.members|length) }}
|
||||
{% elseif column == 'actions' %}
|
||||
{% set event = actions(app.user, 'team', 'index', {'team': entry}) %}
|
||||
{{ widgets.table_actions(event.actions) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,20 +2,39 @@
|
||||
|
||||
{% block form_content %}
|
||||
|
||||
<script>
|
||||
function toggleWorkingContractModeForm(value)
|
||||
{
|
||||
document.querySelectorAll('.work_contract_mode').forEach(function (element) {
|
||||
element.classList.add('d-none');
|
||||
});
|
||||
|
||||
// Show the element with the class 'work_contract_mode_' + value
|
||||
var elementToShow = document.querySelector('.work_contract_mode_' + value);
|
||||
if (elementToShow) {
|
||||
elementToShow.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{% form_theme form 'form/horizontal.html.twig' %}
|
||||
|
||||
{{ form_row(form._token) }}
|
||||
|
||||
<fieldset class="form-fieldset form-fieldset-light">
|
||||
<legend>{{ 'work_times_should'|trans }}</legend>
|
||||
{{ form_row(form.workHoursMonday) }}
|
||||
{{ form_row(form.workHoursTuesday) }}
|
||||
{{ form_row(form.workHoursWednesday) }}
|
||||
{{ form_row(form.workHoursThursday) }}
|
||||
{{ form_row(form.workHoursFriday) }}
|
||||
{{ form_row(form.workHoursSaturday) }}
|
||||
{{ form_row(form.workHoursSunday) }}
|
||||
</fieldset>
|
||||
{% if form.workContractMode is defined %}
|
||||
<fieldset class="form-fieldset form-fieldset-light border-0 pb-0">
|
||||
<legend>{{ 'work_times_should'|trans }}</legend>
|
||||
{{ form_row(form.workContractMode, {attr:{'onchange': 'toggleWorkingContractModeForm(this.value)'}}) }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
{% for mode in form.vars.workContractModes %}
|
||||
<fieldset class="form-fieldset form-fieldset-light border-0 work_contract_mode work_contract_mode_{{ mode.getId() }} {% if user.getWorkContractMode() != mode.getId() %}d-none{% endif %}">
|
||||
{% for field in mode.getFormFields() %}
|
||||
{{ form_row(form.children[field]) }}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
<fieldset class="form-fieldset form-fieldset-light">
|
||||
{{- form_rest(form) -}}
|
||||
|
||||
@@ -107,10 +107,10 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
return $client->request($method, $this->createUrl($url), $parameters, [], $server, $content);
|
||||
}
|
||||
|
||||
protected function assertEntityNotFound(string $role, string $url, string $method = 'GET', ?string $message = null): void
|
||||
protected function assertEntityNotFound(string $role, string $url): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$this->request($client, $url, $method);
|
||||
$this->request($client, $url);
|
||||
$this->assertApiException($client->getResponse(), [
|
||||
'code' => Response::HTTP_NOT_FOUND,
|
||||
'message' => 'Not Found'
|
||||
@@ -272,6 +272,25 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
protected static function getExpectedResponseStructure(string $type): array
|
||||
{
|
||||
switch ($type) {
|
||||
case 'Invoice':
|
||||
case 'InvoiceCollection':
|
||||
return [
|
||||
'id' => 'int',
|
||||
'comment' => '@string',
|
||||
'createdAt' => 'datetime',
|
||||
'currency' => 'string',
|
||||
'customer' => ['result' => 'object', 'type' => '@Customer'],
|
||||
'user' => ['result' => 'object', 'type' => '@User'],
|
||||
'dueDays' => 'int',
|
||||
'invoiceNumber' => 'string',
|
||||
'metaFields' => 'array',
|
||||
'paymentDate' => '@datetime',
|
||||
'status' => 'string',
|
||||
'tax' => 'float',
|
||||
'total' => 'float',
|
||||
'vat' => 'float',
|
||||
];
|
||||
|
||||
case 'PageActionItem':
|
||||
return [
|
||||
'id' => 'string',
|
||||
|
||||
@@ -245,7 +245,7 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
|
||||
public function testNotFound(): void
|
||||
{
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/activities/' . PHP_INT_MAX, 'GET', 'App\\Entity\\Activity object not found by the @ParamConverter annotation.');
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/activities/' . PHP_INT_MAX);
|
||||
}
|
||||
|
||||
public function testPostAction(): void
|
||||
|
||||
@@ -38,7 +38,7 @@ class ApiDocControllerTest extends ControllerBaseTest
|
||||
}
|
||||
}
|
||||
|
||||
$expectedKeys = ['Actions', 'Activity', 'Default', 'Customer', 'Project', 'Tag', 'Team', 'Timesheet', 'User'];
|
||||
$expectedKeys = ['Actions', 'Activity', 'Default', 'Customer', 'Project', 'Tag', 'Team', 'Timesheet', 'User', 'Invoice'];
|
||||
$actual = array_keys($tags);
|
||||
|
||||
sort($actual);
|
||||
@@ -70,6 +70,8 @@ class ApiDocControllerTest extends ControllerBaseTest
|
||||
'/api/customers/{id}/meta',
|
||||
'/api/customers/{id}/rates',
|
||||
'/api/customers/{id}/rates/{rateId}',
|
||||
'/api/invoices',
|
||||
'/api/invoices/{id}',
|
||||
'/api/projects',
|
||||
'/api/projects/{id}',
|
||||
'/api/projects/{id}/meta',
|
||||
|
||||
@@ -190,7 +190,7 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
|
||||
public function testNotFound(): void
|
||||
{
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/customers/' . PHP_INT_MAX, 'GET', 'App\\Entity\\Customer object not found by the @ParamConverter annotation.');
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/customers/' . PHP_INT_MAX);
|
||||
}
|
||||
|
||||
public function testPostAction(): void
|
||||
|
||||
125
tests/API/InvoiceControllerTest.php
Normal file
125
tests/API/InvoiceControllerTest.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?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 API;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use App\Entity\User;
|
||||
use App\Tests\API\APIControllerBaseTest;
|
||||
use App\Tests\DataFixtures\InvoiceFixtures;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
*/
|
||||
class InvoiceControllerTest extends APIControllerBaseTest
|
||||
{
|
||||
/**
|
||||
* @return Invoice[]
|
||||
*/
|
||||
protected function importInvoiceFixtures(int $amount, ?array $status = null): array
|
||||
{
|
||||
$fixture = new InvoiceFixtures();
|
||||
$fixture->setAmount($amount);
|
||||
if (\is_array($status)) {
|
||||
$fixture->setStatus($status);
|
||||
}
|
||||
|
||||
return $this->importFixture($fixture);
|
||||
}
|
||||
|
||||
public function testIsSecure(): void
|
||||
{
|
||||
$this->assertUrlIsSecured('/api/invoices');
|
||||
}
|
||||
|
||||
public function testGetCollection(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->importInvoiceFixtures(10);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/api/invoices');
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals(10, \count($result));
|
||||
self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]);
|
||||
}
|
||||
|
||||
public function testGetCollectionWithQuery(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->importInvoiceFixtures(5, [Invoice::STATUS_PENDING]);
|
||||
$this->importInvoiceFixtures(2, [Invoice::STATUS_NEW]);
|
||||
$this->importInvoiceFixtures(7, [Invoice::STATUS_PAID]);
|
||||
$this->importInvoiceFixtures(1, [Invoice::STATUS_CANCELED]);
|
||||
|
||||
$query = ['order' => 'ASC', 'orderBy' => 'name', 'status' => [Invoice::STATUS_PAID]];
|
||||
$this->assertAccessIsGranted($client, '/api/invoices', 'GET', $query);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals(7, \count($result));
|
||||
self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]);
|
||||
}
|
||||
|
||||
public function testGetCollectionWithPagination(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->importInvoiceFixtures(20);
|
||||
|
||||
$query = ['page' => 2, 'size' => 4];
|
||||
$this->assertAccessIsGranted($client, '/api/invoices', 'GET', $query);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals(4, \count($result));
|
||||
$this->assertPagination($client->getResponse(), 2, 4, 5, 20);
|
||||
self::assertApiResponseTypeStructure('InvoiceCollection', $result[0]);
|
||||
}
|
||||
|
||||
public function testGetEntityIsSecure(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
$invoices = $this->importInvoiceFixtures(1);
|
||||
|
||||
$this->assertApiAccessDenied($client, '/api/invoices/' . $invoices[0]->getId());
|
||||
}
|
||||
|
||||
public function testGetEntity(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$invoices = $this->importInvoiceFixtures(1);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/api/invoices/' . $invoices[0]->getId());
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('Invoice', $result);
|
||||
}
|
||||
|
||||
public function testNotFound(): void
|
||||
{
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/invoices/' . PHP_INT_MAX);
|
||||
}
|
||||
}
|
||||
@@ -326,7 +326,7 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
|
||||
public function testNotFound(): void
|
||||
{
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/projects/' . PHP_INT_MAX, 'GET', 'App\\Entity\\Project object not found by the @ParamConverter annotation.');
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/projects/' . PHP_INT_MAX);
|
||||
}
|
||||
|
||||
public function testPostAction(): void
|
||||
|
||||
@@ -83,7 +83,7 @@ class TeamControllerTest extends APIControllerBaseTest
|
||||
|
||||
public function testNotFound(): void
|
||||
{
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/teams/' . PHP_INT_MAX, 'GET', 'App\\Entity\\Team object not found by the @ParamConverter annotation.');
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/teams/' . PHP_INT_MAX);
|
||||
}
|
||||
|
||||
public function testDeleteActionWithUnknownTeam(): void
|
||||
|
||||
@@ -448,7 +448,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
|
||||
public function testGetEntityNotFound(): void
|
||||
{
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/' . PHP_INT_MAX, 'GET', 'App\\Entity\\Timesheet object not found by the @ParamConverter annotation.');
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/' . PHP_INT_MAX);
|
||||
}
|
||||
|
||||
public function testPostAction(): void
|
||||
|
||||
@@ -34,7 +34,7 @@ class PermissionControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/permissions');
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 134);
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 135);
|
||||
$this->assertPageActions($client, [
|
||||
'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'),
|
||||
]);
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Tests\DataFixtures\TeamFixtures;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use App\WorkingTime\Mode\WorkingTimeModeDay;
|
||||
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
|
||||
use Symfony\Component\HttpKernel\HttpKernelBrowser;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||||
@@ -552,14 +553,15 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$user = $this->getUserByRole(User::ROLE_USER);
|
||||
$calculator = (new WorkingTimeModeDay())->getCalculator($user);
|
||||
|
||||
$this->assertEquals(0, $user->getWorkHoursMonday());
|
||||
$this->assertEquals(0, $user->getWorkHoursTuesday());
|
||||
$this->assertEquals(0, $user->getWorkHoursWednesday());
|
||||
$this->assertEquals(0, $user->getWorkHoursThursday());
|
||||
$this->assertEquals(0, $user->getWorkHoursFriday());
|
||||
$this->assertEquals(0, $user->getWorkHoursSaturday());
|
||||
$this->assertEquals(0, $user->getWorkHoursSunday());
|
||||
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('monday this week')));
|
||||
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('tuesday this week')));
|
||||
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('wednesday this week')));
|
||||
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('thursday this week')));
|
||||
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('friday this week')));
|
||||
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('saturday this week')));
|
||||
$this->assertEquals(0, $calculator->getWorkHoursForDay(new \DateTime('sunday this week')));
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=user_contract]')->form();
|
||||
|
||||
@@ -580,13 +582,14 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$this->assertHasFlashSuccess($client);
|
||||
|
||||
$user = $this->getUserByRole(User::ROLE_USER);
|
||||
$calculator = (new WorkingTimeModeDay())->getCalculator($user);
|
||||
|
||||
$this->assertEquals(3600, $user->getWorkHoursMonday());
|
||||
$this->assertEquals(7200, $user->getWorkHoursTuesday());
|
||||
$this->assertEquals(10800, $user->getWorkHoursWednesday());
|
||||
$this->assertEquals(16200, $user->getWorkHoursThursday());
|
||||
$this->assertEquals(18720, $user->getWorkHoursFriday());
|
||||
$this->assertEquals(25140, $user->getWorkHoursSaturday());
|
||||
$this->assertEquals(60, $user->getWorkHoursSunday());
|
||||
$this->assertEquals(3600, $calculator->getWorkHoursForDay(new \DateTime('monday this week')));
|
||||
$this->assertEquals(7200, $calculator->getWorkHoursForDay(new \DateTime('tuesday this week')));
|
||||
$this->assertEquals(10800, $calculator->getWorkHoursForDay(new \DateTime('wednesday this week')));
|
||||
$this->assertEquals(16200, $calculator->getWorkHoursForDay(new \DateTime('thursday this week')));
|
||||
$this->assertEquals(18720, $calculator->getWorkHoursForDay(new \DateTime('friday this week')));
|
||||
$this->assertEquals(25140, $calculator->getWorkHoursForDay(new \DateTime('saturday this week')));
|
||||
$this->assertEquals(60, $calculator->getWorkHoursForDay(new \DateTime('sunday this week')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ use Faker\Factory;
|
||||
*/
|
||||
final class ActivityFixtures implements TestFixture
|
||||
{
|
||||
use FixturesTrait;
|
||||
|
||||
private int $amount = 0;
|
||||
private bool $isGlobal = false;
|
||||
private ?bool $isVisible = null;
|
||||
@@ -127,20 +129,4 @@ final class ActivityFixtures implements TestFixture
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return array<int|string, Project>
|
||||
*/
|
||||
private function getAllProjects(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var Project[] $entries */
|
||||
$entries = $manager->getRepository(Project::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ final class CustomerFixtures implements TestFixture
|
||||
* Will be called prior to persisting the object.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return CustomerFixtures
|
||||
*/
|
||||
public function setCallback(callable $callback): CustomerFixtures
|
||||
{
|
||||
@@ -63,7 +62,6 @@ final class CustomerFixtures implements TestFixture
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Customer[]
|
||||
*/
|
||||
public function load(ObjectManager $manager): array
|
||||
|
||||
91
tests/DataFixtures/FixturesTrait.php
Normal file
91
tests/DataFixtures/FixturesTrait.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?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\DataFixtures;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
trait FixturesTrait
|
||||
{
|
||||
/**
|
||||
* @return array<int, User>
|
||||
*/
|
||||
private function getAllUsers(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var User[] $entries */
|
||||
$entries = $manager->getRepository(User::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
if ($temp->getId() === null) {
|
||||
continue;
|
||||
}
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Customer>
|
||||
*/
|
||||
private function getAllCustomers(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var Customer[] $entries */
|
||||
$entries = $manager->getRepository(Customer::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
if ($temp->getId() === null) {
|
||||
continue;
|
||||
}
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Project>
|
||||
*/
|
||||
private function getAllProjects(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var Project[] $entries */
|
||||
$entries = $manager->getRepository(Project::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
if ($temp->getId() === null) {
|
||||
continue;
|
||||
}
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Activity>
|
||||
*/
|
||||
private function getAllActivities(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var Activity[] $entries */
|
||||
$entries = $manager->getRepository(Activity::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
if ($temp->getId() === null) {
|
||||
continue;
|
||||
}
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
77
tests/DataFixtures/InvoiceFixtures.php
Normal file
77
tests/DataFixtures/InvoiceFixtures.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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\DataFixtures;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
*/
|
||||
class InvoiceFixtures implements TestFixture
|
||||
{
|
||||
use FixturesTrait;
|
||||
|
||||
private int $amount = 50;
|
||||
private array $status = [Invoice::STATUS_CANCELED, Invoice::STATUS_NEW, Invoice::STATUS_PAID, Invoice::STATUS_PENDING];
|
||||
|
||||
public function setAmount(int $amount = 50): void
|
||||
{
|
||||
$this->amount = $amount;
|
||||
}
|
||||
|
||||
public function setStatus(array $status): void
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Invoice[]
|
||||
*/
|
||||
public function load(ObjectManager $manager): array
|
||||
{
|
||||
$created = [];
|
||||
|
||||
$faker = Factory::create();
|
||||
|
||||
$customers = $this->getAllCustomers($manager);
|
||||
$users = $this->getAllUsers($manager);
|
||||
|
||||
for ($i = 0; $i < $this->amount; $i++) {
|
||||
$total = $faker->randomFloat(2, 50, 5000);
|
||||
$vat = $faker->randomFloat(2, 0, 23) / 10;
|
||||
$tax = $total * $vat;
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice->setStatus($this->status[array_rand($this->status)]);
|
||||
$invoice->setTotal($total);
|
||||
$invoice->setVat($vat);
|
||||
$invoice->setTax($tax);
|
||||
$invoice->setCustomer($customers[array_rand($customers)]);
|
||||
|
||||
$prefix = uniqid($i . '_') . '_';
|
||||
$invoice->setInvoiceNumber($prefix . $faker->randomNumber(3));
|
||||
$invoice->setFilename($prefix . $faker->randomNumber(3));
|
||||
$invoice->setCreatedAt($faker->dateTimeBetween('-1 year', 'now'));
|
||||
$invoice->setUser($users[array_rand($users)]);
|
||||
$invoice->setDueDays($faker->randomNumber(2));
|
||||
$invoice->setComment($faker->text(300));
|
||||
$invoice->setCurrency($faker->currencyCode());
|
||||
|
||||
$manager->persist($invoice);
|
||||
$created[] = $invoice;
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
|
||||
return $created;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ use Faker\Factory;
|
||||
class InvoiceTemplateFixtures implements TestFixture
|
||||
{
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return InvoiceTemplate[]
|
||||
*/
|
||||
public function load(ObjectManager $manager): array
|
||||
|
||||
@@ -19,6 +19,8 @@ use Faker\Factory;
|
||||
*/
|
||||
final class ProjectFixtures implements TestFixture
|
||||
{
|
||||
use FixturesTrait;
|
||||
|
||||
private int $amount = 0;
|
||||
private ?bool $isVisible = null;
|
||||
/**
|
||||
@@ -114,20 +116,4 @@ final class ProjectFixtures implements TestFixture
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return array<int|string, Customer>
|
||||
*/
|
||||
private function getAllCustomers(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var Customer[] $entries */
|
||||
$entries = $manager->getRepository(Customer::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Tests\DataFixtures;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@@ -19,22 +18,15 @@ use Doctrine\Persistence\ObjectManager;
|
||||
*/
|
||||
final class TeamFixtures implements TestFixture
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $amount = 0;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $addCustomer = true;
|
||||
use FixturesTrait;
|
||||
|
||||
private int $amount = 0;
|
||||
private bool $addCustomer = true;
|
||||
/**
|
||||
* @var User[]
|
||||
*/
|
||||
private $skipUser = [];
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $addUser = true;
|
||||
private array $skipUser = [];
|
||||
private bool $addUser = true;
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
@@ -44,7 +36,6 @@ final class TeamFixtures implements TestFixture
|
||||
* Will be called prior to persisting the object.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return TeamFixtures
|
||||
*/
|
||||
public function setCallback(callable $callback): TeamFixtures
|
||||
{
|
||||
@@ -87,7 +78,6 @@ final class TeamFixtures implements TestFixture
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Team[]
|
||||
*/
|
||||
public function load(ObjectManager $manager): array
|
||||
@@ -135,36 +125,4 @@ final class TeamFixtures implements TestFixture
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return array<int|string, Customer>
|
||||
*/
|
||||
private function getAllCustomers(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/** @var Customer[] $entries */
|
||||
$entries = $manager->getRepository(Customer::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return array<int|string, User>
|
||||
*/
|
||||
private function getAllUsers(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var User[] $entries */
|
||||
$entries = $manager->getRepository(User::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ use Faker\Factory;
|
||||
*/
|
||||
final class TimesheetFixtures implements TestFixture
|
||||
{
|
||||
use FixturesTrait;
|
||||
|
||||
private int $running = 0;
|
||||
/**
|
||||
* @var Activity[]
|
||||
@@ -307,54 +309,6 @@ final class TimesheetFixtures implements TestFixture
|
||||
return $start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return array<int|string, Activity>
|
||||
*/
|
||||
private function getAllActivities(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var Activity[] $entries */
|
||||
$entries = $manager->getRepository(Activity::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return array<int|string, Project>
|
||||
*/
|
||||
private function getAllProjects(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var Project[] $entries */
|
||||
$entries = $manager->getRepository(Project::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return array<int|string, User>
|
||||
*/
|
||||
private function getAllUsers(ObjectManager $manager): array
|
||||
{
|
||||
$all = [];
|
||||
/** @var User[] $entries */
|
||||
$entries = $manager->getRepository(User::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $start
|
||||
* @param array<Tag> $tagArray
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Entity\UserPreference;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Tests\Security\TestUserEntity;
|
||||
use App\WorkingTime\Mode\WorkingTimeModeDay;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
@@ -74,13 +75,6 @@ class UserTest extends TestCase
|
||||
$user->setAccountNumber('A-058375');
|
||||
self::assertEquals('A-058375', $user->getAccountNumber());
|
||||
|
||||
self::assertEquals(0, $user->getWorkHoursMonday());
|
||||
self::assertEquals(0, $user->getWorkHoursTuesday());
|
||||
self::assertEquals(0, $user->getWorkHoursWednesday());
|
||||
self::assertEquals(0, $user->getWorkHoursThursday());
|
||||
self::assertEquals(0, $user->getWorkHoursFriday());
|
||||
self::assertEquals(0, $user->getWorkHoursSaturday());
|
||||
self::assertEquals(0, $user->getWorkHoursSunday());
|
||||
self::assertEquals(0, $user->getHolidaysPerYear());
|
||||
self::assertFalse($user->hasWorkHourConfiguration());
|
||||
self::assertNull($user->getPublicHolidayGroup());
|
||||
@@ -88,10 +82,23 @@ class UserTest extends TestCase
|
||||
self::assertNull($user->getSupervisor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @group legacy
|
||||
*/
|
||||
public function testWorkContract(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
self::assertEquals(0, $user->getWorkHoursMonday());
|
||||
self::assertEquals(0, $user->getWorkHoursTuesday());
|
||||
self::assertEquals(0, $user->getWorkHoursWednesday());
|
||||
self::assertEquals(0, $user->getWorkHoursThursday());
|
||||
self::assertEquals(0, $user->getWorkHoursFriday());
|
||||
self::assertEquals(0, $user->getWorkHoursSaturday());
|
||||
self::assertEquals(0, $user->getWorkHoursSunday());
|
||||
self::assertFalse($user->hasWorkHourConfiguration());
|
||||
|
||||
$monday = new \DateTime('2023-05-08 12:00:00', new \DateTimeZone('Europe/Berlin'));
|
||||
$tuesday = new \DateTime('2023-05-09 12:00:00', new \DateTimeZone('Europe/Berlin'));
|
||||
$wednesday = new \DateTime('2023-05-10 12:00:00', new \DateTimeZone('Europe/Berlin'));
|
||||
@@ -108,6 +115,8 @@ class UserTest extends TestCase
|
||||
self::assertFalse($user->isWorkDay($saturday));
|
||||
self::assertFalse($user->isWorkDay($sunday));
|
||||
|
||||
$user->setWorkContractMode(WorkingTimeModeDay::ID);
|
||||
|
||||
$user->setWorkHoursMonday(7200);
|
||||
self::assertTrue($user->hasWorkHourConfiguration());
|
||||
$user->setWorkHoursTuesday(7300);
|
||||
|
||||
@@ -18,13 +18,23 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
class ConfigureMainMenuEventTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @deprecated
|
||||
* @group legacy
|
||||
*/
|
||||
public function testLegacy(): void
|
||||
{
|
||||
$sut = new ConfigureMainMenuEvent();
|
||||
|
||||
self::assertNotNull($sut->getAppsMenu());
|
||||
}
|
||||
|
||||
public function testGetterAndSetter(): void
|
||||
{
|
||||
$sut = new ConfigureMainMenuEvent();
|
||||
|
||||
self::assertNotNull($sut->getMenu());
|
||||
self::assertNotNull($sut->getAdminMenu());
|
||||
self::assertNotNull($sut->getAppsMenu());
|
||||
self::assertNotNull($sut->getSystemMenu());
|
||||
|
||||
self::assertNull($sut->getTimesheetMenu());
|
||||
|
||||
@@ -34,15 +34,15 @@ use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractRendererTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* @param string $classname
|
||||
* @return ExportRendererInterface|TimesheetExportInterface
|
||||
* @param class-string $classname
|
||||
*/
|
||||
protected function getAbstractRenderer(string $classname)
|
||||
protected function getAbstractRenderer(string $classname): ExportRendererInterface|TimesheetExportInterface
|
||||
{
|
||||
$languages = [
|
||||
'en' => LocaleService::DEFAULT_SETTINGS
|
||||
@@ -53,7 +53,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
$security->expects($this->any())->method('isGranted')->willReturn(true);
|
||||
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$dateExtension = new LocaleFormatExtensions(new LocaleService($languages), $security);
|
||||
$dateExtension = new LocaleFormatExtensions(new LocaleService($languages));
|
||||
|
||||
$dispatcher = new EventDispatcher();
|
||||
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
|
||||
@@ -61,11 +61,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
return new $classname($translator, $dateExtension, $dispatcher, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportRendererInterface $renderer
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function render(ExportRendererInterface $renderer)
|
||||
protected function render(ExportRendererInterface $renderer): Response
|
||||
{
|
||||
$customer = new Customer('Customer Name');
|
||||
$customer->setNumber('A-0123456789');
|
||||
@@ -198,29 +194,29 @@ class MetaFieldColumnSubscriber implements EventSubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
public function loadTimesheetField(TimesheetMetaDisplayEvent $event)
|
||||
public function loadTimesheetField(TimesheetMetaDisplayEvent $event): void
|
||||
{
|
||||
$event->addField($this->prepareEntity(new TimesheetMeta(), 'foo'));
|
||||
$event->addField($this->prepareEntity(new TimesheetMeta(), 'foo2'));
|
||||
}
|
||||
|
||||
public function loadCustomerField(CustomerMetaDisplayEvent $event)
|
||||
public function loadCustomerField(CustomerMetaDisplayEvent $event): void
|
||||
{
|
||||
$event->addField($this->prepareEntity(new CustomerMeta(), 'customer-foo'));
|
||||
}
|
||||
|
||||
public function loadProjectField(ProjectMetaDisplayEvent $event)
|
||||
public function loadProjectField(ProjectMetaDisplayEvent $event): void
|
||||
{
|
||||
$event->addField($this->prepareEntity(new ProjectMeta(), 'project-foo'));
|
||||
$event->addField($this->prepareEntity(new ProjectMeta(), 'project-foo2')->setIsVisible(false));
|
||||
}
|
||||
|
||||
public function loadActivityField(ActivityMetaDisplayEvent $event)
|
||||
public function loadActivityField(ActivityMetaDisplayEvent $event): void
|
||||
{
|
||||
$event->addField($this->prepareEntity(new ActivityMeta(), 'activity-foo'));
|
||||
}
|
||||
|
||||
private function prepareEntity(MetaTableTypeInterface $meta, string $name)
|
||||
private function prepareEntity(MetaTableTypeInterface $meta, string $name): MetaTableTypeInterface
|
||||
{
|
||||
return $meta
|
||||
->setLabel('Working place')
|
||||
|
||||
@@ -42,7 +42,9 @@ class UserExporterTest extends TestCase
|
||||
$user->setEmail('test@example.com');
|
||||
$user->setEnabled(false);
|
||||
$user->addRole(User::ROLE_TEAMLEAD);
|
||||
$date = $user->getRegisteredAt();
|
||||
|
||||
$date = new \DateTime('2024-09-01 14:45:00');
|
||||
$user->setRegisteredAt($date);
|
||||
|
||||
$sut = new UserExporter($spreadsheetExporter, $annotationExtractor, $userPreferenceExtractor);
|
||||
$spreadsheet = $sut->export([$user], new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT));
|
||||
|
||||
@@ -33,15 +33,15 @@ use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractRendererTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* @param string $classname
|
||||
* @return TimesheetExportInterface
|
||||
* @param class-string $classname
|
||||
*/
|
||||
protected function getAbstractRenderer(string $classname)
|
||||
protected function getAbstractRenderer(string $classname): TimesheetExportInterface
|
||||
{
|
||||
$languages = [
|
||||
'en' => LocaleService::DEFAULT_SETTINGS
|
||||
@@ -55,7 +55,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
$security->expects($this->any())->method('isGranted')->willReturn(true);
|
||||
|
||||
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||
$dateExtension = new LocaleFormatExtensions(new LocaleService($languages), $security);
|
||||
$dateExtension = new LocaleFormatExtensions(new LocaleService($languages));
|
||||
|
||||
$dispatcher = new EventDispatcher();
|
||||
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
|
||||
@@ -63,11 +63,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
return new $classname($translator, $dateExtension, $dispatcher, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetExportInterface $renderer
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function render(TimesheetExportInterface $renderer)
|
||||
protected function render(TimesheetExportInterface $renderer): Response
|
||||
{
|
||||
$customer = new Customer('Customer Name');
|
||||
$customer->setNumber('A-0123456789');
|
||||
@@ -185,29 +181,29 @@ class MetaFieldColumnSubscriber implements EventSubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
public function loadTimesheetField(TimesheetMetaDisplayEvent $event)
|
||||
public function loadTimesheetField(TimesheetMetaDisplayEvent $event): void
|
||||
{
|
||||
$event->addField($this->prepareEntity(new TimesheetMeta(), 'foo'));
|
||||
$event->addField($this->prepareEntity(new TimesheetMeta(), 'foo2'));
|
||||
}
|
||||
|
||||
public function loadCustomerField(CustomerMetaDisplayEvent $event)
|
||||
public function loadCustomerField(CustomerMetaDisplayEvent $event): void
|
||||
{
|
||||
$event->addField($this->prepareEntity(new CustomerMeta(), 'customer-foo'));
|
||||
}
|
||||
|
||||
public function loadProjectField(ProjectMetaDisplayEvent $event)
|
||||
public function loadProjectField(ProjectMetaDisplayEvent $event): void
|
||||
{
|
||||
$event->addField($this->prepareEntity(new ProjectMeta(), 'project-foo'));
|
||||
$event->addField($this->prepareEntity(new ProjectMeta(), 'project-foo2')->setIsVisible(false));
|
||||
}
|
||||
|
||||
public function loadActivityField(ActivityMetaDisplayEvent $event)
|
||||
public function loadActivityField(ActivityMetaDisplayEvent $event): void
|
||||
{
|
||||
$event->addField($this->prepareEntity(new ActivityMeta(), 'activity-foo'));
|
||||
}
|
||||
|
||||
private function prepareEntity(MetaTableTypeInterface $meta, string $name)
|
||||
private function prepareEntity(MetaTableTypeInterface $meta, string $name): MetaTableTypeInterface
|
||||
{
|
||||
return $meta
|
||||
->setLabel('Working place')
|
||||
|
||||
@@ -20,7 +20,7 @@ class CustomerLoaderTest extends AbstractLoaderTest
|
||||
{
|
||||
public function testLoadResults(): void
|
||||
{
|
||||
$em = $this->getEntityManagerMock(1);
|
||||
$em = $this->getEntityManagerMock(0);
|
||||
|
||||
$query = new CustomerQuery();
|
||||
$query->loadTeams();
|
||||
@@ -31,4 +31,22 @@ class CustomerLoaderTest extends AbstractLoaderTest
|
||||
|
||||
$sut->loadResults([$entity]);
|
||||
}
|
||||
|
||||
public function testLoadResultsWithMocks(): void
|
||||
{
|
||||
$em = $this->getEntityManagerMock(1);
|
||||
|
||||
$query = new CustomerQuery();
|
||||
$query->loadTeams();
|
||||
|
||||
$sut = new CustomerLoader($em, $query);
|
||||
|
||||
$entity = $this->createMock(Customer::class);
|
||||
$entity->method('getId')->willReturn(1);
|
||||
|
||||
$entity2 = $this->createMock(Customer::class);
|
||||
$entity->method('getId')->willReturn(2);
|
||||
|
||||
$sut->loadResults([$entity, $entity2]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Intl\Util\IntlTestHelper;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
@@ -55,10 +54,7 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
$user = new User();
|
||||
$user->setTimezone('Europe/Vienna');
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects($this->any())->method('getUser')->willReturn($user);
|
||||
|
||||
$sut = new LocaleFormatExtensions(new LocaleService($languageSettings), $security);
|
||||
$sut = new LocaleFormatExtensions(new LocaleService($languageSettings));
|
||||
$sut->setLocale($locale);
|
||||
|
||||
return $sut;
|
||||
@@ -502,9 +498,7 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
],
|
||||
];
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
|
||||
$sut = new LocaleFormatExtensions(new LocaleService($this->localeEn), $security);
|
||||
$sut = new LocaleFormatExtensions(new LocaleService($this->localeEn));
|
||||
$sut->setLocale('en');
|
||||
|
||||
self::assertEquals($expected, $sut->getJavascriptConfiguration());
|
||||
@@ -586,22 +580,6 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
self::assertFalse($sut->isWeekend(new \DateTime('first friday this month')));
|
||||
self::assertTrue($sut->isWeekend(new \DateTime('first saturday this month')));
|
||||
self::assertTrue($sut->isWeekend(new \DateTime('first sunday this month')));
|
||||
|
||||
// seconds case: a user with work-hour configuration
|
||||
$user = new User();
|
||||
$user->setWorkHoursTuesday(1);
|
||||
$user->setWorkHoursThursday(1);
|
||||
$user->setWorkHoursSaturday(1);
|
||||
$user->setWorkHoursSunday(1);
|
||||
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
self::assertTrue($sut->isWeekend(new \DateTime('first monday this month'), $user));
|
||||
self::assertFalse($sut->isWeekend(new \DateTime('first tuesday this month'), $user));
|
||||
self::assertTrue($sut->isWeekend(new \DateTime('first wednesday this month'), $user));
|
||||
self::assertFalse($sut->isWeekend(new \DateTime('first thursday this month'), $user));
|
||||
self::assertTrue($sut->isWeekend(new \DateTime('first friday this month'), $user));
|
||||
self::assertFalse($sut->isWeekend(new \DateTime('first saturday this month'), $user));
|
||||
self::assertFalse($sut->isWeekend(new \DateTime('first sunday this month'), $user));
|
||||
}
|
||||
|
||||
protected function getTimesheet($seconds): Timesheet
|
||||
|
||||
@@ -41,9 +41,8 @@ class PaginationTest extends TestCase
|
||||
$query = new TimesheetQuery();
|
||||
$query->setPage(3);
|
||||
$query->setPageSize(1);
|
||||
$query->setIsApiCall(true);
|
||||
$sut = new Pagination(new ArrayAdapter([1, 2, 3, 4, 5]), $query);
|
||||
$this->assertFalse($sut->getNormalizeOutOfRangePages());
|
||||
$this->assertTrue($sut->getNormalizeOutOfRangePages());
|
||||
$this->assertEquals(1, $sut->getMaxPerPage());
|
||||
$this->assertEquals(3, $sut->getCurrentPage());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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\WorkingTime\Calculator;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Calculator\WorkingTimeCalculatorDay
|
||||
*/
|
||||
class WorkingTimeCalculatorDayTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$monday = new \DateTimeImmutable('monday last week 12:00:00');
|
||||
$tuesday = $monday->modify('+1 day');
|
||||
$wednesday = $tuesday->modify('+1 day');
|
||||
$thursday = $wednesday->modify('+1 day');
|
||||
$friday = $thursday->modify('+1 day');
|
||||
$saturday = $friday->modify('+1 day');
|
||||
$sunday = $saturday->modify('+1 day');
|
||||
|
||||
$user = new User();
|
||||
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_MONDAY, 0);
|
||||
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_TUESDAY, 3600);
|
||||
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_WEDNESDAY, 0);
|
||||
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_THURSDAY, 7200);
|
||||
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_FRIDAY, 0);
|
||||
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_SATURDAY, 1800);
|
||||
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_SUNDAY, 9000);
|
||||
|
||||
$sut = new WorkingTimeCalculatorDay($user);
|
||||
|
||||
self::assertFalse($sut->isWorkDay($monday));
|
||||
self::assertTrue($sut->isWorkDay($tuesday));
|
||||
self::assertFalse($sut->isWorkDay($wednesday));
|
||||
self::assertTrue($sut->isWorkDay($thursday));
|
||||
self::assertFalse($sut->isWorkDay($friday));
|
||||
self::assertTrue($sut->isWorkDay($saturday));
|
||||
self::assertTrue($sut->isWorkDay($sunday));
|
||||
|
||||
self::assertEquals(0, $sut->getWorkHoursForDay($monday));
|
||||
self::assertEquals(3600, $sut->getWorkHoursForDay($tuesday));
|
||||
self::assertEquals(0, $sut->getWorkHoursForDay($wednesday));
|
||||
self::assertEquals(7200, $sut->getWorkHoursForDay($thursday));
|
||||
self::assertEquals(0, $sut->getWorkHoursForDay($friday));
|
||||
self::assertEquals(1800, $sut->getWorkHoursForDay($saturday));
|
||||
self::assertEquals(9000, $sut->getWorkHoursForDay($sunday));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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\WorkingTime\Calculator;
|
||||
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculatorNone;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Calculator\WorkingTimeCalculatorNone
|
||||
*/
|
||||
class WorkingTimeCalculatorNoneTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$date = new \DateTime();
|
||||
$sut = new WorkingTimeCalculatorNone();
|
||||
self::assertTrue($sut->isWorkDay($date));
|
||||
self::assertEquals(0, $sut->getWorkHoursForDay($date));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user