Release 2.0.27 (#4107)
This commit is contained in:
@@ -318,7 +318,7 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
this._applyDateToField(newBegin.plus({seconds: seconds}), null, this._endTime);
|
||||
} else if (begin === null && end !== null) {
|
||||
this._applyDateToField(end.minus({seconds: seconds}), this._beginDate, this._beginTime);
|
||||
} else if (begin !== null && seconds > 0) {
|
||||
} else if (begin !== null && seconds >= 0) {
|
||||
this._applyDateToField(begin.plus({seconds: seconds}), null, this._endTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,3 +13,5 @@
|
||||
--bs-gray-400: var(--tblr-border-color);
|
||||
--bs-gray-200: var(--tblr-gray-200);
|
||||
}
|
||||
|
||||
.fc-day-sat, .fc-day-sun { background-color: var(--tblr-bg-surface-secondary); }
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^0.21",
|
||||
"kevinpapst/tabler-bundle": "^0.22",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
|
||||
172
composer.lock
generated
172
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": "ff7a790b502d29429fee1326938b0f69",
|
||||
"content-hash": "ea0cac01bf542aa09f23052e82090ec8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "azuyalabs/yasumi",
|
||||
@@ -2238,22 +2238,22 @@
|
||||
},
|
||||
{
|
||||
"name": "jms/serializer",
|
||||
"version": "3.24.0",
|
||||
"version": "3.25.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/schmittjoh/serializer.git",
|
||||
"reference": "bd18f0a6ee4d75515b33091afa3333e100e0be43"
|
||||
"reference": "d1384d37926a32b38731c1d9fed6dc71ad630d8b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/schmittjoh/serializer/zipball/bd18f0a6ee4d75515b33091afa3333e100e0be43",
|
||||
"reference": "bd18f0a6ee4d75515b33091afa3333e100e0be43",
|
||||
"url": "https://api.github.com/repos/schmittjoh/serializer/zipball/d1384d37926a32b38731c1d9fed6dc71ad630d8b",
|
||||
"reference": "d1384d37926a32b38731c1d9fed6dc71ad630d8b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/annotations": "^1.13 || ^2.0",
|
||||
"doctrine/instantiator": "^1.0.3 || ^2.0",
|
||||
"doctrine/lexer": "^1.1 || ^2",
|
||||
"doctrine/lexer": "^2",
|
||||
"jms/metadata": "^2.6",
|
||||
"php": "^7.2||^8.0",
|
||||
"phpstan/phpdoc-parser": "^0.4 || ^0.5 || ^1.0"
|
||||
@@ -2322,7 +2322,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/schmittjoh/serializer/issues",
|
||||
"source": "https://github.com/schmittjoh/serializer/tree/3.24.0"
|
||||
"source": "https://github.com/schmittjoh/serializer/tree/3.25.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2330,20 +2330,20 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-06-04T20:28:00+00:00"
|
||||
"time": "2023-06-09T15:44:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jms/serializer-bundle",
|
||||
"version": "5.2.1",
|
||||
"version": "5.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/schmittjoh/JMSSerializerBundle.git",
|
||||
"reference": "c772704a0b3cb772fa391ff5ac7d36fd6cecebf4"
|
||||
"reference": "3279738a958454793ca1e318a7dab6cfcff60124"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/schmittjoh/JMSSerializerBundle/zipball/c772704a0b3cb772fa391ff5ac7d36fd6cecebf4",
|
||||
"reference": "c772704a0b3cb772fa391ff5ac7d36fd6cecebf4",
|
||||
"url": "https://api.github.com/repos/schmittjoh/JMSSerializerBundle/zipball/3279738a958454793ca1e318a7dab6cfcff60124",
|
||||
"reference": "3279738a958454793ca1e318a7dab6cfcff60124",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2409,7 +2409,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/schmittjoh/JMSSerializerBundle/issues",
|
||||
"source": "https://github.com/schmittjoh/JMSSerializerBundle/tree/5.2.1"
|
||||
"source": "https://github.com/schmittjoh/JMSSerializerBundle/tree/5.3.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2417,20 +2417,20 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-02-05T11:03:45+00:00"
|
||||
"time": "2023-06-13T14:47:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "kevinpapst/tabler-bundle",
|
||||
"version": "0.21",
|
||||
"version": "0.22",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kevinpapst/TablerBundle.git",
|
||||
"reference": "64a291914df3b6088744c71b4584b75ecc4c541a"
|
||||
"reference": "fdc3c182f13dec439ce5110dededba2895518f8f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/64a291914df3b6088744c71b4584b75ecc4c541a",
|
||||
"reference": "64a291914df3b6088744c71b4584b75ecc4c541a",
|
||||
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/fdc3c182f13dec439ce5110dededba2895518f8f",
|
||||
"reference": "fdc3c182f13dec439ce5110dededba2895518f8f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2479,7 +2479,7 @@
|
||||
"description": "Admin/Backend theme bundle for Symfony based on Tabler.io",
|
||||
"support": {
|
||||
"issues": "https://github.com/kevinpapst/TablerBundle/issues",
|
||||
"source": "https://github.com/kevinpapst/TablerBundle/tree/0.21"
|
||||
"source": "https://github.com/kevinpapst/TablerBundle/tree/0.22"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2491,7 +2491,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-06-06T21:31:13+00:00"
|
||||
"time": "2023-06-15T06:00:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laminas/laminas-escaper",
|
||||
@@ -2697,33 +2697,36 @@
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
"version": "v2.4.0",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3"
|
||||
"reference": "b46726e666b5d2ad32959ae9492ee1034e798162"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3",
|
||||
"reference": "3fa72e4c71a43f9e9118752a5c90e476a8dc9eb3",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/b46726e666b5d2ad32959ae9492ee1034e798162",
|
||||
"reference": "b46726e666b5d2ad32959ae9492ee1034e798162",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"myclabs/php-enum": "^1.5",
|
||||
"php": "^8.0",
|
||||
"psr/http-message": "^1.0"
|
||||
"ext-zlib": "*",
|
||||
"php-64bit": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-zip": "*",
|
||||
"friendsofphp/php-cs-fixer": "^3.9",
|
||||
"guzzlehttp/guzzle": "^6.5.3 || ^7.2.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.16",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.4",
|
||||
"phpunit/phpunit": "^8.5.8 || ^9.4.2",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"vimeo/psalm": "^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"guzzlehttp/psr7": "^2.4",
|
||||
"psr/http-message": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -2759,7 +2762,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/v2.4.0"
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.0.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2771,7 +2774,7 @@
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2022-12-08T12:29:14+00:00"
|
||||
"time": "2023-04-19T19:51:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
@@ -3161,69 +3164,6 @@
|
||||
],
|
||||
"time": "2023-03-08T13:26:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/php-enum",
|
||||
"version": "1.8.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/php-enum.git",
|
||||
"reference": "a867478eae49c9f59ece437ae7f9506bfaa27483"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/a867478eae49c9f59ece437ae7f9506bfaa27483",
|
||||
"reference": "a867478eae49c9f59ece437ae7f9506bfaa27483",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^7.3 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"squizlabs/php_codesniffer": "1.*",
|
||||
"vimeo/psalm": "^4.6.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MyCLabs\\Enum\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"stubs/Stringable.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP Enum contributors",
|
||||
"homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "PHP Enum implementation",
|
||||
"homepage": "http://github.com/myclabs/php-enum",
|
||||
"keywords": [
|
||||
"enum"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/myclabs/php-enum/issues",
|
||||
"source": "https://github.com/myclabs/php-enum/tree/1.8.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/mnapoli",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-08-04T09:53:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nelmio/api-doc-bundle",
|
||||
"version": "v4.11.1",
|
||||
@@ -3844,16 +3784,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "1.28.0",
|
||||
"version": "1.29.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a"
|
||||
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a",
|
||||
"reference": "6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0",
|
||||
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3871,7 +3811,7 @@
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"ezyang/htmlpurifier": "^4.15",
|
||||
"maennchen/zipstream-php": "^2.1",
|
||||
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
@@ -3883,12 +3823,12 @@
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
"dompdf/dompdf": "^1.0 || ^2.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"mitoteam/jpgraph": "^10.2.4",
|
||||
"mitoteam/jpgraph": "^10.3",
|
||||
"mpdf/mpdf": "^8.1.1",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpstan/phpstan": "^1.1",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^8.5 || ^9.0",
|
||||
"phpunit/phpunit": "^8.5 || ^9.0 || ^10.0",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"tecnickcom/tcpdf": "^6.5"
|
||||
},
|
||||
@@ -3943,9 +3883,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.28.0"
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0"
|
||||
},
|
||||
"time": "2023-02-25T12:24:49+00:00"
|
||||
"time": "2023-06-14T22:48:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpword",
|
||||
@@ -10717,16 +10657,16 @@
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.22.0",
|
||||
"version": "v1.23.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/FakerPHP/Faker.git",
|
||||
"reference": "f85772abd508bd04e20bb4b1bbe260a68d0066d2"
|
||||
"reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/FakerPHP/Faker/zipball/f85772abd508bd04e20bb4b1bbe260a68d0066d2",
|
||||
"reference": "f85772abd508bd04e20bb4b1bbe260a68d0066d2",
|
||||
"url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01",
|
||||
"reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10779,9 +10719,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/FakerPHP/Faker/issues",
|
||||
"source": "https://github.com/FakerPHP/Faker/tree/v1.22.0"
|
||||
"source": "https://github.com/FakerPHP/Faker/tree/v1.23.0"
|
||||
},
|
||||
"time": "2023-05-14T12:31:37+00:00"
|
||||
"time": "2023-06-12T08:44:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
@@ -11115,16 +11055,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.10.18",
|
||||
"version": "1.10.19",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "52b6416c579663eebdd2f1d97df21971daf3b43f"
|
||||
"reference": "af5a296ff02610c1bfb4ddfac9fd4a08657b9046"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/52b6416c579663eebdd2f1d97df21971daf3b43f",
|
||||
"reference": "52b6416c579663eebdd2f1d97df21971daf3b43f",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/af5a296ff02610c1bfb4ddfac9fd4a08657b9046",
|
||||
"reference": "af5a296ff02610c1bfb4ddfac9fd4a08657b9046",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11173,7 +11113,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-06-07T22:00:43+00:00"
|
||||
"time": "2023-06-14T15:26:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
|
||||
120
phpstan.neon
120
phpstan.neon
@@ -2141,31 +2141,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Event/CalendarConfigurationEvent.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'actions' on mixed\\.$#"
|
||||
count: 11
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'children' on mixed\\.$#"
|
||||
count: 2
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset non\\-falsy\\-string on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset string on mixed\\.$#"
|
||||
count: 7
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:__construct\\(\\) has parameter \\$payload with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:addAction\\(\\) has parameter \\$action with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -2181,26 +2156,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:getActions\\(\\) should return array but returns mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:replaceAction\\(\\) has parameter \\$action with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#"
|
||||
count: 2
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, mixed given\\.$#"
|
||||
count: 9
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Event\\\\PermissionSectionsEvent\\:\\:\\$sections type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -2261,16 +2221,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/AbstractActionsSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'timesheet' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/AbstractTimesheetSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'activity' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/ActivitySubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getCustomer\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -2286,81 +2236,11 @@ parameters:
|
||||
count: 2
|
||||
path: src/EventSubscriber/Actions/ActivitySubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'customer' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/CustomerSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'available' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/DashboardSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'widgets' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/DashboardSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'invoice' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/InvoiceSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'token' on mixed\\.$#"
|
||||
count: 4
|
||||
path: src/EventSubscriber/Actions/InvoiceSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'template' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/InvoiceTemplateSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'token' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/InvoiceTemplateSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'plugin' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/PluginSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getHomepage\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/PluginSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'project' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/ProjectSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/ProjectSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'tag' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/TagSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'team' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/TeamSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'user' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/UserFormsSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'user' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/EventSubscriber/Actions/UserSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
|
||||
count: 1
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
public/build/app.99c7b6b4.js
Normal file
2
public/build/app.99c7b6b4.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
@@ -3,10 +3,10 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/app.1adbc54f.js"
|
||||
"/build/app.99c7b6b4.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.5710a463.css"
|
||||
"/build/app.a61c91dc.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -63,8 +63,8 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||
"/build/app.1adbc54f.js": "sha384-5uMKKT4bInjtdsb3E6rH70YcbOenw11LI3aLD/dNdMAkoIIa4Pqu20RC3Sq0oLeO",
|
||||
"/build/app.5710a463.css": "sha384-kqam1K3HVJSgDgp3YsKvzA5VGQQB2USKq6UbuIHQvn9tshMribY5Axgg2ar7vTIR",
|
||||
"/build/app.99c7b6b4.js": "sha384-bPAdOMl3I6Ia3clnHgDiwoNKKe/bKnAdHUJdl7BXbsFNMg2wHsUDrGH+uNCQowqM",
|
||||
"/build/app.a61c91dc.css": "sha384-7rtbsyyUucsBktCZZXFuqnSHX0cvCWQ0KQgHpolGcWlFhaIEPn+ipnOc5yySeYwT",
|
||||
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build/app.css": "/build/app.5710a463.css",
|
||||
"build/app.js": "/build/app.1adbc54f.js",
|
||||
"build/app.css": "/build/app.a61c91dc.css",
|
||||
"build/app.js": "/build/app.99c7b6b4.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
|
||||
"build/invoice.css": "/build/invoice.5bea118e.css",
|
||||
|
||||
@@ -52,7 +52,7 @@ final class ReloadCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Reloading configurations ...');
|
||||
$io->text('Validating config file syntax ...');
|
||||
|
||||
// many users execute the bin/console command from arbitrary locations
|
||||
$path = getcwd();
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.26';
|
||||
public const VERSION = '2.0.27';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20026;
|
||||
public const VERSION_ID = 20027;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -122,17 +122,12 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
* Adds an "error" flash message to the stack.
|
||||
*
|
||||
* @param string $translationKey
|
||||
* @param array<string, string>|string $reason passing an array is deprecated
|
||||
* @param string $reason
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function flashError(string $translationKey, array|string $reason = ''): void
|
||||
protected function flashError(string $translationKey, string $reason = ''): void
|
||||
{
|
||||
if (\is_array($reason)) {
|
||||
@trigger_error('Calling "flashError" with an array $reason is deprecated and will be removed soon. Refactor and pass a string instead.', E_USER_DEPRECATED);
|
||||
$reason = \array_key_exists('%reason%', $reason) ? $reason['%reason%'] : '';
|
||||
}
|
||||
|
||||
$this->addFlashTranslated('error', $translationKey, ['%reason%' => $reason]);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
* Controller used to manage activities.
|
||||
*/
|
||||
#[Route(path: '/admin/activity')]
|
||||
#[IsGranted(new Expression("is_granted('view_activity') or is_granted('view_teamlead_activity') or is_granted('view_team_activity')"))]
|
||||
final class ActivityController extends AbstractController
|
||||
{
|
||||
public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService)
|
||||
@@ -56,6 +55,7 @@ final class ActivityController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_activity', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_activity_paginated', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'activity')"))]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
@@ -329,7 +329,11 @@ final class ActivityController extends AbstractController
|
||||
$this->activityService->updateActivity($activity);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
if ($this->isGranted('view', $activity)) {
|
||||
return $this->redirectToRoute('activity_details', ['id' => $activity->getId()]);
|
||||
} else {
|
||||
return new Response();
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
@@ -389,6 +393,7 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'activity_export', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'activity')"))]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
|
||||
@@ -47,10 +47,9 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Controller used to manage customer in the admin part of the site.
|
||||
* Controller used to manage customers.
|
||||
*/
|
||||
#[Route(path: '/admin/customer')]
|
||||
#[IsGranted(new Expression("is_granted('view_customer') or is_granted('view_teamlead_customer') or is_granted('view_team_customer')"))]
|
||||
final class CustomerController extends AbstractController
|
||||
{
|
||||
public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
@@ -59,6 +58,7 @@ final class CustomerController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_customer', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_customer_paginated', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'customer')"))]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
@@ -452,6 +452,7 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'customer_export', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'customer')"))]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
@@ -492,7 +493,11 @@ final class CustomerController extends AbstractController
|
||||
return $this->redirectToRouteAfterCreate('customer_details', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
if ($this->isGranted('view', $customer)) {
|
||||
return $this->redirectToRoute('customer_details', ['id' => $customer->getId()]);
|
||||
} else {
|
||||
return new Response();
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->handleFormUpdateException($ex, $editForm);
|
||||
}
|
||||
|
||||
@@ -433,7 +433,7 @@ final class InvoiceController extends AbstractController
|
||||
$table->addColumn('calculator', ['class' => 'd-none', 'orderBy' => false, 'title' => 'invoice_calculator', 'translation_domain' => 'invoice-calculator']);
|
||||
$table->addColumn('renderer', ['class' => 'd-none', 'orderBy' => false, 'title' => 'invoice_renderer', 'translation_domain' => 'invoice-renderer']);
|
||||
$table->addColumn('language', ['class' => 'd-none text-nowrap', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = $this->createPageSetup('admin_invoice_template.title');
|
||||
$page->setDataTable($table);
|
||||
|
||||
@@ -54,7 +54,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
* Controller used to manage projects.
|
||||
*/
|
||||
#[Route(path: '/admin/project')]
|
||||
#[IsGranted(new Expression("is_granted('view_project') or is_granted('view_teamlead_project') or is_granted('view_team_project')"))]
|
||||
final class ProjectController extends AbstractController
|
||||
{
|
||||
public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService)
|
||||
@@ -63,6 +62,7 @@ final class ProjectController extends AbstractController
|
||||
|
||||
#[Route(path: '/', defaults: ['page' => 1], name: 'admin_project', methods: ['GET'])]
|
||||
#[Route(path: '/page/{page}', requirements: ['page' => '[1-9]\d*'], name: 'admin_project_paginated', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'project')"))]
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
@@ -435,7 +435,11 @@ final class ProjectController extends AbstractController
|
||||
$this->projectService->updateProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
if ($this->isGranted('view', $project)) {
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
} else {
|
||||
return new Response();
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
@@ -512,6 +516,7 @@ final class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/export', name: 'project_export', methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('listing', 'project')"))]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
|
||||
@@ -789,7 +789,9 @@ final class Configuration implements ConfigurationInterface
|
||||
->defaultValue('Login with SAML')
|
||||
->end()
|
||||
->scalarNode('provider')
|
||||
->defaultNull()
|
||||
// the "default" was only added, to prevent support requests by people who did not
|
||||
// adjust their config between 1.x and 2.0
|
||||
->defaultValue('default')
|
||||
->end()
|
||||
->arrayNode('roles')
|
||||
->addDefaultsIfNotSet()
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Doctrine;
|
||||
|
||||
/**
|
||||
* Used to identify EventSubscribers, that work upon EntityManager events and listen on data changes.
|
||||
* These Subscribers will deactivated on batch imports, for performance gains and reduced DB queries.
|
||||
* These Subscribers will be deactivated on batch imports, for performance gains and reduced DB queries.
|
||||
*/
|
||||
interface DataSubscriberInterface
|
||||
{
|
||||
|
||||
@@ -13,17 +13,17 @@ use App\Entity\User;
|
||||
|
||||
/**
|
||||
* This event is triggered for every action:
|
||||
* - once per side load for table actions
|
||||
* - once for every row item
|
||||
* - once per side load for page actions
|
||||
* - once for every entity item (table row)
|
||||
*
|
||||
* @property array{'actions': array, 'view': string} $payload
|
||||
*/
|
||||
class PageActionsEvent extends ThemeEvent
|
||||
{
|
||||
private string $action;
|
||||
private string $view;
|
||||
private int $divider = 0;
|
||||
private ?string $locale = null;
|
||||
|
||||
public function __construct(User $user, array $payload, string $action, string $view)
|
||||
public function __construct(User $user, array $payload, private string $action, private string $view)
|
||||
{
|
||||
// only for BC reasons, do not access it directly!
|
||||
if (!\array_key_exists('actions', $payload)) {
|
||||
@@ -34,8 +34,6 @@ class PageActionsEvent extends ThemeEvent
|
||||
$payload['view'] = $view;
|
||||
}
|
||||
parent::__construct($user, $payload);
|
||||
$this->action = $action;
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
public function getActionName(): string
|
||||
@@ -101,7 +99,7 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
public function hasSubmenu(string $submenu): bool
|
||||
{
|
||||
if (!\array_key_exists($submenu, $this->payload['actions'])) {
|
||||
if (!$this->hasAction($submenu)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -110,7 +108,7 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
public function addActionToSubmenu(string $submenu, string $key, array $action): void
|
||||
{
|
||||
if (\array_key_exists($submenu, $this->payload['actions'])) {
|
||||
if ($this->hasAction($submenu)) {
|
||||
if (!\array_key_exists('children', $this->payload['actions'][$submenu])) {
|
||||
$this->payload['actions'][$submenu]['children'] = [];
|
||||
}
|
||||
@@ -125,14 +123,14 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
public function addAction(string $key, array $action): void
|
||||
{
|
||||
if (!\array_key_exists($key, $this->payload['actions'])) {
|
||||
if (!$this->hasAction($key)) {
|
||||
$this->payload['actions'][$key] = $action;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeAction(string $key): void
|
||||
{
|
||||
if (\array_key_exists($key, $this->payload['actions'])) {
|
||||
if ($this->hasAction($key)) {
|
||||
unset($this->payload['actions'][$key]);
|
||||
}
|
||||
}
|
||||
@@ -153,6 +151,11 @@ class PageActionsEvent extends ThemeEvent
|
||||
$this->addAction('create', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' : ''), 'title' => 'create', 'accesskey' => 'a']);
|
||||
}
|
||||
|
||||
public function addEdit(string $url, bool $modal = true): void
|
||||
{
|
||||
$this->addAction('edit', ['url' => $url, 'class' => ($modal ? 'modal-ajax-form' : ''), 'translation_domain' => 'actions', 'title' => 'edit']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to a configuration section.
|
||||
*
|
||||
|
||||
@@ -16,9 +16,9 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
final class ReportingEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var ReportInterface[]
|
||||
* @var array<string, ReportInterface>
|
||||
*/
|
||||
private $reports = [];
|
||||
private array $reports = [];
|
||||
|
||||
public function __construct(private User $user)
|
||||
{
|
||||
@@ -37,7 +37,7 @@ final class ReportingEvent extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReportInterface[]
|
||||
* @return array<ReportInterface>
|
||||
*/
|
||||
public function getReports(): array
|
||||
{
|
||||
|
||||
@@ -23,11 +23,12 @@ class ThemeEvent extends Event
|
||||
public const CONTENT_AFTER = 'app.theme.content_after';
|
||||
|
||||
private string $content = '';
|
||||
protected mixed $payload;
|
||||
|
||||
public function __construct(private ?User $user = null, mixed $payload = null)
|
||||
/**
|
||||
* @param array<string, mixed|array<mixed>> $payload
|
||||
*/
|
||||
public function __construct(private ?User $user = null, protected array $payload = [])
|
||||
{
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
@@ -47,18 +48,11 @@ class ThemeEvent extends Event
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPayload(): mixed
|
||||
/**
|
||||
* @return array<string, mixed|array<mixed>>
|
||||
*/
|
||||
public function getPayload(): array
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0.19, will be removed with 2.1
|
||||
*/
|
||||
public function setPayload(mixed $payload): void
|
||||
{
|
||||
@trigger_error('ThemeEvent::setPayload() is deprecated, use AbstractActionsSubscriber instead.', E_USER_DEPRECATED);
|
||||
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
|
||||
41
src/Event/WorkingTimeApproveMonthEvent.php
Normal file
41
src/Event/WorkingTimeApproveMonthEvent.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\WorkingTime\Model\Month;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class WorkingTimeApproveMonthEvent extends Event
|
||||
{
|
||||
public function __construct(private User $user, private Month $month, private \DateTimeInterface $approvalDate, private User $approver)
|
||||
{
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getMonth(): Month
|
||||
{
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
public function getApprovalDate(): \DateTimeInterface
|
||||
{
|
||||
return $this->approvalDate;
|
||||
}
|
||||
|
||||
public function getApprover(): User
|
||||
{
|
||||
return $this->approver;
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,7 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $timesheet)) {
|
||||
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
|
||||
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path($routeEdit, ['id' => $timesheet->getId()]), 'class' => $class]);
|
||||
$event->addEdit($this->path($routeEdit, ['id' => $timesheet->getId()]), !$event->isView('edit'));
|
||||
}
|
||||
|
||||
if ($this->isGranted('duplicate', $timesheet)) {
|
||||
|
||||
@@ -35,8 +35,7 @@ final class ActivitySubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $activity)) {
|
||||
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
|
||||
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_activity_edit', ['id' => $activity->getId()]), 'class' => $class]);
|
||||
$event->addEdit($this->path('admin_activity_edit', ['id' => $activity->getId()]), !$event->isView('edit'));
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $activity)) {
|
||||
|
||||
@@ -38,8 +38,7 @@ final class CustomerSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $customer)) {
|
||||
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
|
||||
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_customer_edit', ['id' => $customer->getId()]), 'class' => $class]);
|
||||
$event->addEdit($this->path('admin_customer_edit', ['id' => $customer->getId()]), !$event->isView('edit'));
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $customer)) {
|
||||
|
||||
@@ -35,7 +35,7 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
|
||||
$allowCustomer = $this->isGranted('access', $invoice->getCustomer());
|
||||
|
||||
if ($allowCustomer && $allowCreate) {
|
||||
$event->addAction('edit', ['url' => $this->path('admin_invoice_edit', ['id' => $invoice->getId()]), 'class' => 'modal-ajax-form']);
|
||||
$event->addEdit($this->path('admin_invoice_edit', ['id' => $invoice->getId()]));
|
||||
}
|
||||
|
||||
if ($allowCustomer && $allowView) {
|
||||
|
||||
@@ -31,7 +31,7 @@ final class InvoiceTemplateSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('manage_invoice_template')) {
|
||||
$event->addAction('edit', ['url' => $this->path('admin_invoice_template_edit', ['id' => $template->getId()]), 'class' => 'modal-ajax-form']);
|
||||
$event->addEdit($this->path('admin_invoice_template_edit', ['id' => $template->getId()]));
|
||||
$event->addAction('copy', ['url' => $this->path('admin_invoice_template_copy', ['id' => $template->getId()]), 'class' => 'modal-ajax-form']);
|
||||
$event->addDelete($this->path('admin_invoice_template_delete', ['id' => $template->getId(), 'csrfToken' => $payload['token']]), false);
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@ final class ProjectSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $project)) {
|
||||
$class = $event->isView('edit') ? '' : 'modal-ajax-form';
|
||||
$event->addAction('edit', ['title' => 'edit', 'translation_domain' => 'actions', 'url' => $this->path('admin_project_edit', ['id' => $project->getId()]), 'class' => $class]);
|
||||
$event->addEdit($this->path('admin_project_edit', ['id' => $project->getId()]), !$event->isView('edit'));
|
||||
}
|
||||
|
||||
if ($this->isGranted('permissions', $project)) {
|
||||
|
||||
@@ -42,8 +42,7 @@ final class TagSubscriber extends AbstractActionsSubscriber
|
||||
}
|
||||
|
||||
if ($this->isGranted('manage_tag')) {
|
||||
$class = ($event->isView('edit') ? '' : 'modal-ajax-form');
|
||||
$event->addAction('edit', ['url' => $this->path('tags_edit', ['id' => $id]), 'class' => $class]);
|
||||
$event->addEdit($this->path('tags_edit', ['id' => $id]));
|
||||
}
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
|
||||
@@ -32,7 +32,7 @@ final class TeamSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
if ($this->isGranted('edit', $team)) {
|
||||
if (!$event->isView('edit')) {
|
||||
$event->addAction('edit', ['url' => $this->path('admin_team_edit', ['id' => $team->getId()]), 'title' => 'action.edit']);
|
||||
$event->addEdit($this->path('admin_team_edit', ['id' => $team->getId()]));
|
||||
}
|
||||
|
||||
if ($this->isGranted('create_team')) {
|
||||
|
||||
@@ -9,40 +9,15 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YearPickerType;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ContractByUserForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
public function getParent(): ?string
|
||||
{
|
||||
$builder->add('date', YearPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
]);
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class, ['width' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => YearByUser::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'include_user' => false,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
return YearByUserForm::class;
|
||||
}
|
||||
}
|
||||
|
||||
45
src/Form/YearByUserForm.php
Normal file
45
src/Form/YearByUserForm.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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;
|
||||
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YearPickerType;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class YearByUserForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('date', YearPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
]);
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class, ['width' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => YearByUser::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'include_user' => false,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -23,17 +23,10 @@ abstract class AbstractRenderer
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
abstract protected function getFileExtensions();
|
||||
abstract protected function getFileExtensions(): array;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getContentType();
|
||||
abstract protected function getContentType(): string;
|
||||
|
||||
/**
|
||||
* @param InvoiceDocument $document
|
||||
* @return bool
|
||||
*/
|
||||
public function supports(InvoiceDocument $document): bool
|
||||
{
|
||||
foreach ($this->getFileExtensions() as $extension) {
|
||||
@@ -50,12 +43,7 @@ abstract class AbstractRenderer
|
||||
return (string) new InvoiceFilename($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $file
|
||||
* @param string $filename
|
||||
* @return BinaryFileResponse
|
||||
*/
|
||||
protected function getFileResponse($file, $filename)
|
||||
protected function getFileResponse(mixed $file, string $filename): BinaryFileResponse
|
||||
{
|
||||
$response = new BinaryFileResponse($file);
|
||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||
|
||||
@@ -25,12 +25,8 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
|
||||
{
|
||||
/**
|
||||
* Saves the Spreadhseet and returns the filename.
|
||||
*
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet);
|
||||
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet): string;
|
||||
|
||||
/**
|
||||
* Render the given InvoiceDocument with the data from the InvoiceModel.
|
||||
|
||||
@@ -25,7 +25,7 @@ final class XlsxRenderer extends AbstractSpreadsheetRenderer implements Renderer
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = @tempnam(sys_get_temp_dir(), 'kimai-invoice-xlsx');
|
||||
if (false === $filename) {
|
||||
|
||||
@@ -11,8 +11,18 @@ namespace App\Model;
|
||||
|
||||
class PermissionSection implements PermissionSectionInterface
|
||||
{
|
||||
public function __construct(private string $title, private string $filter)
|
||||
/** @var array<string> */
|
||||
private array $filter;
|
||||
|
||||
/**
|
||||
* @param string|array<string> $filter
|
||||
*/
|
||||
public function __construct(private string $title, string|array $filter)
|
||||
{
|
||||
if (!\is_array($filter)) {
|
||||
$filter = [$filter];
|
||||
}
|
||||
$this->filter = $filter;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
@@ -22,6 +32,12 @@ class PermissionSection implements PermissionSectionInterface
|
||||
|
||||
public function filter(string $permission): bool
|
||||
{
|
||||
return str_contains($permission, $this->filter);
|
||||
foreach ($this->filter as $filter) {
|
||||
if (str_contains($permission, $filter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,13 @@ class ProjectStatisticService
|
||||
->andWhere($qb->expr()->eq('p.visible', true))
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->andWhere($qb->expr()->not($qb->expr()->exists($qb2)))
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('p.start'),
|
||||
$qb->expr()->lte('p.start', ':project_start')
|
||||
)
|
||||
)
|
||||
->setParameter('project_start', $now, Types::DATETIME_MUTABLE)
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('p.end'),
|
||||
|
||||
@@ -19,6 +19,8 @@ use Doctrine\ORM\EntityRepository;
|
||||
*/
|
||||
class WorkingTimeRepository extends EntityRepository
|
||||
{
|
||||
private bool $pendingUpdate = false;
|
||||
|
||||
public function deleteWorkingTime(WorkingTime $workingTime): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
@@ -35,12 +37,15 @@ class WorkingTimeRepository extends EntityRepository
|
||||
|
||||
public function scheduleWorkingTimeUpdate(WorkingTime $workingTime): void
|
||||
{
|
||||
$this->pendingUpdate = true;
|
||||
$this->getEntityManager()->persist($workingTime);
|
||||
}
|
||||
|
||||
public function persistScheduledWorkingTimes(): void
|
||||
{
|
||||
$this->getEntityManager()->flush();
|
||||
if ($this->pendingUpdate) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,7 +79,7 @@ class SamlAuthenticator extends AbstractAuthenticator
|
||||
|
||||
$oneLoginAuth->processResponse();
|
||||
|
||||
// $this->logger->debug('Received SAML response: ' . $oneLoginAuth->getLastResponseXML());
|
||||
// file_put_contents(__DIR__ . '/../../var/log/saml.xml', $oneLoginAuth->getLastResponseXML());
|
||||
|
||||
if ($oneLoginAuth->getErrors()) {
|
||||
throw new AuthenticationException($oneLoginAuth->getLastErrorReason());
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Event\ThemeEvent;
|
||||
use App\Event\ThemeJavascriptTranslationsEvent;
|
||||
use App\Utils\Color;
|
||||
use App\Utils\FormFormatConverter;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Twig\Environment;
|
||||
@@ -25,22 +25,20 @@ use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class ThemeExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private TranslatorInterface $translator, private SystemConfiguration $configuration)
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher, private TranslatorInterface $translator, private SystemConfiguration $configuration, private Security $security)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Environment $environment
|
||||
* @param string $eventName
|
||||
* @param mixed|null $payload
|
||||
* @param array<string, mixed> $payload
|
||||
* @return ThemeEvent
|
||||
*/
|
||||
public function trigger(Environment $environment, string $eventName, $payload = null): ThemeEvent
|
||||
public function trigger(Environment $environment, string $eventName, array $payload = []): ThemeEvent
|
||||
{
|
||||
/** @var AppVariable $app */
|
||||
$app = $environment->getGlobals()['app'];
|
||||
/** @var User $user */
|
||||
$user = $app->getUser();
|
||||
$user = $this->security->getUser();
|
||||
|
||||
$themeEvent = new ThemeEvent($user, $payload);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ final class EntityMultiRoleVoter extends Voter
|
||||
'budget_time',
|
||||
'budget_any',
|
||||
'details',
|
||||
'listing',
|
||||
];
|
||||
private const ALLOWED_SUBJECTS = [
|
||||
'customer',
|
||||
@@ -99,6 +100,13 @@ final class EntityMultiRoleVoter extends Voter
|
||||
$permissions[] = 'time_teamlead';
|
||||
$permissions[] = 'time_team';
|
||||
}
|
||||
|
||||
if ($attribute === 'listing') {
|
||||
$permissions[] = 'view';
|
||||
$permissions[] = 'view_team';
|
||||
$permissions[] = 'view_teamlead';
|
||||
}
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
if ($this->permissionManager->hasRolePermission($user, $permission . '_' . $suffix)) {
|
||||
return true;
|
||||
|
||||
@@ -18,9 +18,9 @@ use App\Model\Year as BaseYear;
|
||||
*/
|
||||
final class Year extends BaseYear
|
||||
{
|
||||
public function __construct(\DateTimeInterface $month, private User $user)
|
||||
public function __construct(\DateTimeInterface $year, private User $user)
|
||||
{
|
||||
parent::__construct($month);
|
||||
parent::__construct($year);
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\WorkingTime;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkingTime;
|
||||
use App\Event\WorkingTimeApproveMonthEvent;
|
||||
use App\Event\WorkingTimeYearEvent;
|
||||
use App\Event\WorkingTimeYearSummaryEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
@@ -94,10 +95,8 @@ final class WorkingTimeService
|
||||
return $year->getMonth($monthDate);
|
||||
}
|
||||
|
||||
public function approveMonth(Month $month, \DateTimeInterface $approvalDate, User $approver): void
|
||||
public function approveMonth(User $user, Month $month, \DateTimeInterface $approvalDate, User $approver): void
|
||||
{
|
||||
$update = false;
|
||||
|
||||
foreach ($month->getDays() as $day) {
|
||||
$workingTime = $day->getWorkingTime();
|
||||
if ($workingTime === null) {
|
||||
@@ -115,12 +114,11 @@ final class WorkingTimeService
|
||||
$workingTime->setApprovedBy($approver);
|
||||
$workingTime->setApprovedAt($approvalDate);
|
||||
$this->workingTimeRepository->scheduleWorkingTimeUpdate($workingTime);
|
||||
$update = true;
|
||||
}
|
||||
|
||||
if ($update) {
|
||||
$this->workingTimeRepository->persistScheduledWorkingTimes();
|
||||
}
|
||||
$this->workingTimeRepository->persistScheduledWorkingTimes();
|
||||
|
||||
$this->eventDispatcher->dispatch(new WorkingTimeApproveMonthEvent($user, $month, $approvalDate, $approver));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -183,9 +183,6 @@
|
||||
"myclabs/deep-copy": {
|
||||
"version": "1.7.0"
|
||||
},
|
||||
"myclabs/php-enum": {
|
||||
"version": "1.7.6"
|
||||
},
|
||||
"nelmio/api-doc-bundle": {
|
||||
"version": "3.0",
|
||||
"recipe": {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{% endif %}
|
||||
{% endset %}
|
||||
{% from '@theme/components/alert.html.twig' import alert %}
|
||||
{{ alert({type: 'info', description: warningMsg|raw}) }}
|
||||
{{ alert({type: 'info', description: warningMsg|raw, important: true}) }}
|
||||
{% endif %}
|
||||
|
||||
{% if summaries|length > 0 %}
|
||||
|
||||
@@ -130,12 +130,28 @@
|
||||
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
|
||||
</tr>
|
||||
{% if not entity.isMonthlyBudget() and timeBudgetVisible and stats.duration > 0 and stats.duration|chart_duration > 0.00 %}
|
||||
{% if stats.internalRate > 0 %}
|
||||
{% set revenueTotal = stats.rateBillable - stats.internalRate %}
|
||||
<tr>
|
||||
<td>{{ 'stats.revenue'|trans }} ({{ 'billable'|trans }} - {{ 'internalRate'|trans }})</td>
|
||||
<td class="text-nowrap text-end">{{ revenueTotal|money(currency) }}</td>
|
||||
<td class="text-nowrap text-end">-</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% set realHourlyRate = stats.rateBillable / stats.duration|chart_duration %}
|
||||
<tr>
|
||||
<td>{{ 'hourlyRate'|trans }} ({{ 'billable'|trans }} / {{ durationTrans|trans }})</td>
|
||||
<td class="text-nowrap text-end">{{ realHourlyRate|money(currency) }}</td>
|
||||
<td class="text-nowrap text-end">-</td>
|
||||
</tr>
|
||||
{% if stats.internalRate > 0 %}
|
||||
{% set revenueHourlyRate = revenueTotal / stats.duration|chart_duration %}
|
||||
<tr>
|
||||
<td>{{ 'hourlyRate'|trans }} ({{ 'stats.revenue'|trans }} / {{ durationTrans|trans }})</td>
|
||||
<td class="text-nowrap text-end">{{ revenueHourlyRate|money(currency) }}</td>
|
||||
<td class="text-nowrap text-end">-</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if entity.budget > 0 %}
|
||||
<tr>
|
||||
|
||||
35
templates/embeds/datatable.html.twig
Normal file
35
templates/embeds/datatable.html.twig
Normal file
@@ -0,0 +1,35 @@
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
{% from "macros/widgets.html.twig" import nothing_found %}
|
||||
|
||||
{% if not dataTable.hasResults() %}
|
||||
{{ nothing_found(dataTable.getReloadEvents()) }}
|
||||
{% else %}
|
||||
{% block datatable_header %}
|
||||
{{ tables.header(dataTable) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block datatable_before %}{% endblock %}
|
||||
|
||||
{% set sortedColumns = dataTable.sortedColumnNames %}
|
||||
{% block datatable_outer %}
|
||||
{% for entry in dataTable %}
|
||||
{% block datatable_row %}
|
||||
<tr{% block datatable_row_attr %}{% endblock %}>
|
||||
{% for column, data in sortedColumns %}
|
||||
{% block datatable_column %}
|
||||
<td class="{{ tables.class(dataTable, column) }}"{% block datatable_column_attr %}{% endblock %}>
|
||||
{% block datatable_column_value %}{{ column }}{% endblock %}
|
||||
</td>
|
||||
{% endblock %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endblock %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block datatable_after %}{% endblock %}
|
||||
|
||||
{% block datatable_footer %}
|
||||
{{ tables.footer(dataTable) }}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% from "macros/widgets.html.twig" import username %}
|
||||
|
||||
{% block table_actions %}
|
||||
{# compare templates/macros/datatables.html.twig macro actions(dataTable) #}
|
||||
@@ -14,7 +14,7 @@
|
||||
{% if form.user is defined %}
|
||||
{{ form_widget(form.user) }}
|
||||
{% elseif user is defined and app.user != user %}
|
||||
{{ widgets.username(user) }}
|
||||
{{ username(user) }}
|
||||
{% endif %}
|
||||
{% for field in form %}
|
||||
{% if not field.rendered %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
} %}
|
||||
{% embed formEditTemplate with formOptions %}
|
||||
{% block form_body %}
|
||||
<fieldset class="form-fieldset">
|
||||
<fieldset class="form-fieldset pb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_row(form.customer) }}
|
||||
@@ -22,27 +22,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="form-fieldset">
|
||||
<fieldset class="form-fieldset pb-0">
|
||||
{{ form_row(form.replaceTags) }}
|
||||
{{ form_row(form.tags) }}
|
||||
</fieldset>
|
||||
{% if form.user is defined %}
|
||||
<fieldset class="form-fieldset">
|
||||
<fieldset class="form-fieldset pb-0">
|
||||
{{ form_row(form.user) }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% if form.exported is defined %}
|
||||
<fieldset class="form-fieldset">
|
||||
<fieldset class="form-fieldset pb-0">
|
||||
{{ form_row(form.exported) }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% if form.billable is defined %}
|
||||
<fieldset class="form-fieldset">
|
||||
<fieldset class="form-fieldset pb-0">
|
||||
{{ form_row(form.billable) }}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% if form.recalculateRates is defined %}
|
||||
<fieldset class="form-fieldset">
|
||||
<fieldset class="form-fieldset pb-0">
|
||||
{{ form_row(form.recalculateRates) }}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -55,7 +55,7 @@
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% if form.metaFields is defined %}
|
||||
<fieldset class="form-fieldset">
|
||||
<fieldset class="form-fieldset pb-0">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.updateMeta) }}
|
||||
|
||||
@@ -2,35 +2,22 @@
|
||||
|
||||
{% block form_content %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ 'work_times_should'|trans }}</label>
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'Monday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Tuesday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Wednesday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Thursday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Friday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Saturday'|trans({}, 'system-configuration') }}</th>
|
||||
<th>{{ 'Sunday'|trans({}, 'system-configuration') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ form_widget(form.workHoursMonday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursTuesday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursWednesday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursThursday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursFriday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursSaturday) }}</td>
|
||||
<td>{{ form_widget(form.workHoursSunday) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="form-text mb-0 help-text">{{ 'work_times_should.help'|trans }}</div>
|
||||
</div>
|
||||
{% form_theme form 'form/horizontal.html.twig' %}
|
||||
|
||||
{{ form_rest(form) }}
|
||||
<fieldset class="form-fieldset">
|
||||
<legend>{{ 'work_times_should'|trans }}</legend>
|
||||
<div class="form-text mb-3 help-text">{{ 'work_times_should.help'|trans }}</div>
|
||||
{{ 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>
|
||||
|
||||
<fieldset class="form-fieldset">
|
||||
{{ form_rest(form) }}
|
||||
</fieldset>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -413,7 +413,7 @@ class ConfigurationTest extends TestCase
|
||||
'connection' => [
|
||||
'organization' => []
|
||||
],
|
||||
'provider' => null,
|
||||
'provider' => 'default',
|
||||
],
|
||||
'company' => [
|
||||
'financial_year' => null,
|
||||
|
||||
@@ -96,12 +96,14 @@ class PageActionsEventTest extends TestCase
|
||||
$sut->addCreate('foo5', true);
|
||||
$sut->addCreate('foo6', false);
|
||||
$sut->addQuickExport('foo7');
|
||||
$sut->addEdit('trölölö');
|
||||
|
||||
$expected = [
|
||||
'divider0' => null,
|
||||
'columns' => ['modal' => '#foo2', 'title' => 'modal.columns.title'],
|
||||
'create' => ['url' => 'foo5', 'class' => 'modal-ajax-form', 'title' => 'create', 'accesskey' => 'a'],
|
||||
'download' => ['url' => 'foo7', 'class' => 'toolbar-action', 'title' => 'export'],
|
||||
'edit' => ['url' => 'trölölö', 'class' => 'modal-ajax-form', 'translation_domain' => 'actions', 'title' => 'edit'],
|
||||
'trash' => ['url' => 'foo3', 'class' => 'modal-ajax-form text-red', 'translation_domain' => 'actions'],
|
||||
];
|
||||
$this->assertEquals(\count($expected), $sut->countActions());
|
||||
|
||||
@@ -22,7 +22,7 @@ class ThemeEventTest extends TestCase
|
||||
{
|
||||
$sut = new ThemeEvent();
|
||||
$this->assertNull($sut->getUser());
|
||||
$this->assertNull($sut->getPayload());
|
||||
$this->assertIsArray($sut->getPayload());
|
||||
$this->assertEquals('', $sut->getContent());
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class ThemeEventTest extends TestCase
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$payload = [null, '', 'test', new \stdClass()];
|
||||
$payload = ['foo' => null, '' => '', 'test' => 'test', 'class' => new \stdClass()];
|
||||
|
||||
$sut = new ThemeEvent($user, $payload);
|
||||
$this->assertEquals($payload, $sut->getPayload());
|
||||
@@ -62,10 +62,9 @@ class ThemeEventTest extends TestCase
|
||||
$user = new User();
|
||||
$user->setAlias('foo');
|
||||
|
||||
$payload = [null, '', 'test', new \stdClass()];
|
||||
$payload = ['foo' => null, '' => '', 'test' => 'test', 'class' => new \stdClass()];
|
||||
|
||||
$sut = new ThemeEvent($user);
|
||||
$sut->setPayload($payload);
|
||||
$sut = new ThemeEvent($user, $payload);
|
||||
$this->assertEquals($payload, $sut->getPayload());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class DebugRenderer extends AbstractRenderer implements RendererInterface
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getFileExtensions()
|
||||
protected function getFileExtensions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class DebugRenderer extends AbstractRenderer implements RendererInterface
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getContentType()
|
||||
protected function getContentType(): string
|
||||
{
|
||||
return 'array';
|
||||
}
|
||||
|
||||
32
tests/Model/PermissionSectionTest.php
Normal file
32
tests/Model/PermissionSectionTest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Model\PermissionSection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Model\PermissionSection
|
||||
*/
|
||||
class PermissionSectionTest extends TestCase
|
||||
{
|
||||
public function testFilter(): void
|
||||
{
|
||||
$sut = new PermissionSection('A wonderful name', '_contract');
|
||||
self::assertFalse($sut->filter('contract_settings'));
|
||||
self::assertTrue($sut->filter('my_contract'));
|
||||
self::assertTrue($sut->filter('my_contract_settings'));
|
||||
|
||||
$sut = new PermissionSection('A wonderful name', ['_contract', '_settings']);
|
||||
self::assertFalse($sut->filter('contractor'));
|
||||
self::assertTrue($sut->filter('contract_settings'));
|
||||
self::assertTrue($sut->filter('my_contract_settings'));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use App\Tests\Mocks\SystemConfigurationFactory;
|
||||
use App\Twig\Runtime\ThemeExtension;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\Twig\AppVariable;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
@@ -57,11 +58,14 @@ class ThemeEventExtensionTest extends TestCase
|
||||
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||
$translator->method('trans')->willReturn('foo');
|
||||
|
||||
$security = $this->getMockBuilder(Security::class)->disableOriginalConstructor()->getMock();
|
||||
$security->method('getUser')->willReturn(null);
|
||||
|
||||
$configs = [];
|
||||
$loader = new TestConfigLoader($configs);
|
||||
$configuration = SystemConfigurationFactory::create($loader, $this->getDefaultSettings());
|
||||
|
||||
return new ThemeExtension($dispatcher, $translator, $configuration);
|
||||
return new ThemeExtension($dispatcher, $translator, $configuration, $security);
|
||||
}
|
||||
|
||||
protected function getEnvironment(): Environment
|
||||
|
||||
@@ -1261,6 +1261,14 @@
|
||||
<source>status.closed</source>
|
||||
<target>Erledigt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="opDmYAa" resname="status.approved">
|
||||
<source>status.approved</source>
|
||||
<target>Genehmigt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Na24cyT" resname="status.requested">
|
||||
<source>status.requested</source>
|
||||
<target>Beantragt</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="crrlEUA" resname="preview.skipped_rows">
|
||||
<source>preview.skipped_rows</source>
|
||||
<target>Überspringe Vorschau von %rows% weiteren Zeilen …</target>
|
||||
@@ -1573,6 +1581,46 @@
|
||||
<source>default</source>
|
||||
<target>Standard</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="_WlU8Fp" resname="sickness">
|
||||
<source>sickness</source>
|
||||
<target>Krankheit</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="2SmKENG" resname="other">
|
||||
<source>other</source>
|
||||
<target>Andere</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="csAXaBa" resname="button.request">
|
||||
<source>button.request</source>
|
||||
<target>Einreichen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Od3xvQK" resname="button.report">
|
||||
<source>button.report</source>
|
||||
<target>Melden</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YDrWFFP" resname="sickness.intro">
|
||||
<source>sickness.intro</source>
|
||||
<target>Krankheit, Arztbesuch, Pflege naher Angehöriger etc.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yJg6P3m" resname="holiday.intro">
|
||||
<source>holiday.intro</source>
|
||||
<target>Sie haben bisher %used% von Ihren verfügbaren %days% Urlaubstagen genommen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="DA1.Ln_" resname="time_off.intro">
|
||||
<source>time_off.intro</source>
|
||||
<target>Ihr Stundenkonto weist einen Stand von %hours% Stunden auf.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YPO294a" resname="absence_other.intro">
|
||||
<source>absence_other.intro</source>
|
||||
<target>Familiäre Ereignisse, Umzug, Wetterchaos etc.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="TTnaBm4" resname="day_full">
|
||||
<source>day_full</source>
|
||||
<target>Ganzer Tag</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Eml59KV" resname="day_half">
|
||||
<source>day_half</source>
|
||||
<target>Halber Tag</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -1261,6 +1261,14 @@
|
||||
<source>status.closed</source>
|
||||
<target>Completed</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="opDmYAa" resname="status.approved">
|
||||
<source>status.approved</source>
|
||||
<target>Approved</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Na24cyT" resname="status.requested">
|
||||
<source>status.requested</source>
|
||||
<target>Requested</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="crrlEUA" resname="preview.skipped_rows">
|
||||
<source>Skipped preview of %rows% more rows …</source>
|
||||
<target state="translated">Skipped preview of %rows% more rows …</target>
|
||||
@@ -1573,6 +1581,46 @@
|
||||
<source>default</source>
|
||||
<target>Default</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="_WlU8Fp" resname="sickness">
|
||||
<source>sickness</source>
|
||||
<target>Sickness</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="2SmKENG" resname="other">
|
||||
<source>other</source>
|
||||
<target>Other</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="csAXaBa" resname="button.request">
|
||||
<source>button.request</source>
|
||||
<target>Submit</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Od3xvQK" resname="button.report">
|
||||
<source>button.report</source>
|
||||
<target>Report</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YDrWFFP" resname="sickness.intro">
|
||||
<source>sickness.intro</source>
|
||||
<target>Illness, doctor's visit, care of close relatives, etc.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="yJg6P3m" resname="holiday.intro">
|
||||
<source>holiday.intro</source>
|
||||
<target>You have taken %used% of your available %days% vacation days so far.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="DA1.Ln_" resname="time_off.intro">
|
||||
<source>time_off.intro</source>
|
||||
<target>Your working time account has a balance of %hours% hours.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="YPO294a" resname="absence_other.intro">
|
||||
<source>absence_other.intro</source>
|
||||
<target>Family events, moving house, weather chaos etc.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="TTnaBm4" resname="day_full">
|
||||
<source>day_full</source>
|
||||
<target>Full day</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Eml59KV" resname="day_half">
|
||||
<source>day_half</source>
|
||||
<target>Half day</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
<source>report_project_view</source>
|
||||
<target>Projektübersicht</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Yz_lySk" resname="report_customer_view">
|
||||
<source>report_customer_view</source>
|
||||
<target>Kundenübersicht</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="518eY_l" resname="report_project_details">
|
||||
<source>report_project_details</source>
|
||||
<target>Projektdetails</target>
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
<source>report_project_view</source>
|
||||
<target>Project overview</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Yz_lySk" resname="report_customer_view">
|
||||
<source>report_customer_view</source>
|
||||
<target>Customer overview</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="518eY_l" resname="report_project_details">
|
||||
<source>report_project_details</source>
|
||||
<target>Project details</target>
|
||||
|
||||
Reference in New Issue
Block a user