diff --git a/assets/sass/invoice-pdf.scss b/assets/sass/invoice-pdf.scss index 043308dc4..5d63443c3 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 b5870d5fd..03ecd8287 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 ba312ab0f..db9e45ae7 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 993e07e40..638ea20af 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 cc96b4b7a..d5789bbdd 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 000000000..81d284929 --- /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 5ad0bcca3..000000000 --- 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 bc9288a03..421955a0a 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 a1e2b8931..22b8c3972 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 75105c539..84b2146fe 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 c78523dcc..b9aea5ddc 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 6872a1fbd..2f6b74fd0 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 05d893d10..9e14ba81d 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 68cdeae0b..dccac8a12 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 f17e2fb3b..9e1e03340 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 de9b2e9f8..7588abe43 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 70865317a..b2abd2d8b 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 72c606e99..306667135 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 3f48db34c..66c6c80c6 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 7f3e5c28b..d920638b4 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 aa09f136f..e8b567622 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 3df5702ca..501c803bc 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 73df99582..6e06f8c80 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 ba113b706..b0f017a6a 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 bcd813581..ca9cae203 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 390977246..5d42c5aca 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 ce67c22a4..0d5f874ee 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 3cfbe1b9a..1cf0433e3 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 21cc6c39b..726f01952 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 de14028fa..b247b2ab0 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 66d0d7166..398a68708 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 ca5befd17..31c4caaae 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 ba866309a..918173e66 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 3fd58623e..df7c8fed7 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 e3d8bcb6c..eb8d79caf 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 4845c92bf..cd2100ac4 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 f124c3a02..8950018a3 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 c6386f199..b01d7ea17 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 72322d0ce..26e4981d1 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 05cf1b8ec..ef87c1380 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 f8b67da4f..803855475 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 c1d5fd31b..e1f48bf98 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