Release 2.0.27 (#4107)

This commit is contained in:
Kevin Papst
2023-07-04 17:01:29 +02:00
committed by GitHub
parent 6a99ad41ca
commit 651f812da8
62 changed files with 506 additions and 427 deletions

View File

@@ -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);
}
}

View File

@@ -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); }

View File

@@ -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
View File

@@ -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",

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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",

View File

@@ -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",

View File

@@ -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();

View File

@@ -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
*/

View File

@@ -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]);
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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()

View File

@@ -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
{

View File

@@ -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.
*

View File

@@ -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
{

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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)) {

View File

@@ -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')) {

View File

@@ -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')) {

View File

@@ -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;
}
}

View 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',
]);
}
}

View File

@@ -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);

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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'),

View File

@@ -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();
}
}
/**

View File

@@ -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());

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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));
}
/**

View File

@@ -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": {

View File

@@ -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 %}

View File

@@ -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>

View 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 %}

View File

@@ -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 %}

View File

@@ -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) }}

View File

@@ -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 %}

View File

@@ -413,7 +413,7 @@ class ConfigurationTest extends TestCase
'connection' => [
'organization' => []
],
'provider' => null,
'provider' => 'default',
],
'company' => [
'financial_year' => null,

View File

@@ -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());

View File

@@ -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());
}
}

View File

@@ -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';
}

View 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'));
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>