diff --git a/assets/sass/invoice-pdf.scss b/assets/sass/invoice-pdf.scss index 043308dc..5d63443c 100644 --- a/assets/sass/invoice-pdf.scss +++ b/assets/sass/invoice-pdf.scss @@ -34,10 +34,10 @@ p { .text-center { text-align: center; } -.text-left { +.text-left, .text-begin { text-align: left; } -.text-right { +.text-right, .text-end { text-align: right; } .text-nowrap { diff --git a/assets/sass/invoice.scss b/assets/sass/invoice.scss index b5870d5f..03ecd828 100644 --- a/assets/sass/invoice.scss +++ b/assets/sass/invoice.scss @@ -80,7 +80,6 @@ body { font-size: 20px; > small { - color: #666; display: block; margin-top: 5px; } @@ -107,6 +106,13 @@ body { text-align: right; } + .text-left { + text-align: left; + } + .text-right { + text-align: right; + } + .invoice-items { .table { thead th { diff --git a/composer.lock b/composer.lock index ba312ab0..db9e45ae 100644 --- a/composer.lock +++ b/composer.lock @@ -1449,16 +1449,16 @@ }, { "name": "doctrine/orm", - "version": "2.14.1", + "version": "2.14.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "de7eee5ed7b1b35c99b118f26f210a8281e6db8e" + "reference": "e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/de7eee5ed7b1b35c99b118f26f210a8281e6db8e", - "reference": "de7eee5ed7b1b35c99b118f26f210a8281e6db8e", + "url": "https://api.github.com/repos/doctrine/orm/zipball/e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52", + "reference": "e5fb1a4a8f5aa4e37612f66a22aac10291bcbb52", "shasum": "" }, "require": { @@ -1487,14 +1487,14 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^11.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.9.8", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpstan/phpstan": "~1.4.10 || 1.10.6", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.7.1", + "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^4.4 || ^5.4 || ^6.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.4.0" + "vimeo/psalm": "4.30.0 || 5.9.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1544,9 +1544,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.14.1" + "source": "https://github.com/doctrine/orm/tree/2.14.2" }, - "time": "2023-01-16T18:36:59+00:00" + "time": "2023-03-30T15:18:54+00:00" }, { "name": "doctrine/persistence", @@ -4068,16 +4068,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.17.1", + "version": "1.18.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "d3753fcb3abc6f78f5de6f72153d4b9c99c72dee" + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/d3753fcb3abc6f78f5de6f72153d4b9c99c72dee", - "reference": "d3753fcb3abc6f78f5de6f72153d4b9c99c72dee", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", "shasum": "" }, "require": { @@ -4107,9 +4107,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.17.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" }, - "time": "2023-04-04T11:11:22+00:00" + "time": "2023-04-07T11:51:11+00:00" }, { "name": "psr/cache", @@ -4265,21 +4265,21 @@ }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -4299,7 +4299,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -4311,27 +4311,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client/tree/1.0.2" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-04-10T20:12:12+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "e616d01114759c4c489f93b099585439f795fe35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", "shasum": "" }, "require": { "php": ">=7.0.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -4351,7 +4351,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interfaces for PSR-7 HTTP message factories", @@ -4366,9 +4366,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2023-04-10T20:10:41+00:00" }, { "name": "psr/http-message", @@ -10691,16 +10691,16 @@ }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "3.4.2", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "601988c5b46dbd20a0f886f967210aba378a6fd5" + "reference": "fd39829fed8f090ef6e185d33449d47c2fb59c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/601988c5b46dbd20a0f886f967210aba378a6fd5", - "reference": "601988c5b46dbd20a0f886f967210aba378a6fd5", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/fd39829fed8f090ef6e185d33449d47c2fb59c9c", + "reference": "fd39829fed8f090ef6e185d33449d47c2fb59c9c", "shasum": "" }, "require": { @@ -10754,7 +10754,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.4.2" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.4.3" }, "funding": [ { @@ -10770,7 +10770,7 @@ "type": "tidelift" } ], - "time": "2022-04-28T17:58:29+00:00" + "time": "2023-04-11T12:37:36+00:00" }, { "name": "fakerphp/faker", @@ -11174,16 +11174,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.11", + "version": "1.10.12", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21" + "reference": "7b2aaf999e522e2b10011a594cf06dc37de7fbd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8aa62e6ea8b58ffb650e02940e55a788cbc3fe21", - "reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7b2aaf999e522e2b10011a594cf06dc37de7fbd8", + "reference": "7b2aaf999e522e2b10011a594cf06dc37de7fbd8", "shasum": "" }, "require": { @@ -11232,7 +11232,7 @@ "type": "tidelift" } ], - "time": "2023-04-04T19:17:42+00:00" + "time": "2023-04-12T10:33:29+00:00" }, { "name": "phpstan/phpstan-doctrine", @@ -11407,16 +11407,16 @@ }, { "name": "phpstan/phpstan-symfony", - "version": "1.2.24", + "version": "1.2.25", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "db81b1861aac7cc2e66115cb33b4d1ea2a73d096" + "reference": "1da7bf450c6b351fec08ca0aa97298473d4f6ab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/db81b1861aac7cc2e66115cb33b4d1ea2a73d096", - "reference": "db81b1861aac7cc2e66115cb33b4d1ea2a73d096", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/1da7bf450c6b351fec08ca0aa97298473d4f6ab3", + "reference": "1da7bf450c6b351fec08ca0aa97298473d4f6ab3", "shasum": "" }, "require": { @@ -11472,9 +11472,9 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.24" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.25" }, - "time": "2023-03-30T08:38:10+00:00" + "time": "2023-04-05T12:16:20+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/phpstan.neon b/phpstan.neon index 993e07e4..638ea20a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4330,11 +4330,6 @@ parameters: count: 2 path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - - message: "#^Cannot call method getAlias\\(\\) on App\\\\Entity\\\\User\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - message: "#^Cannot call method getCurrency\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" count: 1 @@ -4345,26 +4340,11 @@ parameters: count: 1 path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\User\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - message: "#^Cannot call method getTimestamp\\(\\) on DateTime\\|null\\.$#" count: 2 path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - - message: "#^Cannot call method getTitle\\(\\) on App\\\\Entity\\\\User\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - - - message: "#^Cannot call method getUserIdentifier\\(\\) on App\\\\Entity\\\\User\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php - - message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceItemDefaultHydrator\\:\\:hydrate\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -4430,11 +4410,6 @@ parameters: count: 1 path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - - message: "#^Cannot call method getBegin\\(\\) on App\\\\Repository\\\\Query\\\\InvoiceQuery\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - message: "#^Cannot call method getCompany\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" count: 1 @@ -4450,11 +4425,6 @@ parameters: count: 1 path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - - message: "#^Cannot call method getEnd\\(\\) on App\\\\Repository\\\\Query\\\\InvoiceQuery\\|null\\.$#" - count: 1 - path: src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php - - message: "#^Cannot call method getLanguage\\(\\) on App\\\\Entity\\\\InvoiceTemplate\\|null\\.$#" count: 1 @@ -4790,11 +4760,6 @@ parameters: count: 1 path: src/Kernel.php - - - message: "#^Method App\\\\Ldap\\\\FormLoginLdapFactory\\:\\:createAuthenticator\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Ldap/FormLoginLdapFactory.php - - message: "#^Method App\\\\Ldap\\\\LdapAuthenticator\\:\\:supports\\(\\) should return bool but returns bool\\|null\\.$#" count: 1 @@ -5735,11 +5700,6 @@ parameters: count: 1 path: src/Repository/TagRepository.php - - - message: "#^Method App\\\\Repository\\\\TagRepository\\:\\:findIdsByTagNameList\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Repository/TagRepository.php - - message: "#^Method App\\\\Repository\\\\TagRepository\\:\\:findTagsByName\\(\\) has parameter \\$tagNames with no value type specified in iterable type array\\.$#" count: 1 diff --git a/public/build/entrypoints.json b/public/build/entrypoints.json index cc96b4b7..d5789bbd 100644 --- a/public/build/entrypoints.json +++ b/public/build/entrypoints.json @@ -24,7 +24,7 @@ "/build/invoice.c8ae95ad.js" ], "css": [ - "/build/invoice.3c80ee80.css" + "/build/invoice.d0ae14e1.css" ] }, "invoice-pdf": { @@ -33,7 +33,7 @@ "/build/invoice-pdf.d86b82ee.js" ], "css": [ - "/build/invoice-pdf.c88953bb.css" + "/build/invoice-pdf.2b749265.css" ] }, "chart": { @@ -68,9 +68,9 @@ "/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW", "/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg", "/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks", - "/build/invoice.3c80ee80.css": "sha384-xjFM2m/EeN7z42ygpt77ll5zAcHTeOjbZFAw2Qcu3IJutgebiAbrXjnH5aJfF5Z6", + "/build/invoice.d0ae14e1.css": "sha384-DZ9f2EsTAIOVoFBZe4jMaW3R6yNVGCtcn3Dq3hxZnWRDdFzhyt1O1iQqRZUyQFTM", "/build/invoice-pdf.d86b82ee.js": "sha384-A0HJqP+MvEqQr1uG8wViCeEWxBRKyS6l8D+Ao4pFYHUA12gCC1gRYhk9I+SJPvZq", - "/build/invoice-pdf.c88953bb.css": "sha384-ZvSi1e+ZKGzvZJUtAPLjzOSTh13N9zRevq44GKdYdBja/DAplGE55saY2Ur+83yv", + "/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7", "/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W", "/build/calendar.8577d9cd.js": "sha384-1qlF+4tr7/zve+wvZef7sVcbUxhK0/yEm5Hbuct8K/ZV4JtENdNY7YRLqOcZ/8Y3", "/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK", diff --git a/public/build/invoice-pdf.2b749265.css b/public/build/invoice-pdf.2b749265.css new file mode 100644 index 00000000..81d28492 --- /dev/null +++ b/public/build/invoice-pdf.2b749265.css @@ -0,0 +1 @@ +body{font-family:sans-serif;font-size:10pt;margin:4px;padding:0}h1,h2,h3,h4,h5,p,table,td,th,tr{margin:0;padding:0}th{border:none;text-align:left}td{vertical-align:top}p{margin-bottom:12px}.text-small{font-size:7pt}.padding-left{padding-left:15px}.text-center{text-align:center}.text-begin,.text-left{text-align:left}.text-end,.text-right{text-align:right}.text-nowrap{white-space:nowrap}.date{font-size:80%;padding-top:6px;text-align:right}table{border-spacing:0;width:100%}p,table.addresses{line-height:14pt}table.addresses{margin-bottom:30px}table.header{border-bottom:1px solid #999;padding-bottom:4px}table.header .title{font-size:140%;font-weight:400}table.footer{border-top:1px solid #999;font-size:80%;line-height:14px;padding-top:8px}table.footer td{vertical-align:top}table.items{border-collapse:collapse;margin-bottom:60px;margin-top:50px}table.items tbody td,table.items thead th{border-bottom:1px solid #ccc}table.items tbody td.first,table.items thead th.first{padding-left:0}table.items tbody td.last,table.items tfoot td.last,table.items thead th.last{padding-right:0}table.items td,table.items th{padding:8px 6px}table.items tfoot td,table.items tfoot th{line-height:1px;padding:20px 0 0}table.items tfoot td.last{padding-left:10px}table.items tr.odd{background-color:#f5f5f5} \ No newline at end of file diff --git a/public/build/invoice-pdf.c88953bb.css b/public/build/invoice-pdf.c88953bb.css deleted file mode 100644 index 5ad0bcca..00000000 --- a/public/build/invoice-pdf.c88953bb.css +++ /dev/null @@ -1 +0,0 @@ -body{font-family:sans-serif;font-size:10pt;margin:4px;padding:0}h1,h2,h3,h4,h5,p,table,td,th,tr{margin:0;padding:0}th{border:none;text-align:left}td{vertical-align:top}p{margin-bottom:12px}.text-small{font-size:7pt}.padding-left{padding-left:15px}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-nowrap{white-space:nowrap}.date{font-size:80%;padding-top:6px;text-align:right}table{border-spacing:0;width:100%}p,table.addresses{line-height:14pt}table.addresses{margin-bottom:30px}table.header{border-bottom:1px solid #999;padding-bottom:4px}table.header .title{font-size:140%;font-weight:400}table.footer{border-top:1px solid #999;font-size:80%;line-height:14px;padding-top:8px}table.footer td{vertical-align:top}table.items{border-collapse:collapse;margin-bottom:60px;margin-top:50px}table.items tbody td,table.items thead th{border-bottom:1px solid #ccc}table.items tbody td.first,table.items thead th.first{padding-left:0}table.items tbody td.last,table.items tfoot td.last,table.items thead th.last{padding-right:0}table.items td,table.items th{padding:8px 6px}table.items tfoot td,table.items tfoot th{line-height:1px;padding:20px 0 0}table.items tfoot td.last{padding-left:10px}table.items tr.odd{background-color:#f5f5f5} \ No newline at end of file diff --git a/public/build/invoice.3c80ee80.css b/public/build/invoice.d0ae14e1.css similarity index 89% rename from public/build/invoice.3c80ee80.css rename to public/build/invoice.d0ae14e1.css index bc9288a0..421955a0 100644 --- a/public/build/invoice.3c80ee80.css +++ b/public/build/invoice.d0ae14e1.css @@ -1 +1 @@ -*,:after,:before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);background-color:var(--bs-body-bg);color:var(--bs-body-color);font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);margin:0;text-align:var(--bs-body-text-align)}hr{border:0;border-top:1px solid;color:inherit;margin:1rem 0;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-weight:500;line-height:1.2;margin-bottom:.5rem;margin-top:0}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-bottom:1rem;margin-top:0}abbr[title]{cursor:help;text-decoration:underline dotted;text-decoration-skip-ink:none}address{font-style:normal;line-height:inherit;margin-bottom:1rem}ol,ul{padding-left:2rem}dl,ol,ul{margin-bottom:1rem;margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{background-color:var(--bs-highlight-bg);padding:.1875em}sub,sup{font-size:.75em;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;font-size:.875em;margin-bottom:1rem;margin-top:0;overflow:auto}pre code{color:inherit;font-size:inherit;word-break:normal}code{word-wrap:break-word;color:var(--bs-code-color);font-size:.875em}a>code{color:inherit}kbd{background-color:var(--bs-body-color);border-radius:.25rem;color:var(--bs-body-bg);font-size:.875em;padding:.1875rem .375rem}kbd kbd{font-size:1em;padding:0}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{border-collapse:collapse;caption-side:bottom}caption{color:#6c757d;padding-bottom:.5rem;padding-top:.5rem;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border:0 solid;border-color:inherit}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{border-style:none;padding:0}textarea{resize:vertical}fieldset{border:0;margin:0;min-width:0;padding:0}legend{float:left;font-size:calc(1.275rem + .3vw);line-height:inherit;margin-bottom:.5rem;padding:0;width:100%}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{-webkit-appearance:button;font:inherit}output{display:inline-block}iframe{border:0}summary{cursor:pointer;display:list-item}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-inline,.list-unstyled{list-style:none;padding-left:0}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{font-size:1.25rem;margin-bottom:1rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{color:#6c757d;font-size:.875em;margin-bottom:1rem;margin-top:-1rem}.blockquote-footer:before{content:"\2014\00A0"}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-left:calc(var(--bs-gutter-x)*-.5);margin-right:calc(var(--bs-gutter-x)*-.5);margin-top:calc(var(--bs-gutter-y)*-1)}.row>*{flex-shrink:0;margin-top:var(--bs-gutter-y);max-width:100%;padding-left:calc(var(--bs-gutter-x)*.5);padding-right:calc(var(--bs-gutter-x)*.5);width:100%}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333%}.col-2{flex:0 0 auto;width:16.66667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333%}.col-5{flex:0 0 auto;width:41.66667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333%}.col-8{flex:0 0 auto;width:66.66667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333%}.col-11{flex:0 0 auto;width:91.66667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333%}.col-sm-2{flex:0 0 auto;width:16.66667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333%}.col-sm-5{flex:0 0 auto;width:41.66667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333%}.col-sm-8{flex:0 0 auto;width:66.66667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333%}.col-sm-11{flex:0 0 auto;width:91.66667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333%}.col-md-2{flex:0 0 auto;width:16.66667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333%}.col-md-5{flex:0 0 auto;width:41.66667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333%}.col-md-8{flex:0 0 auto;width:66.66667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333%}.col-md-11{flex:0 0 auto;width:91.66667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333%}.col-lg-2{flex:0 0 auto;width:16.66667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333%}.col-lg-5{flex:0 0 auto;width:41.66667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333%}.col-lg-8{flex:0 0 auto;width:66.66667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333%}.col-lg-11{flex:0 0 auto;width:91.66667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333%}.col-xl-2{flex:0 0 auto;width:16.66667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333%}.col-xl-5{flex:0 0 auto;width:41.66667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333%}.col-xl-8{flex:0 0 auto;width:66.66667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333%}.col-xl-11{flex:0 0 auto;width:91.66667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333%}.col-xxl-2{flex:0 0 auto;width:16.66667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333%}.col-xxl-5{flex:0 0 auto;width:41.66667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333%}.col-xxl-8{flex:0 0 auto;width:66.66667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333%}.col-xxl-11{flex:0 0 auto;width:91.66667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333%}.offset-xxl-2{margin-left:16.66667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333%}.offset-xxl-5{margin-left:41.66667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333%}.offset-xxl-8{margin-left:66.66667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333%}.offset-xxl-11{margin-left:91.66667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0,0,0,.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0,0,0,.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0,0,0,.075);border-color:var(--bs-table-border-color);color:var(--bs-table-color);margin-bottom:1rem;vertical-align:top;width:100%}.table>:not(caption)>*>*{background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg);padding:.5rem}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped-columns>:not(caption)>tr>:nth-child(2n),.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000}.table-primary,.table-secondary{border-color:var(--bs-table-border-color);color:var(--bs-table-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000}.table-info,.table-success{border-color:var(--bs-table-border-color);color:var(--bs-table-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000}.table-danger,.table-warning{border-color:var(--bs-table-border-color);color:var(--bs-table-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000}.table-dark,.table-light{border-color:var(--bs-table-border-color);color:var(--bs-table-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff}.table-responsive{-webkit-overflow-scrolling:touch;overflow-x:auto}@media (max-width:575.98px){.table-responsive-sm{-webkit-overflow-scrolling:touch;overflow-x:auto}}@media (max-width:767.98px){.table-responsive-md{-webkit-overflow-scrolling:touch;overflow-x:auto}}@media (max-width:991.98px){.table-responsive-lg{-webkit-overflow-scrolling:touch;overflow-x:auto}}@media (max-width:1199.98px){.table-responsive-xl{-webkit-overflow-scrolling:touch;overflow-x:auto}}@media (max-width:1399.98px){.table-responsive-xxl{-webkit-overflow-scrolling:touch;overflow-x:auto}}.list-group{--bs-list-group-color:#212529;--bs-list-group-bg:#fff;--bs-list-group-border-color:rgba(0,0,0,.125);--bs-list-group-border-width:1px;--bs-list-group-border-radius:0.375rem;--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#495057;--bs-list-group-action-hover-color:#495057;--bs-list-group-action-hover-bg:#f8f9fa;--bs-list-group-action-active-color:#212529;--bs-list-group-action-active-bg:#e9ecef;--bs-list-group-disabled-color:#6c757d;--bs-list-group-disabled-bg:#fff;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;border-radius:var(--bs-list-group-border-radius);display:flex;flex-direction:column;margin-bottom:0;padding-left:0}.list-group-numbered{counter-reset:section;list-style-type:none}.list-group-numbered>.list-group-item:before{content:counters(section,".") ". ";counter-increment:section}.list-group-item-action{color:var(--bs-list-group-action-color);text-align:inherit;width:100%}.list-group-item-action:focus,.list-group-item-action:hover{background-color:var(--bs-list-group-action-hover-bg);color:var(--bs-list-group-action-hover-color);text-decoration:none;z-index:1}.list-group-item-action:active{background-color:var(--bs-list-group-action-active-bg);color:var(--bs-list-group-action-active-color)}.list-group-item{background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color);color:var(--bs-list-group-color);display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);position:relative;text-decoration:none}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{background-color:var(--bs-list-group-disabled-bg);color:var(--bs-list-group-disabled-color);pointer-events:none}.list-group-item.active{background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color);color:var(--bs-list-group-active-color);z-index:2}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{border-top-width:var(--bs-list-group-border-width);margin-top:calc(var(--bs-list-group-border-width)*-1)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-md>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{background-color:#cfe2ff;color:#084298}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{background-color:#bacbe6;color:#084298}.list-group-item-primary.list-group-item-action.active{background-color:#084298;border-color:#084298;color:#fff}.list-group-item-secondary{background-color:#e2e3e5;color:#41464b}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{background-color:#cbccce;color:#41464b}.list-group-item-secondary.list-group-item-action.active{background-color:#41464b;border-color:#41464b;color:#fff}.list-group-item-success{background-color:#d1e7dd;color:#0f5132}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{background-color:#bcd0c7;color:#0f5132}.list-group-item-success.list-group-item-action.active{background-color:#0f5132;border-color:#0f5132;color:#fff}.list-group-item-info{background-color:#cff4fc;color:#055160}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{background-color:#badce3;color:#055160}.list-group-item-info.list-group-item-action.active{background-color:#055160;border-color:#055160;color:#fff}.list-group-item-warning{background-color:#fff3cd;color:#664d03}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{background-color:#e6dbb9;color:#664d03}.list-group-item-warning.list-group-item-action.active{background-color:#664d03;border-color:#664d03;color:#fff}.list-group-item-danger{background-color:#f8d7da;color:#842029}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{background-color:#dfc2c4;color:#842029}.list-group-item-danger.list-group-item-action.active{background-color:#842029;border-color:#842029;color:#fff}.list-group-item-light{background-color:#fefefe;color:#636464}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{background-color:#e5e5e5;color:#636464}.list-group-item-light.list-group-item-action.active{background-color:#636464;border-color:#636464;color:#fff}.list-group-item-dark{background-color:#d3d3d4;color:#141619}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{background-color:#bebebf;color:#141619}.list-group-item-dark.list-group-item-action.active{background-color:#141619;border-color:#141619;color:#fff}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-border-width:1px;--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:0.375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(0.375rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(0,0,0,.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#fff;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;word-wrap:break-word;background-clip:border-box;background-color:var(--bs-card-bg);border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius);display:flex;flex-direction:column;height:var(--bs-card-height);min-width:0;position:relative}.card>hr{margin-left:0;margin-right:0}.card>.list-group{border-bottom:inherit;border-top:inherit}.card>.list-group:first-child{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius);border-top-width:0}.card>.list-group:last-child{border-bottom-left-radius:var(--bs-card-inner-border-radius);border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{color:var(--bs-card-color);flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(var(--bs-card-title-spacer-y)*-.5)}.card-subtitle,.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color);color:var(--bs-card-cap-color);margin-bottom:0;padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color);color:var(--bs-card-cap-color);padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{border-bottom:0;margin-bottom:calc(var(--bs-card-cap-padding-y)*-1);margin-left:calc(var(--bs-card-cap-padding-x)*-.5);margin-right:calc(var(--bs-card-cap-padding-x)*-.5)}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-left:calc(var(--bs-card-cap-padding-x)*-.5);margin-right:calc(var(--bs-card-cap-padding-x)*-.5)}.card-img-overlay{border-radius:var(--bs-card-inner-border-radius);bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);position:absolute;right:0;top:0}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-left-radius:var(--bs-card-inner-border-radius);border-bottom-right-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{border-left:0;margin-left:0}.card-group>.card:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body.invoice_print{--bs-body-font-size:13px;background-color:#eee}body.invoice_print .table.no-border,body.invoice_print .table.no-border td,body.invoice_print .table.no-border th{border:0}body.invoice_print .h1,body.invoice_print .h2,body.invoice_print .h3,body.invoice_print h1,body.invoice_print h2,body.invoice_print h3{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body.invoice_print .mt-2{margin-top:2em}body.invoice_print .mb-3{margin-bottom:3em}body.invoice_print .pt-1{padding-top:1em}body.invoice_print .pb-4{padding-bottom:4em}body.invoice_print .ps-0{padding-left:0}body.invoice_print .bt-1{border-top:1px solid #000}body.invoice_print .pull-right{float:right}body.invoice_print .text-end{text-align:right}body.invoice_print .invoice{background:#fff;border:none;box-shadow:0 0 20px rgba(80,80,80,.7);margin:105px auto 30px;max-width:210mm;min-height:297mm;padding:50px 65px;position:relative}body.invoice_print .page-header{font-size:20px;margin:10px 0 20px}body.invoice_print .page-header>.small,body.invoice_print .page-header>small{color:#666;display:block;margin-top:5px}body.invoice_print .page-header .h3,body.invoice_print .page-header h3{border-color:#ddd}body.invoice_print div.invoice-address{margin-bottom:30px;margin-top:30px}body.invoice_print table.invoice-meta th{padding-right:40px}body.invoice_print table.invoice-sum th{width:70%}body.invoice_print table.invoice-sum td,body.invoice_print table.invoice-sum th{text-align:right}body.invoice_print .invoice-items .table thead th{border-bottom:1px solid #ddd;font-weight:700;padding-bottom:10px}body.invoice_print .invoice-items .table tfoot{border-top:1px solid #ddd}body.invoice_print .invoice-items .table tfoot tr:first-child td,body.invoice_print .invoice-items .table tfoot tr:first-child th{padding-top:15px}body.invoice_print .invoice-items .table tfoot tr td,body.invoice_print .invoice-items .table tfoot tr th{border:none;padding:4px 8px}body.invoice_print .footer{border-top:1px solid #ccc;font-size:.8em;margin-top:50px;padding-top:10px;width:100%}body.invoice_print #freelancer-invoice{box-sizing:border-box}body.invoice_print #freelancer-invoice .h1,body.invoice_print #freelancer-invoice h1{font:700 100% sans-serif;letter-spacing:.5em;text-align:center;text-transform:uppercase}body.invoice_print #freelancer-invoice .h2,body.invoice_print #freelancer-invoice h2{font:700 1.5em sans-serif;margin-bottom:1em}body.invoice_print #freelancer-invoice table{border-collapse:separate;border-spacing:0;width:100%}body.invoice_print #freelancer-invoice header:after{clear:both;content:"";display:table;font-size:1em}body.invoice_print #freelancer-invoice header address{float:left;font-size:.7em;font-style:normal;line-height:1.25;margin:0 1em 1em 0}body.invoice_print #freelancer-invoice article address{float:left;font-size:1em}body.invoice_print #freelancer-invoice article.address{margin:1em 0 1.5em}body.invoice_print #freelancer-invoice article:after{clear:both;content:"";display:table}body.invoice_print #freelancer-invoice article .h1,body.invoice_print #freelancer-invoice article h1{clip:rect(0 0 0 0);position:absolute}body.invoice_print #freelancer-invoice article p{margin-bottom:20px}body.invoice_print #freelancer-invoice table.balance:after,body.invoice_print #freelancer-invoice table.meta:after{clear:both;content:"";display:table}body.invoice_print #freelancer-invoice table.meta{float:right;font-size:85%;width:50%}body.invoice_print #freelancer-invoice table.meta th{font-weight:400;text-align:right}body.invoice_print #freelancer-invoice table.meta td{text-align:right;width:120px}body.invoice_print #freelancer-invoice table.balance{float:right;margin-top:1em}body.invoice_print #freelancer-invoice table.balance th{font-weight:400;text-align:right}body.invoice_print #freelancer-invoice table.balance td{line-height:22px;text-align:right;width:140px}body.invoice_print #freelancer-invoice table.balance .total{font-weight:700}body.invoice_print #freelancer-invoice table.inventory{border-bottom:1px solid #000;clear:both;margin-top:2em;width:100%}body.invoice_print #freelancer-invoice table.inventory td,body.invoice_print #freelancer-invoice table.inventory th{padding:5px}body.invoice_print #freelancer-invoice table.inventory thead th{border-width:0 0 1px;border-bottom:1px solid #000;font-weight:700;padding-bottom:15px}body.invoice_print #freelancer-invoice table.inventory tbody tr:first-child td{padding-top:15px}body.invoice_print #freelancer-invoice table.inventory tbody tr:last-child td{padding-bottom:15px}body.invoice_print #freelancer-invoice .footer{border-color:#000}@media print{body.invoice_print *{-webkit-print-color-adjust:exact}body.invoice_print .wrapper,body.invoice_print body{background-color:#fff;margin:0;padding:0}body.invoice_print #freelancer-invoice .add,body.invoice_print #freelancer-invoice .cut,body.invoice_print #freelancer-invoice span:empty{display:none}body.invoice_print .invoice{box-shadow:unset;font-size:90%;margin:1em 2em;max-width:unset;min-height:unset;padding:0;position:unset;width:unset}} \ No newline at end of file +*,:after,:before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);background-color:var(--bs-body-bg);color:var(--bs-body-color);font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);margin:0;text-align:var(--bs-body-text-align)}hr{border:0;border-top:1px solid;color:inherit;margin:1rem 0;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-weight:500;line-height:1.2;margin-bottom:.5rem;margin-top:0}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-bottom:1rem;margin-top:0}abbr[title]{cursor:help;text-decoration:underline dotted;text-decoration-skip-ink:none}address{font-style:normal;line-height:inherit;margin-bottom:1rem}ol,ul{padding-left:2rem}dl,ol,ul{margin-bottom:1rem;margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{background-color:var(--bs-highlight-bg);padding:.1875em}sub,sup{font-size:.75em;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;font-size:.875em;margin-bottom:1rem;margin-top:0;overflow:auto}pre code{color:inherit;font-size:inherit;word-break:normal}code{word-wrap:break-word;color:var(--bs-code-color);font-size:.875em}a>code{color:inherit}kbd{background-color:var(--bs-body-color);border-radius:.25rem;color:var(--bs-body-bg);font-size:.875em;padding:.1875rem .375rem}kbd kbd{font-size:1em;padding:0}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{border-collapse:collapse;caption-side:bottom}caption{color:#6c757d;padding-bottom:.5rem;padding-top:.5rem;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border:0 solid;border-color:inherit}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{border-style:none;padding:0}textarea{resize:vertical}fieldset{border:0;margin:0;min-width:0;padding:0}legend{float:left;font-size:calc(1.275rem + .3vw);line-height:inherit;margin-bottom:.5rem;padding:0;width:100%}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{-webkit-appearance:button;font:inherit}output{display:inline-block}iframe{border:0}summary{cursor:pointer;display:list-item}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-inline,.list-unstyled{list-style:none;padding-left:0}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{font-size:1.25rem;margin-bottom:1rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{color:#6c757d;font-size:.875em;margin-bottom:1rem;margin-top:-1rem}.blockquote-footer:before{content:"\2014\00A0"}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-left:calc(var(--bs-gutter-x)*-.5);margin-right:calc(var(--bs-gutter-x)*-.5);margin-top:calc(var(--bs-gutter-y)*-1)}.row>*{flex-shrink:0;margin-top:var(--bs-gutter-y);max-width:100%;padding-left:calc(var(--bs-gutter-x)*.5);padding-right:calc(var(--bs-gutter-x)*.5);width:100%}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333%}.col-2{flex:0 0 auto;width:16.66667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333%}.col-5{flex:0 0 auto;width:41.66667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333%}.col-8{flex:0 0 auto;width:66.66667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333%}.col-11{flex:0 0 auto;width:91.66667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333%}.col-sm-2{flex:0 0 auto;width:16.66667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333%}.col-sm-5{flex:0 0 auto;width:41.66667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333%}.col-sm-8{flex:0 0 auto;width:66.66667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333%}.col-sm-11{flex:0 0 auto;width:91.66667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333%}.col-md-2{flex:0 0 auto;width:16.66667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333%}.col-md-5{flex:0 0 auto;width:41.66667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333%}.col-md-8{flex:0 0 auto;width:66.66667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333%}.col-md-11{flex:0 0 auto;width:91.66667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333%}.col-lg-2{flex:0 0 auto;width:16.66667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333%}.col-lg-5{flex:0 0 auto;width:41.66667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333%}.col-lg-8{flex:0 0 auto;width:66.66667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333%}.col-lg-11{flex:0 0 auto;width:91.66667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333%}.col-xl-2{flex:0 0 auto;width:16.66667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333%}.col-xl-5{flex:0 0 auto;width:41.66667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333%}.col-xl-8{flex:0 0 auto;width:66.66667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333%}.col-xl-11{flex:0 0 auto;width:91.66667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333%}.col-xxl-2{flex:0 0 auto;width:16.66667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333%}.col-xxl-5{flex:0 0 auto;width:41.66667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333%}.col-xxl-8{flex:0 0 auto;width:66.66667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333%}.col-xxl-11{flex:0 0 auto;width:91.66667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333%}.offset-xxl-2{margin-left:16.66667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333%}.offset-xxl-5{margin-left:41.66667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333%}.offset-xxl-8{margin-left:66.66667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333%}.offset-xxl-11{margin-left:91.66667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0,0,0,.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0,0,0,.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0,0,0,.075);border-color:var(--bs-table-border-color);color:var(--bs-table-color);margin-bottom:1rem;vertical-align:top;width:100%}.table>:not(caption)>*>*{background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg);padding:.5rem}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped-columns>:not(caption)>tr>:nth-child(2n),.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000}.table-primary,.table-secondary{border-color:var(--bs-table-border-color);color:var(--bs-table-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000}.table-info,.table-success{border-color:var(--bs-table-border-color);color:var(--bs-table-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000}.table-danger,.table-warning{border-color:var(--bs-table-border-color);color:var(--bs-table-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000}.table-dark,.table-light{border-color:var(--bs-table-border-color);color:var(--bs-table-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff}.table-responsive{-webkit-overflow-scrolling:touch;overflow-x:auto}@media (max-width:575.98px){.table-responsive-sm{-webkit-overflow-scrolling:touch;overflow-x:auto}}@media (max-width:767.98px){.table-responsive-md{-webkit-overflow-scrolling:touch;overflow-x:auto}}@media (max-width:991.98px){.table-responsive-lg{-webkit-overflow-scrolling:touch;overflow-x:auto}}@media (max-width:1199.98px){.table-responsive-xl{-webkit-overflow-scrolling:touch;overflow-x:auto}}@media (max-width:1399.98px){.table-responsive-xxl{-webkit-overflow-scrolling:touch;overflow-x:auto}}.list-group{--bs-list-group-color:#212529;--bs-list-group-bg:#fff;--bs-list-group-border-color:rgba(0,0,0,.125);--bs-list-group-border-width:1px;--bs-list-group-border-radius:0.375rem;--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#495057;--bs-list-group-action-hover-color:#495057;--bs-list-group-action-hover-bg:#f8f9fa;--bs-list-group-action-active-color:#212529;--bs-list-group-action-active-bg:#e9ecef;--bs-list-group-disabled-color:#6c757d;--bs-list-group-disabled-bg:#fff;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;border-radius:var(--bs-list-group-border-radius);display:flex;flex-direction:column;margin-bottom:0;padding-left:0}.list-group-numbered{counter-reset:section;list-style-type:none}.list-group-numbered>.list-group-item:before{content:counters(section,".") ". ";counter-increment:section}.list-group-item-action{color:var(--bs-list-group-action-color);text-align:inherit;width:100%}.list-group-item-action:focus,.list-group-item-action:hover{background-color:var(--bs-list-group-action-hover-bg);color:var(--bs-list-group-action-hover-color);text-decoration:none;z-index:1}.list-group-item-action:active{background-color:var(--bs-list-group-action-active-bg);color:var(--bs-list-group-action-active-color)}.list-group-item{background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color);color:var(--bs-list-group-color);display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);position:relative;text-decoration:none}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{background-color:var(--bs-list-group-disabled-bg);color:var(--bs-list-group-disabled-color);pointer-events:none}.list-group-item.active{background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color);color:var(--bs-list-group-active-color);z-index:2}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{border-top-width:var(--bs-list-group-border-width);margin-top:calc(var(--bs-list-group-border-width)*-1)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-md>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:var(--bs-list-group-border-radius)}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-left-width:0;border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{border-left-width:var(--bs-list-group-border-width);margin-left:calc(var(--bs-list-group-border-width)*-1)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{background-color:#cfe2ff;color:#084298}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{background-color:#bacbe6;color:#084298}.list-group-item-primary.list-group-item-action.active{background-color:#084298;border-color:#084298;color:#fff}.list-group-item-secondary{background-color:#e2e3e5;color:#41464b}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{background-color:#cbccce;color:#41464b}.list-group-item-secondary.list-group-item-action.active{background-color:#41464b;border-color:#41464b;color:#fff}.list-group-item-success{background-color:#d1e7dd;color:#0f5132}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{background-color:#bcd0c7;color:#0f5132}.list-group-item-success.list-group-item-action.active{background-color:#0f5132;border-color:#0f5132;color:#fff}.list-group-item-info{background-color:#cff4fc;color:#055160}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{background-color:#badce3;color:#055160}.list-group-item-info.list-group-item-action.active{background-color:#055160;border-color:#055160;color:#fff}.list-group-item-warning{background-color:#fff3cd;color:#664d03}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{background-color:#e6dbb9;color:#664d03}.list-group-item-warning.list-group-item-action.active{background-color:#664d03;border-color:#664d03;color:#fff}.list-group-item-danger{background-color:#f8d7da;color:#842029}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{background-color:#dfc2c4;color:#842029}.list-group-item-danger.list-group-item-action.active{background-color:#842029;border-color:#842029;color:#fff}.list-group-item-light{background-color:#fefefe;color:#636464}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{background-color:#e5e5e5;color:#636464}.list-group-item-light.list-group-item-action.active{background-color:#636464;border-color:#636464;color:#fff}.list-group-item-dark{background-color:#d3d3d4;color:#141619}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{background-color:#bebebf;color:#141619}.list-group-item-dark.list-group-item-action.active{background-color:#141619;border-color:#141619;color:#fff}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-border-width:1px;--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:0.375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(0.375rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(0,0,0,.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#fff;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;word-wrap:break-word;background-clip:border-box;background-color:var(--bs-card-bg);border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius);display:flex;flex-direction:column;height:var(--bs-card-height);min-width:0;position:relative}.card>hr{margin-left:0;margin-right:0}.card>.list-group{border-bottom:inherit;border-top:inherit}.card>.list-group:first-child{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius);border-top-width:0}.card>.list-group:last-child{border-bottom-left-radius:var(--bs-card-inner-border-radius);border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{color:var(--bs-card-color);flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(var(--bs-card-title-spacer-y)*-.5)}.card-subtitle,.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color);color:var(--bs-card-cap-color);margin-bottom:0;padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color);color:var(--bs-card-cap-color);padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{border-bottom:0;margin-bottom:calc(var(--bs-card-cap-padding-y)*-1);margin-left:calc(var(--bs-card-cap-padding-x)*-.5);margin-right:calc(var(--bs-card-cap-padding-x)*-.5)}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-left:calc(var(--bs-card-cap-padding-x)*-.5);margin-right:calc(var(--bs-card-cap-padding-x)*-.5)}.card-img-overlay{border-radius:var(--bs-card-inner-border-radius);bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);position:absolute;right:0;top:0}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-left-radius:var(--bs-card-inner-border-radius);border-bottom-right-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{border-left:0;margin-left:0}.card-group>.card:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body.invoice_print{--bs-body-font-size:13px;background-color:#eee}body.invoice_print .table.no-border,body.invoice_print .table.no-border td,body.invoice_print .table.no-border th{border:0}body.invoice_print .h1,body.invoice_print .h2,body.invoice_print .h3,body.invoice_print h1,body.invoice_print h2,body.invoice_print h3{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body.invoice_print .mt-2{margin-top:2em}body.invoice_print .mb-3{margin-bottom:3em}body.invoice_print .pt-1{padding-top:1em}body.invoice_print .pb-4{padding-bottom:4em}body.invoice_print .ps-0{padding-left:0}body.invoice_print .bt-1{border-top:1px solid #000}body.invoice_print .pull-right{float:right}body.invoice_print .text-end{text-align:right}body.invoice_print .invoice{background:#fff;border:none;box-shadow:0 0 20px rgba(80,80,80,.7);margin:105px auto 30px;max-width:210mm;min-height:297mm;padding:50px 65px;position:relative}body.invoice_print .page-header{font-size:20px;margin:10px 0 20px}body.invoice_print .page-header>.small,body.invoice_print .page-header>small{display:block;margin-top:5px}body.invoice_print .page-header .h3,body.invoice_print .page-header h3{border-color:#ddd}body.invoice_print div.invoice-address{margin-bottom:30px;margin-top:30px}body.invoice_print table.invoice-meta th{padding-right:40px}body.invoice_print table.invoice-sum th{width:70%}body.invoice_print table.invoice-sum td,body.invoice_print table.invoice-sum th{text-align:right}body.invoice_print .text-left{text-align:left}body.invoice_print .text-right{text-align:right}body.invoice_print .invoice-items .table thead th{border-bottom:1px solid #ddd;font-weight:700;padding-bottom:10px}body.invoice_print .invoice-items .table tfoot{border-top:1px solid #ddd}body.invoice_print .invoice-items .table tfoot tr:first-child td,body.invoice_print .invoice-items .table tfoot tr:first-child th{padding-top:15px}body.invoice_print .invoice-items .table tfoot tr td,body.invoice_print .invoice-items .table tfoot tr th{border:none;padding:4px 8px}body.invoice_print .footer{border-top:1px solid #ccc;font-size:.8em;margin-top:50px;padding-top:10px;width:100%}body.invoice_print #freelancer-invoice{box-sizing:border-box}body.invoice_print #freelancer-invoice .h1,body.invoice_print #freelancer-invoice h1{font:700 100% sans-serif;letter-spacing:.5em;text-align:center;text-transform:uppercase}body.invoice_print #freelancer-invoice .h2,body.invoice_print #freelancer-invoice h2{font:700 1.5em sans-serif;margin-bottom:1em}body.invoice_print #freelancer-invoice table{border-collapse:separate;border-spacing:0;width:100%}body.invoice_print #freelancer-invoice header:after{clear:both;content:"";display:table;font-size:1em}body.invoice_print #freelancer-invoice header address{float:left;font-size:.7em;font-style:normal;line-height:1.25;margin:0 1em 1em 0}body.invoice_print #freelancer-invoice article address{float:left;font-size:1em}body.invoice_print #freelancer-invoice article.address{margin:1em 0 1.5em}body.invoice_print #freelancer-invoice article:after{clear:both;content:"";display:table}body.invoice_print #freelancer-invoice article .h1,body.invoice_print #freelancer-invoice article h1{clip:rect(0 0 0 0);position:absolute}body.invoice_print #freelancer-invoice article p{margin-bottom:20px}body.invoice_print #freelancer-invoice table.balance:after,body.invoice_print #freelancer-invoice table.meta:after{clear:both;content:"";display:table}body.invoice_print #freelancer-invoice table.meta{float:right;font-size:85%;width:50%}body.invoice_print #freelancer-invoice table.meta th{font-weight:400;text-align:right}body.invoice_print #freelancer-invoice table.meta td{text-align:right;width:120px}body.invoice_print #freelancer-invoice table.balance{float:right;margin-top:1em}body.invoice_print #freelancer-invoice table.balance th{font-weight:400;text-align:right}body.invoice_print #freelancer-invoice table.balance td{line-height:22px;text-align:right;width:140px}body.invoice_print #freelancer-invoice table.balance .total{font-weight:700}body.invoice_print #freelancer-invoice table.inventory{border-bottom:1px solid #000;clear:both;margin-top:2em;width:100%}body.invoice_print #freelancer-invoice table.inventory td,body.invoice_print #freelancer-invoice table.inventory th{padding:5px}body.invoice_print #freelancer-invoice table.inventory thead th{border-width:0 0 1px;border-bottom:1px solid #000;font-weight:700;padding-bottom:15px}body.invoice_print #freelancer-invoice table.inventory tbody tr:first-child td{padding-top:15px}body.invoice_print #freelancer-invoice table.inventory tbody tr:last-child td{padding-bottom:15px}body.invoice_print #freelancer-invoice .footer{border-color:#000}@media print{body.invoice_print *{-webkit-print-color-adjust:exact}body.invoice_print .wrapper,body.invoice_print body{background-color:#fff;margin:0;padding:0}body.invoice_print #freelancer-invoice .add,body.invoice_print #freelancer-invoice .cut,body.invoice_print #freelancer-invoice span:empty{display:none}body.invoice_print .invoice{box-shadow:unset;font-size:90%;margin:1em 2em;max-width:unset;min-height:unset;padding:0;position:unset;width:unset}} \ No newline at end of file diff --git a/public/build/manifest.json b/public/build/manifest.json index a1e2b893..22b8c397 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -3,9 +3,9 @@ "build/app.js": "/build/app.cd312ca8.js", "build/export-pdf.css": "/build/export-pdf.d8a6c23b.css", "build/export-pdf.js": "/build/export-pdf.587575e7.js", - "build/invoice.css": "/build/invoice.3c80ee80.css", + "build/invoice.css": "/build/invoice.d0ae14e1.css", "build/invoice.js": "/build/invoice.c8ae95ad.js", - "build/invoice-pdf.css": "/build/invoice-pdf.c88953bb.css", + "build/invoice-pdf.css": "/build/invoice-pdf.2b749265.css", "build/invoice-pdf.js": "/build/invoice-pdf.d86b82ee.js", "build/chart.js": "/build/chart.f5becfac.js", "build/calendar.css": "/build/calendar.bb473428.css", diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php index 75105c53..84b2146f 100644 --- a/src/API/TimesheetController.php +++ b/src/API/TimesheetController.php @@ -76,6 +76,7 @@ final class TimesheetController extends BaseApiController #[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiToken')] #[Rest\QueryParam(name: 'user', requirements: '\d+|all', strict: true, nullable: true, description: "User ID to filter timesheets. Needs permission 'view_other_timesheet', pass 'all' to fetch data for all user (default: current user)")] + #[Rest\QueryParam(name: 'users', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of user IDs to filter, e.g.: users[]=1&users[]=2 (ignored if user=all)')] #[Rest\QueryParam(name: 'customer', requirements: '\d+', strict: true, nullable: true, description: 'Customer ID to filter timesheets')] #[Rest\QueryParam(name: 'customers', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of customer IDs to filter, e.g.: customers[]=1&customers[]=2')] #[Rest\QueryParam(name: 'project', requirements: '\d+', strict: true, nullable: true, description: 'Project ID to filter timesheets')] @@ -98,24 +99,32 @@ final class TimesheetController extends BaseApiController public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response { $query = new TimesheetQuery(false); - $query->setUser($this->getUser()); + $seeAll = false; if ($this->isGranted('view_other_timesheet')) { + /** @var array $users */ + $users = $paramFetcher->get('users'); $userId = $paramFetcher->get('user'); - if (\is_string($userId) && $userId !== '') { - if ('all' === $userId) { - $query->setUser(null); - } else { - /** @var User|null $user */ - $user = $userRepository->find($userId); - if ($user === null) { - throw $this->createNotFoundException('Unknown user: ' . $userId); - } - $query->setUser($user); + + if ('all' === $userId) { + $seeAll = true; + } elseif (\is_string($userId) && $userId !== '') { + $users[] = (int) $userId; + } + + if (!$seeAll) { + foreach ($userRepository->findByIds($users) as $user) { + $query->addUser($user); } } } + if ($seeAll) { + $query->setUser(null); + } elseif (!$query->hasUsers()) { + $query->setUser($this->getUser()); + } + /** @var array $customers */ $customers = $paramFetcher->get('customers'); $customer = $paramFetcher->get('customer'); @@ -476,6 +485,7 @@ final class TimesheetController extends BaseApiController #[IsGranted('start', 'timesheet')] #[OA\Response(response: 200, description: 'Restarts a timesheet record for the same customer, project, activity combination. The current user will be the owner of the new record. Kimai tries to stop running records, which is expected to fail depending on the configured rules. Data will be copied from the original record if requested.', content: new OA\JsonContent(ref: '#/components/schemas/TimesheetEntity'))] #[OA\Parameter(name: 'id', in: 'path', description: 'Timesheet record ID to restart', required: true)] + #[Rest\Get(path: '/{id}/restart', name: 'restart_timesheet_get', requirements: ['id' => '\d+'])] #[Rest\Patch(path: '/{id}/restart', name: 'restart_timesheet', requirements: ['id' => '\d+'])] #[ApiSecurity(name: 'apiUser')] #[ApiSecurity(name: 'apiToken')] diff --git a/src/Constants.php b/src/Constants.php index c78523dc..b9aea5dd 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ class Constants /** * The current release version */ - public const VERSION = '2.0.14'; + public const VERSION = '2.0.15'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 20014; + public const VERSION_ID = 20015; /** * The software name */ diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 6872a1fb..2f6b74fd 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -672,6 +672,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem $this->id = null; } + // field will not be set, if it contains a value + $this->modifiedAt = null; $this->exported = false; $currentMeta = $this->meta; diff --git a/src/Form/Toolbar/ExportToolbarForm.php b/src/Form/Toolbar/ExportToolbarForm.php index 05d893d1..9e14ba81 100644 --- a/src/Form/Toolbar/ExportToolbarForm.php +++ b/src/Form/Toolbar/ExportToolbarForm.php @@ -25,7 +25,7 @@ final class ExportToolbarForm extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $this->addSearchTermInputField($builder); - $this->addDateRange($builder, ['timezone' => $options['timezone']], false, true); + $this->addDateRange($builder, ['timezone' => $options['timezone']]); $this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true); $this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true); $this->addActivitySelect($builder, [], true, true, false); diff --git a/src/Form/Toolbar/InvoiceToolbarForm.php b/src/Form/Toolbar/InvoiceToolbarForm.php index 68cdeae0..dccac8a1 100644 --- a/src/Form/Toolbar/InvoiceToolbarForm.php +++ b/src/Form/Toolbar/InvoiceToolbarForm.php @@ -26,7 +26,7 @@ final class InvoiceToolbarForm extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $this->addSearchTermInputField($builder); - $this->addDateRange($builder, ['timezone' => $options['timezone']], false, true); + $this->addDateRange($builder, ['timezone' => $options['timezone']]); $this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true); $this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true); $this->addActivitySelect($builder, [], true, true, false); diff --git a/src/Form/Toolbar/ToolbarFormTrait.php b/src/Form/Toolbar/ToolbarFormTrait.php index f17e2fb3..9e1e0334 100644 --- a/src/Form/Toolbar/ToolbarFormTrait.php +++ b/src/Form/Toolbar/ToolbarFormTrait.php @@ -155,10 +155,10 @@ trait ToolbarFormTrait ]); } - protected function addDateRange(FormBuilderInterface $builder, array $options, bool $allowEmpty = true, bool $required = false): void + protected function addDateRange(FormBuilderInterface $builder, array $options, bool $allowEmpty = true): void { $params = [ - 'required' => $required, + 'required' => !$allowEmpty, 'allow_empty' => $allowEmpty, ]; diff --git a/src/Form/Type/DateRangeType.php b/src/Form/Type/DateRangeType.php index de9b2e9f..7588abe4 100644 --- a/src/Form/Type/DateRangeType.php +++ b/src/Form/Type/DateRangeType.php @@ -134,6 +134,10 @@ final class DateRangeType extends AbstractType return $range; } + if ($dates === null) { + throw new TransformationFailedException('Date range missing'); + } + if (preg_match($pattern, $dates) !== 1) { throw new TransformationFailedException('Invalid date range given'); } diff --git a/src/Form/Type/TagsSelectType.php b/src/Form/Type/TagsSelectType.php index 70865317..b2abd2d8 100644 --- a/src/Form/Type/TagsSelectType.php +++ b/src/Form/Type/TagsSelectType.php @@ -76,9 +76,20 @@ final class TagsSelectType extends AbstractType $foundTagNames[] = $tag->getName(); } - /** @var array $newNames */ - $newNames = array_diff($newNames, $foundTagNames); - foreach ($newNames as $name) { + /** @var array $newNamesCreate */ + $newNamesCreate = array_udiff($newNames, $foundTagNames, function (mixed $userTag, mixed $existingTag) { + if (!\is_string($userTag) || !\is_string($existingTag)) { + return -1; + } + + if (mb_strtolower($userTag) === mb_strtolower($existingTag)) { + return 0; + } + + return strcmp($userTag, $existingTag); + }); + + foreach ($newNamesCreate as $name) { $tag = new Tag(); $tag->setName(mb_substr($name, 0, 100)); $this->tagRepository->saveTag($tag); diff --git a/src/Invoice/DefaultInvoiceFormatter.php b/src/Invoice/DefaultInvoiceFormatter.php index 72c606e9..30666713 100644 --- a/src/Invoice/DefaultInvoiceFormatter.php +++ b/src/Invoice/DefaultInvoiceFormatter.php @@ -77,4 +77,9 @@ final class DefaultInvoiceFormatter implements InvoiceFormatter $this->locale = $locale; $this->formatter = null; } + + public function getFormattedAmount(float $amount): string + { + return $this->getFormatter()->amount($amount); + } } diff --git a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php index 3f48db34..66c6c80c 100644 --- a/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceItemDefaultHydrator.php @@ -34,7 +34,7 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator if ($item->isFixedRate()) { $appliedRate = $item->getFixedRate(); - $amount = $item->getAmount(); + $amount = $formatter->getFormattedAmount($item->getAmount()); } $activity = $item->getActivity(); @@ -83,12 +83,18 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator 'entry.date' => $formatter->getFormattedDateTime($begin), 'entry.week' => \intval($begin->format('W')), 'entry.weekyear' => $begin->format('o'), - 'entry.user_id' => $user->getId(), - 'entry.user_name' => $user->getUserIdentifier(), - 'entry.user_title' => $user->getTitle() ?? '', - 'entry.user_alias' => $user->getAlias() ?? '', ]; + if (null !== $user) { + $values = array_merge($values, [ + 'entry.user_id' => $user->getId(), + 'entry.user_name' => $user->getUserIdentifier(), + 'entry.user_title' => $user->getTitle() ?? '', + 'entry.user_alias' => $user->getAlias() ?? '', + 'entry.user_display' => $user->getDisplayName() ?? '', + ]); + } + if (null !== $activity) { $values = array_merge($values, [ 'entry.activity' => $activity->getName(), diff --git a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php index 7f3e5c28..d920638b 100644 --- a/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php @@ -35,7 +35,8 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator 'customer.name' => $customer->getName() ?? '', 'customer.contact' => $customer->getContact() ?? '', 'customer.company' => $customer->getCompany() ?? '', - 'customer.vat' => $customer->getVatId() ?? '', + 'customer.vat' => $customer->getVatId() ?? '', // deprecated since 2.0.15 + 'customer.vat_id' => $customer->getVatId() ?? '', 'customer.number' => $customer->getNumber() ?? '', 'customer.country' => $customer->getCountry(), 'customer.homepage' => $customer->getHomepage() ?? '', diff --git a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php index aa09f136..e8b56762 100644 --- a/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelDefaultHydrator.php @@ -22,9 +22,6 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator $subtotal = $model->getCalculator()->getSubtotal(); $formatter = $model->getFormatter(); - $begin = $model->getQuery()->getBegin(); - $end = $model->getQuery()->getEnd(); - $values = [ 'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()), 'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()), @@ -33,6 +30,7 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'invoice.language' => $model->getTemplate()->getLanguage(), // since 1.9 'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency), 'invoice.vat' => $model->getCalculator()->getVat(), + 'invoice.tax_hide' => $model->isHideZeroTax() && $tax === 0.00, 'invoice.tax' => $formatter->getFormattedMoney($tax, $currency), 'invoice.tax_nc' => $formatter->getFormattedMoney($tax, $currency, false), 'invoice.tax_plain' => $tax, @@ -65,25 +63,66 @@ final class InvoiceModelDefaultHydrator implements InvoiceModelHydrator 'query.end_month' => '', // since 1.9 'query.end_month_number' => '', // since 1.9 'query.end_year' => '', // since 1.9 + + // since 2.0.15 + 'user.see_others' => ($model->getQuery()?->getUser() === null), ]; - if ($begin !== null) { - $values = array_merge($values, [ - 'query.day' => $begin->format('d'), // @deprecated - but impossible to delete - 'query.month' => $formatter->getFormattedMonthName($begin), // @deprecated - but impossible to delete - 'query.month_number' => $begin->format('m'), // @deprecated - but impossible to delete - 'query.year' => $begin->format('Y'), // @deprecated - but impossible to delete - 'query.begin' => $formatter->getFormattedDateTime($begin), - 'query.begin_day' => $begin->format('d'), - 'query.begin_month' => $formatter->getFormattedMonthName($begin), - 'query.begin_month_number' => $begin->format('m'), - 'query.begin_year' => $begin->format('Y'), - 'query.end' => $formatter->getFormattedDateTime($end), // since 1.9 - 'query.end_day' => $end->format('d'), // since 1.9 - 'query.end_month' => $formatter->getFormattedMonthName($end), // since 1.9 - 'query.end_month_number' => $end->format('m'), // since 1.9 - 'query.end_year' => $end->format('Y'), // since 1.9 - ]); + $query = $model->getQuery(); + if ($query !== null) { + $begin = $query->getBegin(); + $end = $query->getEnd(); + + if ($begin !== null) { + $values = array_merge($values, [ + 'query.day' => $begin->format('d'), + // @deprecated - but impossible to delete + 'query.month' => $formatter->getFormattedMonthName($begin), + // @deprecated - but impossible to delete + 'query.month_number' => $begin->format('m'), + // @deprecated - but impossible to delete + 'query.year' => $begin->format('Y'), + // @deprecated - but impossible to delete + 'query.begin' => $formatter->getFormattedDateTime($begin), + 'query.begin_day' => $begin->format('d'), + 'query.begin_month' => $formatter->getFormattedMonthName($begin), + 'query.begin_month_number' => $begin->format('m'), + 'query.begin_year' => $begin->format('Y'), + 'query.end' => $formatter->getFormattedDateTime($end), + // since 1.9 + 'query.end_day' => $end->format('d'), + // since 1.9 + 'query.end_month' => $formatter->getFormattedMonthName($end), + // since 1.9 + 'query.end_month_number' => $end->format('m'), + // since 1.9 + 'query.end_year' => $end->format('Y'), + // since 1.9 + ]); + } + + // since 2.0.15 + $activity = $query->getActivity(); + if ($activity !== null) { + $prefix = 'query.activity.'; + + $values = array_merge($values, [ + $prefix . 'name' => $activity->getName() ?? '', + $prefix . 'comment' => $activity->getComment() ?? '', + ]); + } + + // since 2.0.15 + $project = $query->getProject(); + if ($project !== null) { + $prefix = 'query.project.'; + + $values = array_merge($values, [ + $prefix . 'name' => $project->getName() ?? '', + $prefix . 'comment' => $project->getComment() ?? '', + $prefix . 'order_number' => $project->getOrderNumber(), + ]); + } } $entries = $model->getEntries(); diff --git a/src/Invoice/Hydrator/InvoiceModelUserHydrator.php b/src/Invoice/Hydrator/InvoiceModelUserHydrator.php index 3df5702c..501c803b 100644 --- a/src/Invoice/Hydrator/InvoiceModelUserHydrator.php +++ b/src/Invoice/Hydrator/InvoiceModelUserHydrator.php @@ -9,7 +9,6 @@ namespace App\Invoice\Hydrator; -use App\Entity\UserPreference; use App\Invoice\InvoiceModel; use App\Invoice\InvoiceModelHydrator; @@ -28,9 +27,9 @@ final class InvoiceModelUserHydrator implements InvoiceModelHydrator 'user.email' => $user->getEmail(), 'user.title' => $user->getTitle() ?? '', 'user.alias' => $user->getAlias() ?? '', + 'user.display' => $user->getDisplayName() ?? '', ]; - /** @var UserPreference $metaField */ foreach ($user->getPreferences() as $metaField) { $values = array_merge($values, [ 'user.meta.' . $metaField->getName() => $metaField->getValue(), diff --git a/src/Invoice/InvoiceFormatter.php b/src/Invoice/InvoiceFormatter.php index 73df9958..6e06f8c8 100644 --- a/src/Invoice/InvoiceFormatter.php +++ b/src/Invoice/InvoiceFormatter.php @@ -24,6 +24,8 @@ interface InvoiceFormatter public function getFormattedTime(DateTime $date): string; + public function getFormattedAmount(float $amount): string; + public function getFormattedMoney(float $amount, ?string $currency, bool $withCurrency = true): string; public function getFormattedMonthName(DateTime $date): string; diff --git a/src/Invoice/ServiceInvoice.php b/src/Invoice/ServiceInvoice.php index ba113b70..b0f017a6 100644 --- a/src/Invoice/ServiceInvoice.php +++ b/src/Invoice/ServiceInvoice.php @@ -278,7 +278,7 @@ final class ServiceInvoice { $document = $this->getDocumentByName($model->getTemplate()->getRenderer()); if (null === $document) { - throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer()); + throw new \Exception('Please adjust your invoice template, the renderer is invalid: ' . $model->getTemplate()->getRenderer()); } foreach ($this->getRenderer() as $renderer) { @@ -426,7 +426,7 @@ final class ServiceInvoice $calculator = $this->getCalculatorByName($template->getCalculator()); if (null === $calculator) { - throw new \Exception('Please adjust your invoice template, the invoice calculator is invalid: ' . $template->getCalculator()); + throw new \Exception('Please adjust your invoice template, the sum calculator is invalid: ' . $template->getCalculator()); } $model->setCalculator($calculator); diff --git a/src/Repository/Query/TimesheetQuery.php b/src/Repository/Query/TimesheetQuery.php index bcd81358..ca9cae20 100644 --- a/src/Repository/Query/TimesheetQuery.php +++ b/src/Repository/Query/TimesheetQuery.php @@ -93,6 +93,14 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface return array_values($this->users); } + /** + * Check if there is one or more users in the query + */ + public function hasUsers(): bool + { + return $this->timesheetUser !== null || \count($this->users) > 0; + } + /** * Limit the data exclusively to the user. */ diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php index 39097724..5d42c5ac 100644 --- a/src/Repository/TagRepository.php +++ b/src/Repository/TagRepository.php @@ -66,28 +66,6 @@ class TagRepository extends EntityRepository return $this->findOneBy(['name' => $tagName]); } - /** - * Find ids of the given tagNames separated by comma - * @param string $tagNames - * @return array - */ - public function findIdsByTagNameList(string $tagNames): array - { - $qb = $this - ->createQueryBuilder('t') - ->select('t.id'); - $list = array_filter(array_unique(array_map('trim', explode(',', $tagNames)))); - $cnt = 0; - foreach ($list as $listElem) { - $qb - ->orWhere('t.name like :elem' . $cnt) - ->setParameter('elem' . $cnt, '%' . $listElem . '%'); - $cnt++; - } - - return array_column($qb->getQuery()->getScalarResult(), 'id'); - } - /** * Find all tag names in an alphabetical order * diff --git a/templates/embeds/budgets.html.twig b/templates/embeds/budgets.html.twig index ce67c22a..0d5f874e 100644 --- a/templates/embeds/budgets.html.twig +++ b/templates/embeds/budgets.html.twig @@ -129,7 +129,7 @@ {{ stats.internalRate|money(currency) }} {{ percentReached|number_format(2) }}% - {% if not entity.isMonthlyBudget() and timeBudgetVisible and stats.duration > 0 %} + {% if not entity.isMonthlyBudget() and timeBudgetVisible and stats.duration > 0 and stats.duration|chart_duration > 0.00 %} {% set realHourlyRate = stats.rateBillable / stats.duration|chart_duration %} {{ 'hourlyRate'|trans }} ({{ 'billable'|trans }} / {{ durationTrans|trans }}) diff --git a/templates/invoice/renderer/default.pdf.twig b/templates/invoice/renderer/default.pdf.twig index 3cfbe1b9..1cf0433e 100644 --- a/templates/invoice/renderer/default.pdf.twig +++ b/templates/invoice/renderer/default.pdf.twig @@ -1,13 +1,8 @@ -{%- set fallback = app.request is not null ? app.request.locale : 'en' -%} -{%- set language = model.template.language|default(fallback) -%} -{%- set isDecimal = true -%} -{%- set currency = model.currency -%} -{%- set project = model.query.project -%} - + - - {% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %} + + {% block title %}{{ invoice['invoice.number'] }}-{{ invoice['customer.company']|default(invoice['customer.name'])|u.snake }}{% endblock %} @@ -18,10 +13,10 @@
- {{ model.template.title }} + {{ invoice['template.title'] }} - {{ 'date'|trans }}: {{ model.invoiceDate|date_short }} + {{ 'date'|trans }}: {{ invoice['invoice.date'] }}
@@ -32,10 +27,10 @@ {{ 'contact'|trans }}: - {{ model.template.contact|nl2str(' – ') }} + {{ invoice['template.contact']|nl2str(' – ') }}
{{ 'invoice_bank_account'|trans }}: - {{ model.template.paymentDetails|nl2str(' – ') }} + {{ invoice['template.payment_details']|nl2str(' – ') }} {{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }} @@ -50,24 +45,24 @@ mpdf--> {{ 'invoice.to'|trans }}
- {{ model.customer.company|default(model.customer.name) }} + {{ invoice['customer.company']|default(invoice['customer.name']) }}
- {{ model.customer.address|nl2br }} - {% if model.customer.vatId is not empty %} + {{ invoice['customer.address']|nl2br }} + {% if invoice['customer.vat_id'] is not empty %}
- {{ 'vat_id'|trans }}: {{ model.customer.vatId }} + {{ 'vat_id'|trans }}: {{ invoice['customer.vat_id'] }} {% endif %} {{ 'invoice.from'|trans }}
- {{ model.template.company }} + {{ invoice['template.company'] }}
- {{ model.template.address|trim|nl2br }} - {% if model.template.vatId is not empty %} + {{ invoice['template.address']|trim|nl2br }} + {% if invoice['template.vat_id'] is not empty %}
{{ 'vat_id'|trans }}: - {{ model.template.vatId }} + {{ invoice['template.vat_id'] }} {% endif %} @@ -75,27 +70,27 @@ mpdf-->

{{ 'invoice.number'|trans }}: - {{ model.invoiceNumber }} + {{ invoice['invoice.number'] }}
{{ 'invoice.due_days'|trans }}: - {{ model.dueDate|date_short }} + {{ invoice['invoice.due_date'] }} - {% if model.customer.number is not empty %} + {% if invoice['customer.number'] is not empty %}
{{ 'number'|trans }}: - {{ model.customer.number }} + {{ invoice['customer.number'] }} {% endif %} - {% if project is not null and project.orderNumber is not empty %} + {% if invoice['query.project.order_number'] is defined and invoice['query.project.order_number'] is not empty %}
{{ 'orderNumber'|trans }}: - {{ project.orderNumber }} + {{ invoice['query.project.order_number'] }} {% endif %}

- {% if model.customer.invoiceText is not empty%} -

{{ model.customer.invoiceText|md2html }}

+ {% if invoice['customer.invoice_text'] is not empty%} +

{{ invoice['customer.invoice_text']|md2html }}

{% endif %} @@ -109,43 +104,39 @@ mpdf--> - {% for id, entry in model.calculator.entries %} + {% for invoiceLineItem in entries %} - {% set duration = entry.duration|duration(isDecimal) %} - {% if entry.fixedRate %} - {% set rate = entry.fixedRate %} - {% set duration = entry.amount|amount %} - {% else %} - {% set rate = entry.hourlyRate %} - {% endif %} - - + - - - + + + {% endfor %} - {% if model.calculator.tax > 0 or not model.isHideZeroTax() %} + {% if not invoice['invoice.tax_hide'] %} - + - + {% endif %} @@ -153,16 +144,16 @@ mpdf--> {{ 'invoice.total'|trans }}
{{ entry.begin|date_short }} - {% if entry.description is not empty %} - {{ entry.description|nl2br }} + {{ invoiceLineItem['entry.begin'] }} + {% if invoiceLineItem['entry.description'] is not empty %} + {{ invoiceLineItem['entry.description']|nl2br }} {% else %} - {% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }} + {% if invoiceLineItem['entry.activity'] is defined %} + {{ invoiceLineItem['entry.activity'] }} / + {% endif %} + {{ invoiceLineItem['entry.project'] }} {% endif %} {{ rate|money(currency) }}{{ duration }}{{ entry.rate|money(currency) }}{{ invoiceLineItem['entry.rate'] }}{{ invoiceLineItem['entry.amount'] }}{{ invoiceLineItem['entry.total'] }}
{{ 'invoice.subtotal'|trans }} {{ model.calculator.subtotal|money(currency) }}{{ invoice['invoice.subtotal'] }}
- {{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%) + {{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%) {{ model.calculator.tax|money(currency) }}{{ invoice['invoice.tax'] }}
- {{ model.calculator.total|money(currency) }} + {{ invoice['invoice.total'] }}
- {% if model.template.paymentTerms is not empty %} -
- {{ model.template.paymentTerms|md2html }} -
+ {% if invoice['template.payment_terms'] is not empty %} +
+ {{ invoice['template.payment_terms']|md2html }} +
{% endif %} diff --git a/templates/invoice/renderer/invoice.html.twig b/templates/invoice/renderer/invoice.html.twig index 21cc6c39..726f0195 100644 --- a/templates/invoice/renderer/invoice.html.twig +++ b/templates/invoice/renderer/invoice.html.twig @@ -1,166 +1,158 @@ -{% set fallback = app.request is not null ? app.request.locale : 'en' %} -{% set language = model.template.language|default(fallback) %} -{% set isDecimal = true %} -{% set currency = model.currency %} -{% set project = model.query.project %} - + - - - {% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %} - -
-
+
+
-
-
- +
+
+ +
-
-
-
- {{ 'invoice.to'|trans }} -
- {{ model.customer.company|default(model.customer.name) }}
- {{ model.customer.address|nl2br }} - {% if model.customer.vatId is not empty %} -
- {{ 'vat_id'|trans }}: {{ model.customer.vatId }} - {% endif %} - {% if model.customer.number is not empty %} -
- {{ 'number'|trans }}: {{ model.customer.number }} - {% endif %} - {% if project is not null and project.orderNumber is not empty %} -
- {{ 'orderNumber'|trans }}: {{ project.orderNumber }} - {% endif %} -
+
+
+ {{ 'invoice.to'|trans }} +
+ {{ invoice['customer.company']|default(invoice['customer.name']) }}
+ {{ invoice['customer.address']|nl2br }} + {% if invoice['customer.vat_id'] is not empty %} +
+ {{ 'vat_id'|trans }}: {{ invoice['customer.vat_id'] }} + {% endif %} + {% if invoice['customer.number'] is not empty %} +
+ {{ 'number'|trans }}: {{ invoice['customer.number'] }} + {% endif %} + {% if invoice['query.project.order_number'] is defined and invoice['query.project.order_number'] is not empty %} +
+ {{ 'orderNumber'|trans }}: {{ invoice['query.project.order_number'] }} + {% endif %} +
+
+
+
+ {{ 'invoice.from'|trans }} +
+ {{ invoice['template.company'] }}
+ {{ invoice['template.address']|trim|nl2br }} + {% if invoice['template.vat_id'] is not empty %} +
+ {{ 'vat_id'|trans }}: + {{ invoice['template.vat_id'] }} + {% endif %} +
+
-
-
- {{ 'invoice.from'|trans }} -
- {{ model.template.company }}
- {{ model.template.address|trim|nl2br }} - {% if model.template.vatId is not empty %} + +
+
+

+ {{ 'date'|trans }}: + {{ invoice['invoice.date'] }} +
- {{ 'vat_id'|trans }}: - {{ model.template.vatId }} - {% endif %} -

+ {{ 'invoice.number'|trans }}: + {{ invoice['invoice.number'] }} + +
+ {{ 'invoice.due_days'|trans }}: + {{ invoice['invoice.due_date'] }} +

+
+
-
-
-
-

- {{ 'invoice.number'|trans }}: - {{ model.invoiceNumber }} - -
- {{ 'invoice.due_days'|trans }}: - {{ model.dueDate|date_short }} -

-
-
-
- -
-
- - - - - - - - - - - - {% for entry in model.calculator.entries %} - {% set duration = entry.duration|duration(isDecimal) %} - {% if entry.fixedRate %} - {% set rate = entry.fixedRate %} - {% set duration = entry.amount|amount %} - {% else %} - {% set rate = entry.hourlyRate %} +
+
+
{{ 'date'|trans }}{{ 'description'|trans }}{{ 'unit_price'|trans }}{{ 'amount'|trans }}{{ 'total_rate'|trans }}
+ + + + + + + + + + + {% for invoiceLineItem in entries %} + + + + + + + + {% endfor %} + + + {% if not invoice['invoice.tax_hide'] %} + + + + + + + + {% endif %} - - - - - - - {% endfor %} - - - {% if model.calculator.tax > 0 or not model.isHideZeroTax() %} - - - - - - - + +
{{ 'date'|trans }}{{ 'description'|trans }}{{ 'unit_price'|trans }}{{ 'amount'|trans }}{{ 'total_rate'|trans }}
{{ invoiceLineItem['entry.begin'] }} + {% if invoiceLineItem['entry.description'] is not empty %} + {{ invoiceLineItem['entry.description']|nl2br }} + {% else %} + {% if invoiceLineItem['entry.activity'] is defined %} + {{ invoiceLineItem['entry.activity'] }} / + {% endif %} + {{ invoiceLineItem['entry.project'] }} + {% endif %} + {{ invoiceLineItem['entry.rate'] }}{{ invoiceLineItem['entry.amount'] }}{{ invoiceLineItem['entry.total'] }}
+ {{ 'invoice.subtotal'|trans }} + {{ invoice['invoice.subtotal'] }}
+ {{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%) + {{ invoice['invoice.tax'] }}
{{ entry.begin|date_short }} - {% if entry.description is not empty %} - {{ entry.description|nl2br }} - {% else %} - {% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }} - {% endif %} + + {{ 'invoice.total'|trans }} {{ rate|money(currency) }}{{ duration }}{{ entry.rate|money(currency) }}
- {{ 'invoice.subtotal'|trans }} + + {{ invoice['invoice.total'] }} {{ model.calculator.subtotal|money(currency) }}
- {{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%) - {{ model.calculator.tax|money(currency) }}
+
+
+ +
+
+ {% if invoice['template.payment_terms'] is not empty %} +
+ {{ invoice['template.payment_terms']|md2html }} +
{% endif %} - - - {{ 'invoice.total'|trans }} - - - {{ model.calculator.total|money(currency) }} - - - - +
-
-
-
- {% if model.template.paymentTerms is not empty %} -
- {{ model.template.paymentTerms|md2html }} -
- {% endif %} -
-
+
+

+ {{ 'address'|trans }}: {{ invoice['template.company'] }} – {{ invoice['template.address']|nl2str(' – ') }} +
+ {{ 'invoice_bank_account'|trans }}: {{ invoice['template.payment_details']|nl2str(' – ') }} +
+ {{ 'contact'|trans }}: {{ invoice['template.contact']|nl2str(' – ') }} +

+
-
-

- {{ 'address'|trans }}: {{ model.template.company }} – {{ model.template.address|nl2str(' – ') }} -
- {{ 'invoice_bank_account'|trans }}: {{ model.template.paymentDetails|nl2str(' – ') }} -
- {{ 'contact'|trans }}: {{ model.template.contact|nl2str(' – ') }} -

-
- -
-
+
+
diff --git a/templates/invoice/renderer/service-date.pdf.twig b/templates/invoice/renderer/service-date.pdf.twig index de14028f..b247b2ab 100644 --- a/templates/invoice/renderer/service-date.pdf.twig +++ b/templates/invoice/renderer/service-date.pdf.twig @@ -1,13 +1,8 @@ -{%- set fallback = app.request is not null ? app.request.locale : 'en' -%} -{%- set language = model.template.language|default(fallback) -%} -{%- set isDecimal = true -%} -{%- set currency = model.currency -%} -{%- set project = model.query.project -%} - + - - {% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %} + + {% block title %}{{ invoice['invoice.number'] }}-{{ invoice['customer.company']|default(invoice['customer.name'])|u.snake }}{% endblock %} @@ -18,9 +13,9 @@
- {{ model.template.company }} – {{ model.template.address|nl2str(' – ') }} - {% if model.template.vatId is not empty %} - – {{ 'vat_id'|trans }}: {{ model.template.vatId }} + {{ invoice['template.company'] }} – {{ invoice['template.address']|nl2str(' – ') }} + {% if invoice['template.vat_id'] is not empty %} + – {{ 'vat_id'|trans }}: {{ invoice['template.vat_id'] }} {% endif %} @@ -36,18 +31,18 @@ {{ 'address'|trans }}
- {{ model.template.company }}
- {{ model.template.address|nl2br }} + {{ invoice['template.company'] }}
+ {{ invoice['template.address']|nl2br }}
{{ 'invoice_bank_account'|trans }}
- {{ model.template.paymentDetails|nl2br }} + {{ invoice['template.payment_details']|nl2br }}
{{ 'contact'|trans }}
- {{ model.template.contact|nl2br }} + {{ invoice['template.contact']|nl2br }}
@@ -57,8 +52,8 @@ mpdf-->
- {{ model.customer.company|default(model.customer.name) }}
- {{ model.customer.address|nl2br }} + {{ invoice['customer.company']|default(invoice['customer.name']) }}
+ {{ invoice['customer.address']|nl2br }}
{% set classLeft = 'text-left' %} @@ -66,36 +61,36 @@ mpdf--> - + - + - + - + - {% if model.customer.number is not empty %} + {% if invoice['customer.number'] is not empty %} - + {% endif %} - {% if project is not null and project.orderNumber is not empty %} + {% if invoice['query.project.order_number'] is defined and invoice['query.project.order_number'] is not empty %} - + {% endif %} - {% if model.customer.vatId is not empty %} + {% if invoice['customer.vat_id'] is not empty %} - + {% endif %}
{{ 'date'|trans }}{{ model.invoiceDate|date_short }}{{ invoice['invoice.date'] }}
{{ 'invoice.service_date'|trans }}{{ model.query.end|month_name }} {{ model.query.end|date('Y') }}{{ invoice['query.end_month'] }} {{ invoice['query.end_year'] }}
{{ 'invoice.number'|trans }}{{ model.invoiceNumber }}{{ invoice['invoice.number'] }}
{{ 'invoice.due_days'|trans }}{{ model.dueDate|date_short }}{{ invoice['invoice.due_date'] }}
{{ 'number'|trans }}{{ model.customer.number }}{{ invoice['customer.number'] }}
{{ 'orderNumber'|trans }}{{ project.orderNumber }}{{ invoice['query.project.order_number'] }}
{{ 'vat_id'|trans }}{{ model.customer.vatId }}{{ invoice['customer.vat_id'] }}
@@ -103,10 +98,10 @@ mpdf-->
-

{{ model.template.title }}

+

{{ invoice['template.title'] }}

- {% if model.customer.invoiceText is not empty%} -

{{ model.customer.invoiceText|md2html }}

+ {% if invoice['customer.invoice_text'] is not empty%} +

{{ invoice['customer.invoice_text']|md2html }}

{% endif %} @@ -119,43 +114,38 @@ mpdf--> - {% for entry in model.calculator.entries %} + {% for invoiceLineItem in entries %} - {% set duration = entry.duration|duration(isDecimal) %} - {% if entry.fixedRate is not null %} - {% set rate = entry.fixedRate %} - {% set duration = entry.amount|amount %} - {% else %} - {% set rate = entry.hourlyRate %} - {% endif %} - - - - + + + {% endfor %} - {% if model.calculator.tax > 0 or not model.isHideZeroTax() %} + {% if not invoice['invoice.tax_hide'] %} - + - + {% endif %} @@ -163,16 +153,16 @@ mpdf--> {{ 'invoice.total'|trans }}
- {% if entry.description is not empty %} - {{ entry.description|nl2br }} + {% if invoiceLineItem['entry.description'] is not empty %} + {{ invoiceLineItem['entry.description']|nl2br }} {% else %} - {% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }} + {% if invoiceLineItem['entry.activity'] is defined %} + {{ invoiceLineItem['entry.activity'] }} / + {% endif %} + {{ invoiceLineItem['entry.project'] }} {% endif %} {{ rate|money(currency) }}{{ duration }}{{ entry.rate|money(currency) }}{{ invoiceLineItem['entry.rate'] }}{{ invoiceLineItem['entry.amount'] }}{{ invoiceLineItem['entry.total'] }}
{{ 'invoice.subtotal'|trans }} {{ model.calculator.subtotal|money(currency) }}{{ invoice['invoice.subtotal'] }}
- {{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%) + {{ 'invoice.tax'|trans }} ({{ invoice['invoice.vat'] }}%) {{ model.calculator.tax|money(currency) }}{{ invoice['invoice.tax'] }}
- {{ model.calculator.total|money(currency) }} + {{ invoice['invoice.total'] }}
- {% if model.template.paymentTerms is not empty %} -

- {{ model.template.paymentTerms|md2html }} -

+ {% if invoice['template.payment_terms'] is not empty %} +
+ {{ invoice['template.payment_terms']|md2html }} +
{% endif %} diff --git a/templates/invoice/renderer/timesheet.html.twig b/templates/invoice/renderer/timesheet.html.twig index 66d0d716..398a6870 100644 --- a/templates/invoice/renderer/timesheet.html.twig +++ b/templates/invoice/renderer/timesheet.html.twig @@ -1,88 +1,91 @@ -{% set fallback = app.request is not null ? app.request.locale : 'en' %} -{% set language = model.template.language|default(fallback) %} -{% set isDecimal = true %} -{% set project = model.query.project %} -{% set activity = model.query.activity %} - + - {{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }} -
+
+
- {% if project is not null %} + {% if invoice['query.project.name'] is defined %} {% endif %} - {% if activity is not null %} + {% if invoice['query.activity.name'] is defined %} {% endif %}
{{ 'invoice.from'|trans }} - {% if model.query.user is not empty %} - {{ model.query.user.displayName }} + {% if not invoice['user.see_others'] and invoice['user.display'] is defined %} + {{ invoice['user.display'] }} {% else %} - {{ model.template.company }} + {{ invoice['template.company'] }} {% endif %}
{{ 'date'|trans }} - {% if model.query.begin|date('m') != model.query.end|date('m') or model.query.begin|date('Y') != model.query.end|date('Y') %} - {{ model.query.begin|date_short }} - {{ model.query.end|date_short }} + {% if invoice['query.begin_month_number'] != invoice['query.end_month_number'] or invoice['query.begin_year'] != invoice['query.end_year'] %} + {{ invoice['query.begin'] }} - {{ invoice['query.end'] }} {% else %} - {{ model.query.end|month_name }} {{ model.query.end|date('Y') }} + {{ invoice['query.end_month'] }} {{ invoice['query.end_year'] }} {% endif %}
{{ 'customer'|trans }} - {% if model.customer.number is not empty %}[{{ model.customer.number }}]{% endif %} - {{ model.customer.name }}{% if model.customer.contact is not empty %} / {{ model.customer.contact }}{% endif %} + {% if invoice['customer.number'] is not empty %} + [{{ invoice['customer.number'] }}] + {% endif %} + {{ invoice['customer.name'] }} + {% if invoice['customer.contact'] is not empty %} + / {{ invoice['customer.contact'] }} + {% endif %}
{{ 'project'|trans }} - {{ project.name }} - {% if project.orderNumber is not empty %} - ({{ 'orderNumber'|trans }}: {{ project.orderNumber }}) + {{ invoice['query.project.name'] }} + {% if invoice['query.project.order_number'] is not empty %} + ({{ 'orderNumber'|trans }}: {{ invoice['query.project.order_number'] }}) {% endif %}
{{ 'activity'|trans }} - {{ activity.name }} + {{ invoice['query.activity.name'] }}
+
- {% if model.query.user is empty %} + {% if invoice['user.see_others'] %} {% endif %} @@ -90,44 +93,48 @@ - {% for entry in model.calculator.entries %} + {% for timesheet in entries %} - - {% if model.query.user is empty %} - + + {% if invoice['user.see_others'] %} + {% endif %} - + {% endfor %} - {% if model.query.user is empty %} + {% if invoice['user.see_others'] %} {% endif %} - +
{{ 'date'|trans }}{{ 'user'|trans }}{{ 'activity'|trans }}
{{ entry.begin|date_short }}{{ entry.user.displayName }}{{ timesheet['entry.begin'] }}{{ timesheet['entry.user_display'] }} - {% if entry.description is not empty %} - {{ entry.description|nl2br }} + {% if timesheet['entry.description'] is not empty %} + {{ timesheet['entry.description']|nl2br }} {% else %} - {% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }} + {% if timesheet['entry.activity'] is defined %} + {{ timesheet['entry.activity'] }} / + {% endif %} + {{ timesheet['entry.project'] }} {% endif %} {{ entry.duration|duration(isDecimal) }}{{ timesheet['entry.amount'] }}
{{ 'invoice.total_working_time'|trans }}{{ model.calculator.timeWorked|duration(isDecimal) }}{{ invoice['invoice.duration_decimal'] }}
+
- {% if model.template.paymentTerms is not empty %} + {% if invoice['template.payment_terms'] is not empty %}

{{ 'payment_terms'|trans }}

-

- {{ model.template.paymentTerms|trim|nl2br }} -

+
+ {{ invoice['template.payment_terms']|md2html }} +
{% endif %}
@@ -136,6 +143,7 @@
+
diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php index ca5befd1..31c4caaa 100644 --- a/tests/API/TimesheetControllerTest.php +++ b/tests/API/TimesheetControllerTest.php @@ -49,38 +49,44 @@ class TimesheetControllerTest extends APIControllerBaseTest return $this->importFixture($fixture); } - public function testIsSecure() + public function testIsSecure(): void { $this->assertUrlIsSecured('/api/timesheets'); } - public function testGetCollection() + public function testGetCollection(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/timesheets'); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(10, \count($result)); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(10, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); } - public function testGetCollectionFull() + public function testGetCollectionFull(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', ['full' => 'true']); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(10, \count($result)); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(10, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]); } - public function testGetCollectionForOtherUser() + public function testGetCollectionForOtherUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $this->importFixtureForUser(User::ROLE_USER); @@ -97,15 +103,30 @@ class TimesheetControllerTest extends APIControllerBaseTest $query = ['user' => 2]; $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(10, \count($result)); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(10, \count($result)); + self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); + + $query = ['users' => [2]]; + $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); + + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(10, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); } - public function testGetCollectionForAllUser() + public function testGetCollectionForAllUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $this->importFixtureForUser(User::ROLE_USER); @@ -122,26 +143,32 @@ class TimesheetControllerTest extends APIControllerBaseTest $query = ['user' => 'all']; $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(17, \count($result)); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(17, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); } - public function testGetCollectionForEmptyResult() + public function testGetCollectionForEmptyResult(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); $this->assertAccessIsGranted($client, '/api/timesheets'); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); $this->assertEmpty($result); } - public function testGetCollectionWithQuery() + public function testGetCollectionWithQuery(): void { $modifiedAfter = new \DateTime('-1 hour'); $begin = new \DateTime('first day of this month'); @@ -167,16 +194,18 @@ class TimesheetControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER, 22); $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); $this->assertPagination($client->getResponse(), 2, 4, 6, 22); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(4, \count($result)); + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(4, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); } - public function testGetCollectionWithQueryFailsWith404OnOutOfRangedPage() + public function testGetCollectionWithQueryFailsWith404OnOutOfRangedPage(): void { $begin = new \DateTime('first day of this month'); $begin->setTime(0, 0, 0); @@ -194,7 +223,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiException($client->getResponse(), ['code' => 404, 'message' => 'Not Found']); } - public function testGetCollectionWithSingleParamsQuery() + public function testGetCollectionWithSingleParamsQuery(): void { $begin = new \DateTime('first day of this month'); $begin->setTime(0, 0, 0); @@ -218,15 +247,17 @@ class TimesheetControllerTest extends APIControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(5, \count($result)); + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(5, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); } - public function testExportedFilter() + public function testExportedFilter(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); @@ -255,11 +286,13 @@ class TimesheetControllerTest extends APIControllerBaseTest ]; $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(7, \count($result)); + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(7, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); $query = [ @@ -271,11 +304,13 @@ class TimesheetControllerTest extends APIControllerBaseTest ]; $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(10, \count($result)); + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(10, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); $query = [ @@ -285,15 +320,17 @@ class TimesheetControllerTest extends APIControllerBaseTest 'end' => $end->format(self::DATE_FORMAT_HTML5), ]; $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(17, \count($result)); + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(17, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); } - public function testGetEntity() + public function testGetEntity(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $em = $this->getEntityManager(); @@ -323,9 +360,11 @@ class TimesheetControllerTest extends APIControllerBaseTest $em->flush(); $this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheet->getId()); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); - $this->assertIsArray($result); + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); $expected = [ @@ -353,7 +392,7 @@ class TimesheetControllerTest extends APIControllerBaseTest } } - public function testGetEntityAccessDenied() + public function testGetEntityAccessDenied(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); @@ -363,23 +402,25 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiAccessDenied($client, '/api/timesheets/' . $timesheets[0]->getId(), 'Access denied.'); } - public function testGetEntityAccessAllowedForAdmin() + public function testGetEntityAccessAllowedForAdmin(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $timesheets = $this->importFixtureForUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheets[0]->getId()); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); - $this->assertIsArray($result); + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); } - public function testGetEntityNotFound() + public function testGetEntityNotFound(): void { $this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/' . PHP_INT_MAX, 'GET', 'App\\Entity\\Timesheet object not found by the @ParamConverter annotation.'); } - public function testPostAction() + public function testPostAction(): void { $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE)); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); @@ -396,16 +437,19 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); - $this->assertNotEmpty($result['id']); + self::assertNotEmpty($result['id']); $this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied - $this->assertEquals(2016, $result['rate']); + self::assertEquals(2016, $result['rate']); $this->assertFalse($result['billable']); } - public function testPostActionWithFullExpandedResponse() + public function testPostActionWithFullExpandedResponse(): void { $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE)); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); @@ -422,16 +466,19 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets?full=true', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetExpanded', $result); - $this->assertNotEmpty($result['id']); + self::assertNotEmpty($result['id']); $this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied - $this->assertEquals(2016, $result['rate']); + self::assertEquals(2016, $result['rate']); $this->assertTrue($result['billable']); } - public function testPostActionForDifferentUser() + public function testPostActionForDifferentUser(): void { $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE)); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); @@ -451,18 +498,21 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); - $this->assertNotEmpty($result['id']); - $this->assertEquals($user->getId(), $result['user']); + self::assertNotEmpty($result['id']); + self::assertEquals($user->getId(), $result['user']); $this->assertNotEquals($admin->getId(), $result['user']); $this->assertTrue($result['billable']); } // check for project, as this is a required field. It will not be included in the select, as it is // already filtered within the repository due to the hidden customer - public function testPostActionWithInvisibleProject() + public function testPostActionWithInvisibleProject(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); @@ -496,7 +546,7 @@ class TimesheetControllerTest extends APIControllerBaseTest // check for activity, as this is a required field. It will not be included in the select, as it is // already filtered within the repository due to the hidden flag - public function testPostActionWithInvisibleActivity() + public function testPostActionWithInvisibleActivity(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); @@ -528,7 +578,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiCallValidationError($client->getResponse(), ['activity']); } - public function testPostActionWithNonBillableCustomer() + public function testPostActionWithNonBillableCustomer(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); @@ -557,13 +607,16 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); $this->assertFalse($result['billable']); } - public function testPostActionWithNonBillableCustomerExplicit() + public function testPostActionWithNonBillableCustomerExplicit(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); @@ -593,14 +646,17 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); // explicit overwritten values win! $this->assertTrue($result['billable']); } - public function testPatchAction() + public function testPatchAction(): void { $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE)); $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); @@ -616,16 +672,19 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); - $this->assertNotEmpty($result['id']); - $this->assertEquals(25200, $result['duration']); - $this->assertEquals('foo', $result['description']); + self::assertNotEmpty($result['id']); + self::assertEquals(25200, $result['duration']); + self::assertEquals('foo', $result['description']); $this->assertFalse($result['billable']); } - public function testPatchActionWithInvalidUser() + public function testPatchActionWithInvalidUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); @@ -654,12 +713,12 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiResponseAccessDenied($response); } - public function testPatchActionWithUnknownTimesheet() + public function testPatchActionWithUnknownTimesheet(): void { $this->assertEntityNotFoundForPatch(User::ROLE_USER, '/api/timesheets/255', []); } - public function testInvalidPatchAction() + public function testInvalidPatchAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -674,48 +733,51 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'PATCH', [], json_encode($data)); $response = $client->getResponse(); - $this->assertEquals(400, $response->getStatusCode()); + self::assertEquals(400, $response->getStatusCode()); $this->assertApiCallValidationError($response, ['activity'], false, ['End date must not be earlier then start date.']); } // TODO: TEST PATCH FOR EXPORTED TIMESHEET FOR USER WITHOUT PERMISSION IS REJECTED - public function testDeleteAction() + public function testDeleteAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); $this->assertAccessIsGranted($client, '/api/timesheets/' . $timesheets[0]->getId()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); - $this->assertNotEmpty($result['id']); + self::assertNotEmpty($result['id']); $id = $result['id']; $this->request($client, '/api/timesheets/' . $id, 'DELETE'); $this->assertTrue($client->getResponse()->isSuccessful()); - $this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); + self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); $this->assertEmpty($client->getResponse()->getContent()); } - public function testDeleteActionWithUnknownTimesheet() + public function testDeleteActionWithUnknownTimesheet(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $this->assertNotFoundForDelete($client, '/api/timesheets/255'); } - public function testDeleteActionForDifferentUser() + public function testDeleteActionForDifferentUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $timesheets = $this->importFixtureForUser(User::ROLE_USER); $this->request($client, '/api/timesheets/' . $timesheets[0]->getId(), 'DELETE'); $this->assertTrue($client->getResponse()->isSuccessful()); - $this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); + self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); $this->assertEmpty($client->getResponse()->getContent()); } - public function testDeleteActionWithoutAuthorization() + public function testDeleteActionWithoutAuthorization(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); @@ -727,7 +789,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiResponseAccessDenied($response); } - public function testDeleteActionForExportedRecordIsNotAllowed() + public function testDeleteActionForExportedRecordIsNotAllowed(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); @@ -746,7 +808,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiResponseAccessDenied($response); } - public function testDeleteActionForExportedRecordIsAllowedForAdmin() + public function testDeleteActionForExportedRecordIsAllowedForAdmin(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $this->importFixtureForUser(User::ROLE_USER); @@ -763,7 +825,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertTrue($client->getResponse()->isSuccessful()); } - public function testGetRecentAction() + public function testGetRecentAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); @@ -785,15 +847,18 @@ class TimesheetControllerTest extends APIControllerBaseTest ]; $this->assertAccessIsGranted($client, '/api/timesheets/recent', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(1, \count($result)); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(1, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]); } - public function testActiveAction() + public function testActiveAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); @@ -813,14 +878,17 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets/active'); $this->assertTrue($client->getResponse()->isSuccessful()); - $results = json_decode($client->getResponse()->getContent(), true); - $this->assertEquals(3, \count($results)); - foreach ($results as $timesheet) { + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertEquals(3, \count($result)); + foreach ($result as $timesheet) { self::assertApiResponseTypeStructure('TimesheetCollectionFull', $timesheet); } } - public function testStopAction() + public function testStopAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); @@ -842,9 +910,12 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets/' . $id . '/stop', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); $em = $this->getEntityManager(); @@ -853,7 +924,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertInstanceOf(\DateTime::class, $timesheet->getEnd()); } - public function testStopActionTriggersValidationOnLongRunning() + public function testStopActionTriggersValidationOnLongRunning(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->setSystemConfiguration('timesheet.rules.long_running_duration', 750); @@ -877,12 +948,12 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiCallValidationError($client->getResponse(), ['duration' => 'Maximum 12:30 hours allowed.']); } - public function testStopThrowsNotFound() + public function testStopThrowsNotFound(): void { $this->assertEntityNotFoundForPatch(User::ROLE_USER, '/api/timesheets/11/stop', []); } - public function testStopNotAllowedForUser() + public function testStopNotAllowedForUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); @@ -905,7 +976,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiResponseAccessDenied($client->getResponse(), 'Access denied.'); } - public function testGetCollectionWithTags() + public function testGetCollectionWithTags(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $this->importFixtureForUser(User::ROLE_USER); @@ -924,33 +995,42 @@ class TimesheetControllerTest extends APIControllerBaseTest $query = ['tags' => ['Test']]; $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(10, \count($result)); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(10, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); $query = ['tags' => ['Test', 'Admin']]; $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(10, \count($result)); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(10, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); $query = ['tags' => ['Nothing-2-see', 'here']]; $this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(20, \count($result)); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertNotEmpty($result); + self::assertEquals(20, \count($result)); self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]); } - public function testRestartAction() + public function testRestartAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -965,7 +1045,10 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + self::assertApiResponseTypeStructure('TimesheetEntity', $result); $this->assertEmpty($result['description']); $this->assertEmpty($result['tags']); @@ -975,13 +1058,13 @@ class TimesheetControllerTest extends APIControllerBaseTest $timesheet = $em->getRepository(Timesheet::class)->find($result['id']); $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); $this->assertNull($timesheet->getEnd()); - $this->assertEquals(1, $timesheet->getActivity()->getId()); - $this->assertEquals(1, $timesheet->getProject()->getId()); + self::assertEquals(1, $timesheet->getActivity()->getId()); + self::assertEquals(1, $timesheet->getProject()->getId()); $this->assertEmpty($timesheet->getDescription()); $this->assertEmpty($timesheet->getTags()); } - public function testRestartActionWithBegin() + public function testRestartActionWithBegin(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -997,7 +1080,10 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['begin' => $begin->format(BaseApiController::DATE_FORMAT_PHP)]); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + self::assertApiResponseTypeStructure('TimesheetEntity', $result); $this->assertEmpty($result['description']); $this->assertEmpty($result['tags']); @@ -1006,15 +1092,15 @@ class TimesheetControllerTest extends APIControllerBaseTest /** @var Timesheet $timesheet */ $timesheet = $em->getRepository(Timesheet::class)->find($result['id']); $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); - $this->assertEquals($begin->format(BaseApiController::DATE_FORMAT_PHP), $timesheet->getBegin()->format(BaseApiController::DATE_FORMAT_PHP)); + self::assertEquals($begin->format(BaseApiController::DATE_FORMAT_PHP), $timesheet->getBegin()->format(BaseApiController::DATE_FORMAT_PHP)); $this->assertNull($timesheet->getEnd()); - $this->assertEquals(1, $timesheet->getActivity()->getId()); - $this->assertEquals(1, $timesheet->getProject()->getId()); + self::assertEquals(1, $timesheet->getActivity()->getId()); + self::assertEquals(1, $timesheet->getProject()->getId()); $this->assertEmpty($timesheet->getDescription()); $this->assertEmpty($timesheet->getTags()); } - public function testRestartActionWithCopyData() + public function testRestartActionWithCopyData(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -1034,29 +1120,32 @@ class TimesheetControllerTest extends APIControllerBaseTest $em->flush(); $timesheet = $em->getRepository(Timesheet::class)->find($id); - $this->assertEquals('foo', $timesheet->getDescription()); + self::assertEquals('foo', $timesheet->getDescription()); $this->request($client, '/api/timesheets/' . $id . '/restart', 'PATCH', ['copy' => 'all']); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + self::assertApiResponseTypeStructure('TimesheetEntity', $result); - $this->assertEquals('foo', $result['description']); - $this->assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']); - $this->assertEquals(['another', 'testing', 'bar'], $result['tags']); + self::assertEquals('foo', $result['description']); + self::assertEquals([['name' => 'sdfsdf', 'value' => 'nnnnn'], ['name' => '1234567890', 'value' => '1234567890']], $result['metaFields']); + self::assertEquals(['another', 'testing', 'bar'], $result['tags']); $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ $timesheet = $em->getRepository(Timesheet::class)->find($result['id']); $this->assertInstanceOf(\DateTime::class, $timesheet->getBegin()); $this->assertNull($timesheet->getEnd()); - $this->assertEquals(1, $timesheet->getActivity()->getId()); - $this->assertEquals(1, $timesheet->getProject()->getId()); - $this->assertEquals('foo', $timesheet->getDescription()); - $this->assertEquals(['another', 'testing', 'bar'], $timesheet->getTagsAsArray()); + self::assertEquals(1, $timesheet->getActivity()->getId()); + self::assertEquals(1, $timesheet->getProject()->getId()); + self::assertEquals('foo', $timesheet->getDescription()); + self::assertEquals(['another', 'testing', 'bar'], $timesheet->getTagsAsArray()); } - public function testRestartNotAllowedForUser() + public function testRestartNotAllowedForUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); @@ -1078,12 +1167,12 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiResponseAccessDenied($client->getResponse(), 'Access denied.'); } - public function testRestartThrowsNotFound() + public function testRestartThrowsNotFound(): void { $this->assertEntityNotFoundForPatch(User::ROLE_USER, '/api/timesheets/42/restart', []); } - public function testDuplicateAction() + public function testDuplicateAction(): void { $dateTime = new DateTimeFactory(new \DateTimeZone(self::TEST_TIMEZONE)); $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); @@ -1099,30 +1188,36 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); - $this->assertNotEmpty($result['id']); + self::assertNotEmpty($result['id']); $this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied - $this->assertEquals(2016, $result['rate']); + self::assertEquals(2016, $result['rate']); $this->request($client, '/api/timesheets/' . $result['id'] . '/duplicate', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); - $this->assertIsArray($result); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); self::assertApiResponseTypeStructure('TimesheetEntity', $result); - $this->assertNotEmpty($result['id']); + self::assertNotEmpty($result['id']); $this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied - $this->assertEquals(2016, $result['rate']); + self::assertEquals(2016, $result['rate']); } - public function testDuplicateThrowsNotFound() + public function testDuplicateThrowsNotFound(): void { $this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/11/duplicate', []); } - public function testExportAction() + public function testExportAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -1135,7 +1230,11 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->request($client, '/api/timesheets/' . $id . '/export', 'PATCH'); $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); + + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + self::assertApiResponseTypeStructure('TimesheetEntity', $result); $em->clear(); @@ -1151,7 +1250,7 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertFalse($timesheet->isExported()); } - public function testExportNotAllowedForUser() + public function testExportNotAllowedForUser(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -1161,17 +1260,17 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertApiResponseAccessDenied($client->getResponse(), 'Access denied.'); } - public function testExportThrowsNotFound() + public function testExportThrowsNotFound(): void { $this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/' . PHP_INT_MAX . '/export', []); } - public function testMetaActionThrowsNotFound() + public function testMetaActionThrowsNotFound(): void { $this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/' . PHP_INT_MAX . '/meta', []); } - public function testMetaActionThrowsExceptionOnMissingName() + public function testMetaActionThrowsExceptionOnMissingName(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -1183,7 +1282,7 @@ class TimesheetControllerTest extends APIControllerBaseTest ]); } - public function testMetaActionThrowsExceptionOnMissingValue() + public function testMetaActionThrowsExceptionOnMissingValue(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -1195,7 +1294,7 @@ class TimesheetControllerTest extends APIControllerBaseTest ]); } - public function testMetaActionThrowsExceptionOnMissingMetafield() + public function testMetaActionThrowsExceptionOnMissingMetafield(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -1207,7 +1306,7 @@ class TimesheetControllerTest extends APIControllerBaseTest ]); } - public function testMetaAction() + public function testMetaAction(): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $timesheets = $this->importFixtureForUser(User::ROLE_USER); @@ -1222,13 +1321,16 @@ class TimesheetControllerTest extends APIControllerBaseTest $this->assertTrue($client->getResponse()->isSuccessful()); - $result = json_decode($client->getResponse()->getContent(), true); + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + self::assertApiResponseTypeStructure('TimesheetEntity', $result); - $this->assertEquals(['name' => 'metatestmock', 'value' => 'another,testing,bar'], $result['metaFields'][0]); + self::assertEquals(['name' => 'metatestmock', 'value' => 'another,testing,bar'], $result['metaFields'][0]); $em = $this->getEntityManager(); /** @var Timesheet $timesheet */ $timesheet = $em->getRepository(Timesheet::class)->find($id); - $this->assertEquals('another,testing,bar', $timesheet->getMetaField('metatestmock')->getValue()); + self::assertEquals('another,testing,bar', $timesheet->getMetaField('metatestmock')->getValue()); } } diff --git a/tests/Entity/TimesheetTest.php b/tests/Entity/TimesheetTest.php index ba866309..918173e6 100644 --- a/tests/Entity/TimesheetTest.php +++ b/tests/Entity/TimesheetTest.php @@ -179,9 +179,20 @@ class TimesheetTest extends TestCase public function testClone(): void { $sut = new Timesheet(); + self::assertNull($sut->getModifiedAt()); $sut->setExported(true); $sut->setDescription('Invalid timesheet category "foo" given, expected one of: work, holiday, sickness, parental, overtime'); + $modifiedDate = new \DateTime(); + + $reflection = new \ReflectionClass($sut); + $property = $reflection->getProperty('modifiedAt'); + $property->setAccessible(true); + $property->setValue($sut, $modifiedDate); + $property->setAccessible(false); + + self::assertEquals($modifiedDate, $sut->getModifiedAt()); + $meta = new TimesheetMeta(); $meta->setName('blabla'); $meta->setValue('1234567890'); @@ -198,6 +209,9 @@ class TimesheetTest extends TestCase $clone = clone $sut; + self::assertNotNull($sut->getModifiedAt()); + self::assertNull($clone->getModifiedAt()); + foreach ($sut->getMetaFields() as $metaField) { $cloneMeta = $clone->getMetaField($metaField->getName()); self::assertEquals($cloneMeta->getValue(), $metaField->getValue()); diff --git a/tests/Invoice/DebugFormatter.php b/tests/Invoice/DebugFormatter.php index 3fd58623..df7c8fed 100644 --- a/tests/Invoice/DebugFormatter.php +++ b/tests/Invoice/DebugFormatter.php @@ -65,4 +65,9 @@ class DebugFormatter implements InvoiceFormatter { // does nothing } + + public function getFormattedAmount(float $amount): string + { + return (string) $amount; + } } diff --git a/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php b/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php index e3d8bcb6..eb8d79ca 100644 --- a/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceItemDefaultHydratorTest.php @@ -67,6 +67,7 @@ class InvoiceItemDefaultHydratorTest extends TestCase 'entry.user_id', 'entry.user_name', 'entry.user_alias', + 'entry.user_display', 'entry.user_title', 'entry.activity', 'entry.activity_id', diff --git a/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php index 4845c92b..cd2100ac 100644 --- a/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceModelCustomerHydratorTest.php @@ -44,6 +44,7 @@ class InvoiceModelCustomerHydratorTest extends TestCase 'customer.contact', 'customer.company', 'customer.vat', + 'customer.vat_id', 'customer.country', 'customer.number', 'customer.homepage', diff --git a/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php index f124c3a0..8950018a 100644 --- a/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceModelDefaultHydratorTest.php @@ -41,6 +41,7 @@ class InvoiceModelDefaultHydratorTest extends TestCase 'invoice.vat', 'invoice.language', 'invoice.tax', + 'invoice.tax_hide', 'invoice.tax_nc', 'invoice.tax_plain', 'invoice.total_time', @@ -76,6 +77,7 @@ class InvoiceModelDefaultHydratorTest extends TestCase 'query.end_month', 'query.end_month_number', 'query.end_year', + 'user.see_others', ]; $givenKeys = array_keys($model); diff --git a/tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php b/tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php index c6386f19..b01d7ea1 100644 --- a/tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php +++ b/tests/Invoice/Hydrator/InvoiceModelUserHydratorTest.php @@ -33,6 +33,7 @@ class InvoiceModelUserHydratorTest extends TestCase protected function assertModelStructure(array $model) { $keys = [ + 'user.display', 'user.alias', 'user.email', 'user.name', diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php index 72322d0c..26e4981d 100644 --- a/tests/Invoice/Renderer/DebugRendererTest.php +++ b/tests/Invoice/Renderer/DebugRendererTest.php @@ -84,6 +84,7 @@ class DebugRendererTest extends TestCase 'invoice.currency', 'invoice.currency_symbol', 'invoice.vat', + 'invoice.tax_hide', 'invoice.tax', 'invoice.language', 'invoice.tax_nc', @@ -127,6 +128,7 @@ class DebugRendererTest extends TestCase 'customer.contact', 'customer.company', 'customer.vat', + 'customer.vat_id', 'customer.country', 'customer.number', 'customer.homepage', @@ -150,14 +152,23 @@ class DebugRendererTest extends TestCase 'activity.time_budget_open', 'activity.time_budget_open_plain', 'user.alias', + 'user.display', 'user.email', 'user.name', 'user.title', + 'user.see_others', 'user.meta.hello', 'user.meta.kitty', 'testFromModelHydrator', ]; + if ($activityCounter === 1) { + $keys = array_merge($keys, [ + 'query.activity.comment', + 'query.activity.name', + ]); + } + if ($activityCounter > 1) { $keys = array_merge($keys, [ 'activity.1.budget_open', @@ -171,6 +182,14 @@ class DebugRendererTest extends TestCase ]); } + if ($projectCounter === 1) { + $keys = array_merge($keys, [ + 'query.project.name', + 'query.project.comment', + 'query.project.order_number', + ]); + } + if ($projectCounter > 0) { $keys = array_merge($keys, [ 'project.id', @@ -254,6 +273,7 @@ class DebugRendererTest extends TestCase 'entry.weekyear', 'entry.user_id', 'entry.user_name', + 'entry.user_display', 'entry.user_alias', 'entry.user_title', 'entry.activity', diff --git a/tests/Repository/Query/TimesheetQueryTest.php b/tests/Repository/Query/TimesheetQueryTest.php index 05cf1b8e..ef87c138 100644 --- a/tests/Repository/Query/TimesheetQueryTest.php +++ b/tests/Repository/Query/TimesheetQueryTest.php @@ -61,20 +61,27 @@ class TimesheetQueryTest extends BaseQueryTest protected function assertUser(TimesheetQuery $sut): void { self::assertNull($sut->getUser()); + self::assertFalse($sut->hasUsers()); $expected = new User(); $expected->setUserIdentifier('foo-bar'); $sut->setUser($expected); self::assertEquals($expected, $sut->getUser()); + self::assertTrue($sut->hasUsers()); + + $sut->setUser(null); + self::assertFalse($sut->hasUsers()); } protected function assertUsers(TimesheetQuery $sut): void { self::assertEmpty($sut->getUsers()); + self::assertFalse($sut->hasUsers()); $user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock(); $user->method('getId')->willReturn(1); $sut->addUser($user); + self::assertTrue($sut->hasUsers()); $user = $this->getMockBuilder(User::class)->disableOriginalConstructor()->getMock(); $user->method('getId')->willReturn(1); diff --git a/tests/Repository/TagRepositoryTest.php b/tests/Repository/TagRepositoryTest.php index f8b67da4..80385547 100644 --- a/tests/Repository/TagRepositoryTest.php +++ b/tests/Repository/TagRepositoryTest.php @@ -28,30 +28,6 @@ class TagRepositoryTest extends AbstractRepositoryTest $this->importFixture($data); } - public function testFindIds() - { - $em = $this->getEntityManager(); - /** @var TagRepository $repository */ - $repository = $em->getRepository(Tag::class); - - $result = $repository->findIdsByTagNameList('2018,Test'); - $this->assertIsArray($result); - $this->assertNotEmpty($result); - $this->assertEquals(7, \count($result)); - } - - public function testFindNoIds() - { - $em = $this->getEntityManager(); - /** @var TagRepository $repository */ - $repository = $em->getRepository(Tag::class); - - $result = $repository->findIdsByTagNameList('Simply'); - $this->assertIsArray($result); - $this->assertEmpty($result); - $this->assertEquals(0, \count($result)); - } - public function testFindAllTagNames() { $em = $this->getEntityManager(); diff --git a/tests/phpstan.neon b/tests/phpstan.neon index c1d5fd31..e1f48bf9 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -1277,276 +1277,6 @@ parameters: count: 1 path: API/TimesheetControllerTest.php - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testActiveAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionForDifferentUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionForExportedRecordIsAllowedForAdmin\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionForExportedRecordIsNotAllowed\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionWithUnknownTimesheet\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDeleteActionWithoutAuthorization\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDuplicateAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testDuplicateThrowsNotFound\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testExportAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testExportNotAllowedForUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testExportThrowsNotFound\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testExportedFilter\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollection\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionForAllUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionForEmptyResult\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionForOtherUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionFull\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionWithQuery\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionWithQueryFailsWith404OnOutOfRangedPage\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionWithSingleParamsQuery\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetCollectionWithTags\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetEntity\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetEntityAccessAllowedForAdmin\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetEntityAccessDenied\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetEntityNotFound\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testGetRecentAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testInvalidPatchAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testIsSecure\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaActionThrowsExceptionOnMissingMetafield\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaActionThrowsExceptionOnMissingName\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaActionThrowsExceptionOnMissingValue\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testMetaActionThrowsNotFound\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPatchAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPatchActionWithInvalidUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPatchActionWithUnknownTimesheet\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionForDifferentUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithFullExpandedResponse\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithInvisibleActivity\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithInvisibleProject\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithNonBillableCustomer\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testPostActionWithNonBillableCustomerExplicit\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartActionWithBegin\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartActionWithCopyData\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartNotAllowedForUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testRestartThrowsNotFound\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testStopAction\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testStopActionTriggersValidationOnLongRunning\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testStopNotAllowedForUser\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\TimesheetControllerTest\\:\\:testStopThrowsNotFound\\(\\) has no return type specified\\.$#" - count: 1 - path: API/TimesheetControllerTest.php - - - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" - count: 32 - path: API/TimesheetControllerTest.php - - message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#" count: 1 @@ -8692,16 +8422,6 @@ parameters: count: 1 path: Repository/TagRepositoryTest.php - - - message: "#^Method App\\\\Tests\\\\Repository\\\\TagRepositoryTest\\:\\:testFindIds\\(\\) has no return type specified\\.$#" - count: 1 - path: Repository/TagRepositoryTest.php - - - - message: "#^Method App\\\\Tests\\\\Repository\\\\TagRepositoryTest\\:\\:testFindNoIds\\(\\) has no return type specified\\.$#" - count: 1 - path: Repository/TagRepositoryTest.php - - message: "#^Method App\\\\Tests\\\\Repository\\\\TagRepositoryTest\\:\\:testFindNoTagNames\\(\\) has no return type specified\\.$#" count: 1