release 2.0 beta (#3722)

* remove twitter link
* remove WIP file
* adjust release draft message
* reset code coverage threshold back to 0.5
* changed wordings
* re-activate wizard for fixture accounts
* fix repo url and license
* license identifier
* bump version
* moved Kimai 1 import command from core to plugin
* do not traverse into invoice template subdirectories (#3735)
* fix branch alias
* composer update
* switch language on wizard select
* new twig function to create qr code
* fix daily stats in timesheet listing
* improved html invoice templates
This commit is contained in:
Kevin Papst
2023-01-12 12:10:11 +01:00
committed by GitHub
parent cbd65f1f1d
commit 8069e332fe
38 changed files with 386 additions and 2850 deletions

View File

@@ -9,7 +9,7 @@ coverage:
status: status:
project: project:
default: default:
threshold: 2.5% threshold: 0.5%
patch: off patch: off
changes: no changes: no

View File

@@ -35,11 +35,7 @@ version-resolver:
template: | template: |
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://tobybatch.github.io/kimai2/) [Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://tobybatch.github.io/kimai2/)
**PHP Version compatibility:** **Compatible with PHP 8.1 and 8.2**
- PHP 7.3 and PHP 7.4 are [end-of-life](https://www.php.net/supported-versions.php)
- PHP 8.0 and PHP 8.1 are supported
A feature freeze is in place and only bugfix releases will be published for 1.30.x. Next major release will be in the 2.x series (PHP >= 8.1, Symfony 6, Tabler UI, see #2902).
$CHANGES $CHANGES

View File

@@ -16,10 +16,10 @@ jobs:
action: action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v3 - uses: dessant/lock-threads@v4
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: '180' issue-inactive-days: '90'
exclude-issue-created-before: '' exclude-issue-created-before: ''
exclude-issue-created-after: '' exclude-issue-created-after: ''
exclude-issue-created-between: '' exclude-issue-created-between: ''

View File

@@ -5,9 +5,8 @@
<p align="center"> <p align="center">
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/workflows/CI/badge.svg"></a> <a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/workflows/CI/badge.svg"></a>
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a> <a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a> <a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a> <a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
<a href="https://twitter.com/kimai_org" rel="me"><img alt="Twitter" src="https://img.shields.io/badge/follow-%40kimai__org-00acee"></a>
<a href="https://phpc.social/@kimai" rel="me"><img alt="Mastodon" src="https://img.shields.io/badge/toot-%40kimai-8c8dff"></a> <a href="https://phpc.social/@kimai" rel="me"><img alt="Mastodon" src="https://img.shields.io/badge/toot-%40kimai-8c8dff"></a>
</p> </p>
@@ -75,11 +74,11 @@ The best way to start is to [open a new issue](https://github.com/kimai/kimai/is
In case you want to contribute, but you wouldn't know how, here are some suggestions: In case you want to contribute, but you wouldn't know how, here are some suggestions:
- Spread the word: More user means more people testing and contributing to Kimai - which in turn means better stability and more and better features. Please vote for Kimai on platforms like Slant, Product Hunt, Softpedia or AlternativeTo, you can tweet about it, share it on LinkedIn, reddit or any of your favorite social media platforms. Every bit helps! - Spread the word: More user means more people testing and contributing to Kimai - which in turn means better stability and more and better features. Please vote for Kimai on platforms like Slant, Product Hunt, Softpedia or AlternativeTo, you can toot or tweet about it, share it on LinkedIn, reddit or any of your favorite social media platforms. Every bit helps!
- Answer questions: You know the answer to another user's problem? Share your knowledge! - Answer questions: You know the answer to another user's problem? Share your knowledge.
- Make a feature request: Something can be done better? Something essential missing? Let us know! - Something can be done better? An essential feature is missing? Create a feature request.
- Report bugs - Report bugs: that shouldn't happen too often.
- You don't have to be programmer to help. The documentation and translation could use some love as well. - You don't have to be programmer, the documentation and translation could use some love as well.
- Sponsor the project, free software still costs money - Sponsor the project: free software costs money to create!
There is one simple rule in our "Code of conduct": Don't be an ass! There is one simple rule in our "Code of conduct": Don't be an ass!

36
TODO
View File

@@ -1,36 +0,0 @@
===========================================================================
INVOICES
- HTML Rechnungstemplates funktionieren nicht mehr richtig
===========================================================================
THEME
- Update to latest release und angepasstes CSS entfernen
- Theme Dark Mode mit Modus "Browser" sollte Standard sein:
https://github.com/tabler/tabler/issues/892#event-5666309557
===========================================================================
MIXED
- Alle URLs ändern
- /admin/activity/ zu /activity/
- /admin/project/ zu /project/
- /admin/..../ zu /..../
- /team/timesheet/ zu /timesheets/
- Tags => immer Berechtigung prüfen und "create" option über die VIEW ans Javascript geben
=> den FormType nur ändern zu "AutoComplete" wenn es mehr als 500 Tags sind
=> Übersetzungen (Suche, Erstellen, Keine Ergebnisse) über data attribute ans JS durchreichen
===========================================================================
MIGRATIONS
- rename 2.0 migration once it will be released
===========================================================================

View File

@@ -8,7 +8,7 @@ you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process. Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation. Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [2.0](https://github.com/kevinpapst/kimai2/releases/tag/2.0) ## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
**!! This release requires minimum PHP version to 8.1 !!** **!! This release requires minimum PHP version to 8.1 !!**

View File

@@ -9,9 +9,6 @@
@import "~bootstrap/scss/variables"; @import "~bootstrap/scss/variables";
@import "~bootstrap/scss/maps"; @import "~bootstrap/scss/maps";
@import "~bootstrap/scss/mixins"; @import "~bootstrap/scss/mixins";
//@import "~bootstrap/scss/normalize";
//@import "~bootstrap/scss/print";
//@import "~bootstrap/scss/scaffolding";
@import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type"; @import "~bootstrap/scss/type";
@import "~bootstrap/scss/grid"; @import "~bootstrap/scss/grid";
@@ -19,12 +16,12 @@
@import "~bootstrap/scss/list-group"; @import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/card"; @import "~bootstrap/scss/card";
@import "~bootstrap/scss/utilities"; @import "~bootstrap/scss/utilities";
//@import "~bootstrap/scss/responsive-utilities";
body { body {
font-family: $font-family-sans-serif; font-family: $font-family-sans-serif;
&.invoice_print { &.invoice_print {
--bs-body-font-size: 13px;
background-color: #eee; background-color: #eee;
.table.no-border, .table.no-border td, .table.no-border th { .table.no-border, .table.no-border td, .table.no-border th {
@@ -35,6 +32,38 @@ body {
font-family: $font-family-sans-serif; font-family: $font-family-sans-serif;
} }
.mt-2 {
margin-top: 2em;
}
.mb-3 {
margin-bottom: 3em;
}
.pt-1 {
padding-top: 1em;
}
.pb-4 {
padding-bottom: 4em;
}
.ps-0 {
padding-left: 0;
}
.bt-1 {
border-top: 1px solid #000000;
}
.pull-right {
float: right;
}
.text-end {
text-align: right;
}
.invoice { .invoice {
margin: 105px auto 30px auto; margin: 105px auto 30px auto;
padding: 50px 65px; padding: 50px 65px;
@@ -48,7 +77,7 @@ body {
.page-header { .page-header {
margin: 10px 0 20px 0; margin: 10px 0 20px 0;
font-size: 22px; font-size: 20px;
> small { > small {
color: #666; color: #666;
@@ -79,14 +108,11 @@ body {
} }
.invoice-items { .invoice-items {
margin-top: 2em;
margin-bottom: 3em;
.table { .table {
thead th { thead th {
font-weight: bold; font-weight: bold;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
padding-bottom: 15px padding-bottom: 10px
} }
tfoot { tfoot {
@@ -105,7 +131,6 @@ body {
} }
} }
} }
} }
.footer { .footer {

View File

@@ -1,6 +1,6 @@
{ {
"name": "kimai/kimai", "name": "kimai/kimai",
"license": "MIT", "license": "AGPL-3.0-or-later",
"type": "project", "type": "project",
"description": "Kimai - Time Tracking", "description": "Kimai - Time Tracking",
"authors": [ "authors": [
@@ -10,7 +10,7 @@
}, },
{ {
"name": "All contributors", "name": "All contributors",
"homepage": "https://github.com/kevinpapst/kimai2/contributors" "homepage": "https://github.com/kimai/kimai/contributors"
} }
], ],
"require": { "require": {
@@ -191,7 +191,7 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"v2.x-dev": "2.0.x-dev" "dev-main": "2.0.x-dev"
}, },
"symfony": { "symfony": {
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR", "id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",

103
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "16922ce13576f2e86b8a3c380fcf814a", "content-hash": "f3b0627c043009647c1ab9c3ddea3f22",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@@ -796,16 +796,16 @@
}, },
{ {
"name": "doctrine/doctrine-bundle", "name": "doctrine/doctrine-bundle",
"version": "2.8.0", "version": "2.8.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/DoctrineBundle.git", "url": "https://github.com/doctrine/DoctrineBundle.git",
"reference": "0421ebc069519a0f19b9c39e5dc18c359be0feab" "reference": "fe9b2cc1cd0c9b76553b1d4c1a077590ba231a2d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0421ebc069519a0f19b9c39e5dc18c359be0feab", "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/fe9b2cc1cd0c9b76553b1d4c1a077590ba231a2d",
"reference": "0421ebc069519a0f19b9c39e5dc18c359be0feab", "reference": "fe9b2cc1cd0c9b76553b1d4c1a077590ba231a2d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -891,7 +891,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/DoctrineBundle/issues", "issues": "https://github.com/doctrine/DoctrineBundle/issues",
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.8.0" "source": "https://github.com/doctrine/DoctrineBundle/tree/2.8.1"
}, },
"funding": [ "funding": [
{ {
@@ -907,7 +907,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-12-28T16:35:32+00:00" "time": "2023-01-06T00:24:26+00:00"
}, },
{ {
"name": "doctrine/doctrine-migrations-bundle", "name": "doctrine/doctrine-migrations-bundle",
@@ -1685,16 +1685,16 @@
}, },
{ {
"name": "egulias/email-validator", "name": "egulias/email-validator",
"version": "3.2.4", "version": "3.2.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/egulias/EmailValidator.git", "url": "https://github.com/egulias/EmailValidator.git",
"reference": "5f35e41eba05fdfbabd95d72f83795c835fb7ed2" "reference": "b531a2311709443320c786feb4519cfaf94af796"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/5f35e41eba05fdfbabd95d72f83795c835fb7ed2", "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b531a2311709443320c786feb4519cfaf94af796",
"reference": "5f35e41eba05fdfbabd95d72f83795c835fb7ed2", "reference": "b531a2311709443320c786feb4519cfaf94af796",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1703,7 +1703,6 @@
"symfony/polyfill-intl-idn": "^1.15" "symfony/polyfill-intl-idn": "^1.15"
}, },
"require-dev": { "require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^8.5.8|^9.3.3", "phpunit/phpunit": "^8.5.8|^9.3.3",
"vimeo/psalm": "^4" "vimeo/psalm": "^4"
}, },
@@ -1741,7 +1740,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/egulias/EmailValidator/issues", "issues": "https://github.com/egulias/EmailValidator/issues",
"source": "https://github.com/egulias/EmailValidator/tree/3.2.4" "source": "https://github.com/egulias/EmailValidator/tree/3.2.5"
}, },
"funding": [ "funding": [
{ {
@@ -1749,7 +1748,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-12-30T14:09:25+00:00" "time": "2023-01-02T17:26:14+00:00"
}, },
{ {
"name": "endroid/qr-code", "name": "endroid/qr-code",
@@ -9815,16 +9814,16 @@
}, },
{ {
"name": "tijsverkoyen/css-to-inline-styles", "name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.5", "version": "2.2.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
"reference": "4348a3a06651827a27d989ad1d13efec6bb49b19" "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/4348a3a06651827a27d989ad1d13efec6bb49b19", "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/c42125b83a4fa63b187fdf29f9c93cb7733da30c",
"reference": "4348a3a06651827a27d989ad1d13efec6bb49b19", "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9862,9 +9861,9 @@
"homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
"support": { "support": {
"issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
"source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.5" "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.6"
}, },
"time": "2022-09-12T13:28:28+00:00" "time": "2023-01-03T09:29:04+00:00"
}, },
{ {
"name": "twig/cssinliner-extra", "name": "twig/cssinliner-extra",
@@ -10462,20 +10461,20 @@
}, },
{ {
"name": "zircote/swagger-php", "name": "zircote/swagger-php",
"version": "4.5.3", "version": "4.5.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/zircote/swagger-php.git", "url": "https://github.com/zircote/swagger-php.git",
"reference": "e505bce612a86fe90f8fd50917e0848afc5d2ba8" "reference": "09356f4d68d29bdf3254811fb2602a5d5d1788ea"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/e505bce612a86fe90f8fd50917e0848afc5d2ba8", "url": "https://api.github.com/repos/zircote/swagger-php/zipball/09356f4d68d29bdf3254811fb2602a5d5d1788ea",
"reference": "e505bce612a86fe90f8fd50917e0848afc5d2ba8", "reference": "09356f4d68d29bdf3254811fb2602a5d5d1788ea",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"doctrine/annotations": "^1.7", "doctrine/annotations": "^1.7 || ^2.0",
"ext-json": "*", "ext-json": "*",
"php": ">=7.2", "php": ">=7.2",
"psr/log": "^1.1 || ^2.0 || ^3.0", "psr/log": "^1.1 || ^2.0 || ^3.0",
@@ -10534,9 +10533,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/zircote/swagger-php/issues", "issues": "https://github.com/zircote/swagger-php/issues",
"source": "https://github.com/zircote/swagger-php/tree/4.5.3" "source": "https://github.com/zircote/swagger-php/tree/4.5.4"
}, },
"time": "2022-12-21T18:26:59+00:00" "time": "2023-01-04T00:51:43+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
@@ -10746,16 +10745,16 @@
}, },
{ {
"name": "doctrine/data-fixtures", "name": "doctrine/data-fixtures",
"version": "1.6.1", "version": "1.6.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/data-fixtures.git", "url": "https://github.com/doctrine/data-fixtures.git",
"reference": "1a4232c15143ca3c127812d19b23a7961c41eeed" "reference": "d52cc6d392717734fac908768a7319f8a417401a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/1a4232c15143ca3c127812d19b23a7961c41eeed", "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/d52cc6d392717734fac908768a7319f8a417401a",
"reference": "1a4232c15143ca3c127812d19b23a7961c41eeed", "reference": "d52cc6d392717734fac908768a7319f8a417401a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10808,7 +10807,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/data-fixtures/issues", "issues": "https://github.com/doctrine/data-fixtures/issues",
"source": "https://github.com/doctrine/data-fixtures/tree/1.6.1" "source": "https://github.com/doctrine/data-fixtures/tree/1.6.2"
}, },
"funding": [ "funding": [
{ {
@@ -10824,7 +10823,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-12-23T12:13:51+00:00" "time": "2023-01-05T18:42:27+00:00"
}, },
{ {
"name": "doctrine/doctrine-fixtures-bundle", "name": "doctrine/doctrine-fixtures-bundle",
@@ -10979,16 +10978,16 @@
}, },
{ {
"name": "friendsofphp/php-cs-fixer", "name": "friendsofphp/php-cs-fixer",
"version": "v3.13.1", "version": "v3.13.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "78d2251dd86b49c609a0fd37c20dcf0a00aea5a7" "reference": "3952f08a81bd3b1b15e11c3de0b6bf037faa8496"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/78d2251dd86b49c609a0fd37c20dcf0a00aea5a7", "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/3952f08a81bd3b1b15e11c3de0b6bf037faa8496",
"reference": "78d2251dd86b49c609a0fd37c20dcf0a00aea5a7", "reference": "3952f08a81bd3b1b15e11c3de0b6bf037faa8496",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -11056,7 +11055,7 @@
"description": "A tool to automatically fix PHP code style", "description": "A tool to automatically fix PHP code style",
"support": { "support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.13.1" "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.13.2"
}, },
"funding": [ "funding": [
{ {
@@ -11064,7 +11063,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-12-18T00:47:22+00:00" "time": "2023-01-02T23:53:50+00:00"
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
@@ -11235,16 +11234,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.9.4", "version": "1.9.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2" "reference": "0501435cd342eac7664bd62155b1ef907fc60b6f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03bccee595e2146b7c9d174486b84f4dc61b0f2", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0501435cd342eac7664bd62155b1ef907fc60b6f",
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2", "reference": "0501435cd342eac7664bd62155b1ef907fc60b6f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -11274,7 +11273,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan/issues", "issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.9.4" "source": "https://github.com/phpstan/phpstan/tree/1.9.7"
}, },
"funding": [ "funding": [
{ {
@@ -11290,25 +11289,25 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-12-17T13:33:52+00:00" "time": "2023-01-04T21:59:57+00:00"
}, },
{ {
"name": "phpstan/phpstan-doctrine", "name": "phpstan/phpstan-doctrine",
"version": "1.3.28", "version": "1.3.29",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-doctrine.git", "url": "https://github.com/phpstan/phpstan-doctrine.git",
"reference": "8302a6a214b8cbbda8249cce6ec627033af26c12" "reference": "4967ebbc24a2d7e94f5b2f6dad78e0087dd52fc3"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/8302a6a214b8cbbda8249cce6ec627033af26c12", "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/4967ebbc24a2d7e94f5b2f6dad78e0087dd52fc3",
"reference": "8302a6a214b8cbbda8249cce6ec627033af26c12", "reference": "4967ebbc24a2d7e94f5b2f6dad78e0087dd52fc3",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2 || ^8.0", "php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.8.11" "phpstan/phpstan": "^1.9.7"
}, },
"conflict": { "conflict": {
"doctrine/collections": "<1.0", "doctrine/collections": "<1.0",
@@ -11357,9 +11356,9 @@
"description": "Doctrine extensions for PHPStan", "description": "Doctrine extensions for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-doctrine/issues", "issues": "https://github.com/phpstan/phpstan-doctrine/issues",
"source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.28" "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.29"
}, },
"time": "2022-12-30T21:24:11+00:00" "time": "2023-01-04T21:51:32+00:00"
}, },
{ {
"name": "phpstan/phpstan-phpunit", "name": "phpstan/phpstan-phpunit",

View File

@@ -570,316 +570,6 @@ parameters:
count: 1 count: 1
path: src/Command/InvoiceCreateCommand.php path: src/Command/InvoiceCreateCommand.php
-
message: "#^Call to an undefined method object\\:\\:getEventManager\\(\\)\\.$#"
count: 3
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getCustomers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getName\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 4
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getProjects\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getTimezone\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method getUsers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Cannot call method hasUsers\\(\\) on App\\\\Entity\\\\Team\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:countFromImport\\(\\) has parameter \\$where with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:countFromImport\\(\\) should return int but returns mixed\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createActivity\\(\\) should return App\\\\Entity\\\\Activity but returns App\\\\Entity\\\\Activity\\|null\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createInstanceTeam\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:createInstanceTeam\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:deactivateLifecycleCallbacks\\(\\) has no return type specified\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchAllFromImport\\(\\) has parameter \\$where with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchAllFromImport\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:fetchIteratorFromImport\\(\\) return type has no value type specified in iterable type Traversable\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importActivities\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importCustomers\\(\\) has parameter \\$customers with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$projects with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importProjects\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importTimesheetRecords\\(\\) has parameter \\$fixedRates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importTimesheetRecords\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importUsers\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:importUsers\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownActivity\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownCustomer\\(\\) has parameter \\$oldCustomer with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownGroup\\(\\) has parameter \\$oldGroup with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownProject\\(\\) has parameter \\$oldProject with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:isKnownUser\\(\\) has parameter \\$oldUser with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:prepareOptionsFromInput\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setActivityCache\\(\\) has parameter \\$oldActivity with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setCustomerCache\\(\\) has parameter \\$oldCustomer with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setGroupCache\\(\\) has parameter \\$oldGroup with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setProjectCache\\(\\) has parameter \\$oldProject with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:setUserCache\\(\\) has parameter \\$oldUser with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$customer with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$projects with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$rates with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateKimai1Data\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$customer of method App\\\\Entity\\\\Team\\:\\:addCustomer\\(\\) expects App\\\\Entity\\\\Customer, App\\\\Entity\\\\Customer\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$name of method App\\\\Entity\\\\TimesheetMeta\\:\\:setName\\(\\) expects string, mixed given\\.$#"
count: 3
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$object of method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:persist\\(\\) expects object, App\\\\Entity\\\\Team\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$string of function strtolower expects string, string\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Team\\:\\:addTeamlead\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Team\\:\\:addUser\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#1 \\$user of method App\\\\Entity\\\\Timesheet\\:\\:setUser\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#2 \\$object of method App\\\\Command\\\\KimaiImporterCommand\\:\\:validateImport\\(\\) expects object, App\\\\Entity\\\\Team\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#2 \\$plainPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\Hasher\\\\UserPasswordHasherInterface\\:\\:hashPassword\\(\\) expects string, string\\|null given\\.$#"
count: 2
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#2 \\$team of method App\\\\Command\\\\KimaiImporterCommand\\:\\:setGroupCache\\(\\) expects App\\\\Entity\\\\Team, App\\\\Entity\\\\Team\\|null given\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
-
message: "#^Parameter \\#2 \\$version2 of function version_compare expects string, mixed given\\.$#"
count: 2
path: src/Command/KimaiImporterCommand.php
-
message: "#^Property App\\\\Command\\\\KimaiImporterCommand\\:\\:\\$oldActivities type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Command/KimaiImporterCommand.php
- -
message: "#^Cannot call method getKimaiVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#" message: "#^Cannot call method getKimaiVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1 count: 1
@@ -6761,7 +6451,7 @@ parameters:
path: src/Project/ProjectStatisticService.php path: src/Project/ProjectStatisticService.php
- -
message: "#^Strict comparison using \\!\\=\\= between null and array\\<array\\{id\\: int\\|numeric\\-string, duration\\: int\\|numeric\\-string, rate\\: float\\|int\\|numeric\\-string, internalRate\\: float\\|int\\|numeric\\-string, counter\\: int\\<0, max\\>\\|numeric\\-string, billable\\: bool, exported\\: bool\\}\\> will always evaluate to true\\.$#" message: "#^Strict comparison using \\!\\=\\= between null and list\\<array\\{id\\: int\\|numeric\\-string, duration\\: int\\|numeric\\-string, rate\\: float\\|int\\|numeric\\-string, internalRate\\: float\\|int\\|numeric\\-string, counter\\: int\\<0, max\\>\\|numeric\\-string, billable\\: bool, exported\\: bool\\}\\> will always evaluate to true\\.$#"
count: 1 count: 1
path: src/Project/ProjectStatisticService.php path: src/Project/ProjectStatisticService.php
@@ -6970,31 +6660,6 @@ parameters:
count: 1 count: 1
path: src/Repository/CustomerRepository.php path: src/Repository/CustomerRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:__construct\\(\\) has parameter \\$directories with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:addDirectory\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:findByPaths\\(\\) has parameter \\$paths with no value type specified in iterable type array\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:removeDirectory\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
-
message: "#^Parameter \\#1 \\$filename of function unlink expects string, string\\|false given\\.$#"
count: 1
path: src/Repository/InvoiceDocumentRepository.php
- -
message: "#^Cannot access offset 'counter' on mixed\\.$#" message: "#^Cannot access offset 'counter' on mixed\\.$#"
count: 1 count: 1
@@ -7135,11 +6800,6 @@ parameters:
count: 1 count: 1
path: src/Repository/Paginator/LoaderPaginator.php path: src/Repository/Paginator/LoaderPaginator.php
-
message: "#^Method App\\\\Repository\\\\Paginator\\\\LoaderPaginator\\:\\:getResults\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/Paginator/LoaderPaginator.php
- -
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\) expects array, mixed given\\.$#" message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\) expects array, mixed given\\.$#"
count: 1 count: 1
@@ -7165,11 +6825,6 @@ parameters:
count: 1 count: 1
path: src/Repository/Paginator/QueryBuilderPaginator.php path: src/Repository/Paginator/QueryBuilderPaginator.php
-
message: "#^Method App\\\\Repository\\\\Paginator\\\\QueryBuilderPaginator\\:\\:getResults\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/Paginator/QueryBuilderPaginator.php
- -
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#" message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
count: 1 count: 1

View File

@@ -24,7 +24,7 @@
"/build/invoice.c8ae95ad.js" "/build/invoice.c8ae95ad.js"
], ],
"css": [ "css": [
"/build/invoice.b17784c1.css" "/build/invoice.3c80ee80.css"
] ]
}, },
"invoice-pdf": { "invoice-pdf": {
@@ -68,7 +68,7 @@
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW", "/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg", "/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks", "/build/invoice.c8ae95ad.js": "sha384-2eVY7MBiMQxo1vhfizU+fYfEZbz9bYUdzqxnpTQhxpYiLaxeSV4WnaObq2G7/Pks",
"/build/invoice.b17784c1.css": "sha384-Y1e218/TMmOrl/aQcb77ix5qgFQDPPWpeD6GUtnX16Buj7/Mr6arWMSXFdqlJzAc", "/build/invoice.3c80ee80.css": "sha384-xjFM2m/EeN7z42ygpt77ll5zAcHTeOjbZFAw2Qcu3IJutgebiAbrXjnH5aJfF5Z6",
"/build/invoice-pdf.d86b82ee.js": "sha384-A0HJqP+MvEqQr1uG8wViCeEWxBRKyS6l8D+Ao4pFYHUA12gCC1gRYhk9I+SJPvZq", "/build/invoice-pdf.d86b82ee.js": "sha384-A0HJqP+MvEqQr1uG8wViCeEWxBRKyS6l8D+Ao4pFYHUA12gCC1gRYhk9I+SJPvZq",
"/build/invoice-pdf.c88953bb.css": "sha384-ZvSi1e+ZKGzvZJUtAPLjzOSTh13N9zRevq44GKdYdBja/DAplGE55saY2Ur+83yv", "/build/invoice-pdf.c88953bb.css": "sha384-ZvSi1e+ZKGzvZJUtAPLjzOSTh13N9zRevq44GKdYdBja/DAplGE55saY2Ur+83yv",
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W", "/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@
"build/app.js": "/build/app.694c6cb5.js", "build/app.js": "/build/app.694c6cb5.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css", "build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.587575e7.js", "build/export-pdf.js": "/build/export-pdf.587575e7.js",
"build/invoice.css": "/build/invoice.b17784c1.css", "build/invoice.css": "/build/invoice.3c80ee80.css",
"build/invoice.js": "/build/invoice.c8ae95ad.js", "build/invoice.js": "/build/invoice.c8ae95ad.js",
"build/invoice-pdf.css": "/build/invoice-pdf.c88953bb.css", "build/invoice-pdf.css": "/build/invoice-pdf.c88953bb.css",
"build/invoice-pdf.js": "/build/invoice-pdf.d86b82ee.js", "build/invoice-pdf.js": "/build/invoice-pdf.d86b82ee.js",

View File

@@ -9,12 +9,17 @@
namespace App\API\Authentication; namespace App\API\Authentication;
use App\Entity\User;
use Symfony\Component\Security\Core\Exception\LogicException; use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
final class ApiTokenUpgradeBadge implements BadgeInterface final class ApiTokenUpgradeBadge implements BadgeInterface
{ {
/**
* @param string|null $plaintextApiToken
* @param PasswordUpgraderInterface<User> $passwordUpgrader
*/
public function __construct(private ?string $plaintextApiToken, private PasswordUpgraderInterface $passwordUpgrader) public function __construct(private ?string $plaintextApiToken, private PasswordUpgraderInterface $passwordUpgrader)
{ {
} }
@@ -31,6 +36,9 @@ final class ApiTokenUpgradeBadge implements BadgeInterface
return $password; return $password;
} }
/**
* @return PasswordUpgraderInterface<User>
*/
public function getPasswordUpgrader(): PasswordUpgraderInterface public function getPasswordUpgrader(): PasswordUpgraderInterface
{ {
return $this->passwordUpgrader; return $this->passwordUpgrader;

File diff suppressed because it is too large Load Diff

View File

@@ -77,18 +77,18 @@ abstract class TimesheetAbstractController extends AbstractController
$table->setPaginationRoute($paginationRoute); $table->setPaginationRoute($paginationRoute);
$table->setReloadEvents('kimai.timesheetUpdate kimai.timesheetDelete'); $table->setReloadEvents('kimai.timesheetUpdate kimai.timesheetDelete');
$table->addColumn('date', ['class' => 'alwaysVisible', 'orderBy' => 'begin']); $table->addColumn('date', ['class' => 'alwaysVisible text-nowrap', 'orderBy' => 'begin']);
if ($this->canSeeStartEndTime()) { if ($this->canSeeStartEndTime()) {
$table->addColumn('starttime', ['class' => 'd-none d-sm-table-cell text-center', 'orderBy' => 'begin']); $table->addColumn('starttime', ['class' => 'd-none d-sm-table-cell text-center text-nowrap', 'orderBy' => 'begin']);
$table->addColumn('endtime', ['class' => 'd-none d-sm-table-cell text-center', 'orderBy' => 'end']); $table->addColumn('endtime', ['class' => 'd-none d-sm-table-cell text-center text-nowrap', 'orderBy' => 'end']);
} }
$table->addColumn('duration', ['class' => 'text-end text-nowrap']); $table->addColumn('duration', ['class' => 'text-end text-nowrap']);
if ($canSeeRate) { if ($canSeeRate) {
$table->addColumn('hourlyRate', ['class' => 'text-end d-none']); $table->addColumn('hourlyRate', ['class' => 'text-end d-none text-nowrap']);
$table->addColumn('rate', ['class' => 'text-end']); $table->addColumn('rate', ['class' => 'text-end text-nowrap']);
} }
$table->addColumn('customer', ['class' => 'd-none d-md-table-cell']); $table->addColumn('customer', ['class' => 'd-none d-md-table-cell']);
@@ -98,7 +98,7 @@ abstract class TimesheetAbstractController extends AbstractController
$table->addColumn('tags', ['class' => 'd-none badges', 'orderBy' => false]); $table->addColumn('tags', ['class' => 'd-none badges', 'orderBy' => false]);
foreach ($metaColumns as $metaColumn) { foreach ($metaColumns as $metaColumn) {
$table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false]); $table->addColumn('mf_' . $metaColumn->getName(), ['title' => $metaColumn->getLabel(), 'class' => 'd-none', 'orderBy' => false, 'data' => $metaColumn]);
} }
if ($canSeeUsername) { if ($canSeeUsername) {
@@ -116,11 +116,8 @@ abstract class TimesheetAbstractController extends AbstractController
'page_setup' => $page, 'page_setup' => $page,
'dataTable' => $table, 'dataTable' => $table,
'action_single' => $this->getActionNameSingle(), 'action_single' => $this->getActionNameSingle(),
'canSeeUsername' => $canSeeUsername,
'canSeeRate' => $canSeeRate,
'stats' => $result->getStatistic(), 'stats' => $result->getStatistic(),
'showSummary' => $this->includeSummary(), 'showSummary' => $this->includeSummary(),
'showStartEndTime' => $this->canSeeStartEndTime(),
'metaColumns' => $metaColumns, 'metaColumns' => $metaColumns,
'allowMarkdown' => $this->hasMarkdownSupport(), 'allowMarkdown' => $this->hasMarkdownSupport(),
'editRoute' => $this->getEditRoute() 'editRoute' => $this->getEditRoute()

View File

@@ -16,6 +16,7 @@ use App\Form\Type\SkinType;
use App\Form\Type\TimezoneType; use App\Form\Type\TimezoneType;
use App\User\UserService; use App\User\UserService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@@ -42,18 +43,17 @@ final class WizardController extends AbstractController
if ($wizard === 'profile') { if ($wizard === 'profile') {
$data = [ $data = [
'language' => $request->getLocale(), UserPreference::LOCALE => $request->getLocale(),
'timezone' => $user->getTimezone(), UserPreference::TIMEZONE => $user->getTimezone(),
UserPreference::SKIN => $user->getSkin(),
'reload' => '0',
]; ];
$form = $this->createFormBuilder($data) $form = $this->createFormBuilder($data)
->add(UserPreference::LOCALE, LanguageType::class) ->add(UserPreference::LOCALE, LanguageType::class)
->add(UserPreference::TIMEZONE, TimezoneType::class) ->add(UserPreference::TIMEZONE, TimezoneType::class)
->add(UserPreference::SKIN, SkinType::class, [ ->add(UserPreference::SKIN, SkinType::class)
'attr' => [ ->add('reload', HiddenType::class)
'onchange' => "document.body.classList.remove('theme-light');document.body.classList.remove('theme-light');"
],
])
->setAction($this->generateUrl('wizard', ['wizard' => 'profile'])) ->setAction($this->generateUrl('wizard', ['wizard' => 'profile']))
->setMethod('POST') ->setMethod('POST')
->getForm(); ->getForm();
@@ -69,7 +69,11 @@ final class WizardController extends AbstractController
$user->setWizardAsSeen('profile'); $user->setWizardAsSeen('profile');
$userService->updateUser($user); $userService->updateUser($user);
return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]); if ($data['reload'] === '1') {
return $this->redirectToRoute('wizard', ['wizard' => 'profile', '_locale' => $data['language']]);
} else {
return $this->redirectToRoute('wizard', ['wizard' => 'done', '_locale' => $data['language']]);
}
} }
return $this->render('wizard/profile.html.twig', [ return $this->render('wizard/profile.html.twig', [

View File

@@ -80,9 +80,12 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
$prefs = $this->getUserPreferences($user, $userData[7]); $prefs = $this->getUserPreferences($user, $userData[7]);
$user->setPreferences($prefs); $user->setPreferences($prefs);
// better to be able to test the wizard in demo installations
/*
foreach (User::WIZARDS as $wizard) { foreach (User::WIZARDS as $wizard) {
$user->setWizardAsSeen($wizard); $user->setWizardAsSeen($wizard);
} }
*/
$manager->persist($prefs[0]); $manager->persist($prefs[0]);
$manager->persist($prefs[1]); $manager->persist($prefs[1]);
} }

View File

@@ -439,6 +439,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return (bool) $this->getPreferenceValue('export_decimal', false, false); return (bool) $this->getPreferenceValue('export_decimal', false, false);
} }
public function getSkin(): string
{
return (string) $this->getPreferenceValue(UserPreference::SKIN, 'default', false);
}
public function setTimezone(?string $timezone) public function setTimezone(?string $timezone)
{ {
if ($timezone === null) { if ($timezone === null) {

View File

@@ -15,6 +15,9 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
/**
* @template-implements PasswordUpgraderInterface<User>
*/
class ApiUserRepository implements UserLoaderInterface, PasswordUpgraderInterface class ApiUserRepository implements UserLoaderInterface, PasswordUpgraderInterface
{ {
public function __construct(private UserRepository $userRepository) public function __construct(private UserRepository $userRepository)

View File

@@ -11,6 +11,7 @@ namespace App\Repository;
use App\Model\InvoiceDocument; use App\Model\InvoiceDocument;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
final class InvoiceDocumentRepository final class InvoiceDocumentRepository
{ {
@@ -21,6 +22,9 @@ final class InvoiceDocumentRepository
*/ */
private array $documentDirs = []; private array $documentDirs = [];
/**
* @param array<string> $directories
*/
public function __construct(array $directories) public function __construct(array $directories)
{ {
foreach ($directories as $directory) { foreach ($directories as $directory) {
@@ -31,23 +35,19 @@ final class InvoiceDocumentRepository
/** /**
* @CloudRequired * @CloudRequired
*/ */
public function addDirectory(string $directory) public function addDirectory(string $directory): void
{ {
$this->documentDirs[] = $directory; $this->documentDirs[] = $directory;
return $this;
} }
/** /**
* @CloudRequired * @CloudRequired
*/ */
public function removeDirectory(string $directory) public function removeDirectory(string $directory): void
{ {
if (($key = array_search($directory, $this->documentDirs)) !== false) { if (($key = array_search($directory, $this->documentDirs)) !== false) {
unset($this->documentDirs[$key]); unset($this->documentDirs[$key]);
} }
return $this;
} }
/** /**
@@ -59,7 +59,12 @@ final class InvoiceDocumentRepository
throw new \InvalidArgumentException('Cannot delete built-in invoice template'); throw new \InvalidArgumentException('Cannot delete built-in invoice template');
} }
@unlink(realpath($invoiceDocument->getFilename())); $realpath = realpath($invoiceDocument->getFilename());
if ($realpath === false) {
throw new \InvalidArgumentException('Template does not exist: ' . $invoiceDocument->getFilename());
}
@unlink($realpath);
} }
public function getUploadDirectory(): string public function getUploadDirectory(): string
@@ -135,6 +140,7 @@ final class InvoiceDocumentRepository
/** /**
* Returns an array of invoice documents. * Returns an array of invoice documents.
* *
* @param array<string> $paths
* @return InvoiceDocument[] * @return InvoiceDocument[]
*/ */
private function findByPaths(array $paths): array private function findByPaths(array $paths): array
@@ -153,7 +159,8 @@ final class InvoiceDocumentRepository
continue; continue;
} }
$finder = Finder::create()->ignoreDotFiles(true)->files()->in($searchDir)->name('*.*'); $finder = Finder::create()->ignoreDotFiles(true)->files()->in($searchDir)->depth(0)->name('*.*');
/** @var SplFileInfo $file */
foreach ($finder->getIterator() as $file) { foreach ($finder->getIterator() as $file) {
$doc = new InvoiceDocument($file); $doc = new InvoiceDocument($file);
// the first found invoice document wins // the first found invoice document wins

View File

@@ -24,6 +24,9 @@ final class LoaderPaginator implements PaginatorInterface
return $this->results; return $this->results;
} }
/**
* @return iterable<array-key, iterable<mixed>>
*/
public function getSlice(int $offset, int $length): iterable public function getSlice(int $offset, int $length): iterable
{ {
$query = $this->query $query = $this->query
@@ -34,13 +37,17 @@ final class LoaderPaginator implements PaginatorInterface
return $this->getResults($query); return $this->getResults($query);
} }
/**
* @param Query<null, mixed> $query
* @return iterable<array-key, iterable<mixed>>
*/
private function getResults(Query $query) private function getResults(Query $query)
{ {
$results = $query->execute(); $results = $query->execute();
$this->loader->loadResults($results); $this->loader->loadResults($results);
return $results; return $results; // @phpstan-ignore-line
} }
public function getAll(): iterable public function getAll(): iterable

View File

@@ -23,6 +23,9 @@ final class QueryBuilderPaginator implements PaginatorInterface
return $this->results; return $this->results;
} }
/**
* @return iterable<array-key, iterable<mixed>>
*/
public function getSlice(int $offset, int $length): iterable public function getSlice(int $offset, int $length): iterable
{ {
$query = $this->query $query = $this->query
@@ -33,9 +36,13 @@ final class QueryBuilderPaginator implements PaginatorInterface
return $this->getResults($query); return $this->getResults($query);
} }
/**
* @param Query<null, mixed> $query
* @return iterable<array-key, iterable<mixed>>
*/
private function getResults(Query $query) private function getResults(Query $query)
{ {
return $query->execute(); return $query->execute(); // @phpstan-ignore-line
} }
public function getAll(): iterable public function getAll(): iterable

View File

@@ -34,6 +34,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
/** /**
* @extends \Doctrine\ORM\EntityRepository<User> * @extends \Doctrine\ORM\EntityRepository<User>
* @template-implements PasswordUpgraderInterface<User>
*/ */
class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface class UserRepository extends EntityRepository implements UserLoaderInterface, UserProviderInterface, PasswordUpgraderInterface
{ {
@@ -58,7 +59,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
$entityManager->flush(); $entityManager->flush();
} }
public function upgradePassword(PasswordAuthenticatedUserInterface|UserInterface $user, string $newHashedPassword): void public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{ {
if (!($user instanceof User)) { if (!($user instanceof User)) {
return; return;

View File

@@ -10,6 +10,7 @@
namespace App\Security; namespace App\Security;
use App\Configuration\SystemConfiguration; use App\Configuration\SystemConfiguration;
use App\Entity\User;
use App\Ldap\LdapUserProvider; use App\Ldap\LdapUserProvider;
use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\ChainUserProvider;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
@@ -17,6 +18,9 @@ use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* @template-implements PasswordUpgraderInterface<User>
*/
final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface final class KimaiUserProvider implements UserProviderInterface, PasswordUpgraderInterface
{ {
private ?ChainUserProvider $provider = null; private ?ChainUserProvider $provider = null;

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig\Runtime;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
use Endroid\QrCode\Writer\PngWriter;
use Twig\Extension\RuntimeExtensionInterface;
final class QrCodeExtension implements RuntimeExtensionInterface
{
public function __construct()
{
}
/**
* @param string $data
* @param array<string, mixed> $writerOptions
* @return string
*/
public function qrCodeDataUriFunction(string $data, array $writerOptions = []): string
{
return Builder::create()
->writer(new PngWriter())
->writerOptions($writerOptions)
->data($data)
// if this causes errors at some point and needs to be configurable, keep this default!
->errorCorrectionLevel(new ErrorCorrectionLevelMedium())
->build()
->getDataUri();
}
}

View File

@@ -11,6 +11,7 @@ namespace App\Twig;
use App\Twig\Runtime\EncoreExtension; use App\Twig\Runtime\EncoreExtension;
use App\Twig\Runtime\MarkdownExtension; use App\Twig\Runtime\MarkdownExtension;
use App\Twig\Runtime\QrCodeExtension;
use App\Twig\Runtime\ThemeExtension; use App\Twig\Runtime\ThemeExtension;
use App\Twig\Runtime\TimesheetExtension; use App\Twig\Runtime\TimesheetExtension;
use App\Twig\Runtime\WidgetExtension; use App\Twig\Runtime\WidgetExtension;
@@ -35,6 +36,7 @@ final class RuntimeExtensions extends AbstractExtension
new TwigFunction('encore_entry_css_source', [EncoreExtension::class, 'getEncoreEntryCssSource']), new TwigFunction('encore_entry_css_source', [EncoreExtension::class, 'getEncoreEntryCssSource']),
new TwigFunction('render_widget', [WidgetExtension::class, 'renderWidget'], ['is_safe' => ['html'], 'needs_environment' => true]), new TwigFunction('render_widget', [WidgetExtension::class, 'renderWidget'], ['is_safe' => ['html'], 'needs_environment' => true]),
new TwigFunction('icon', [RuntimeExtension::class, 'createIcon'], ['is_safe' => ['html']]), new TwigFunction('icon', [RuntimeExtension::class, 'createIcon'], ['is_safe' => ['html']]),
new TwigFunction('qr_code_data_uri', [QrCodeExtension::class, 'qrCodeDataUriFunction']),
]; ];
} }

View File

@@ -27,19 +27,21 @@
{% block datatable_before %}{% endblock %} {% block datatable_before %}{% endblock %}
{% set sortedColumns = dataTable.sortedColumnNames %} {% set sortedColumns = dataTable.sortedColumnNames %}
{% for entry in dataTable %} {% block datatable_outer %}
{% block datatable_row %} {% for entry in dataTable %}
<tr{% block datatable_row_attr %}{% endblock %}> {% block datatable_row %}
{% for column, data in sortedColumns %} <tr{% block datatable_row_attr %}{% endblock %}>
{% block datatable_column %} {% for column, data in sortedColumns %}
<td class="{{ tables.class(dataTable, column) }}"{% block datatable_column_attr %}{% endblock %}> {% block datatable_column %}
{% block datatable_column_value %}{% endblock %} <td class="{{ tables.class(dataTable, column) }}"{% block datatable_column_attr %}{% endblock %}>
</td> {% block datatable_column_value %}{% endblock %}
{% endblock %} </td>
{% endfor %} {% endblock %}
</tr> {% endfor %}
{% endblock %} </tr>
{% endfor %} {% endblock %}
{% endfor %}
{% endblock %}
{% block datatable_after %}{% endblock %} {% block datatable_after %}{% endblock %}

View File

@@ -62,7 +62,7 @@
<div class="col-sm-7"></div> <div class="col-sm-7"></div>
</div> </div>
<div class="row invoice-items"> <div class="row invoice-items mt-2 mb-3">
<div class="col-xs-12 table-responsive"> <div class="col-xs-12 table-responsive">
<table class="table"> <table class="table">
<thead> <thead>

View File

@@ -15,9 +15,9 @@
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<table class="table no-border table-sm"> <table class="table no-border table-sm">
<tr> <tr>
<th>{{ 'invoice.from'|trans }}</th> <th class="ps-0">{{ 'invoice.from'|trans }}</th>
<td contenteditable="true"> <td contenteditable="true">
{% if model.query.user is not empty %} {% if model.query.user is not empty %}
{{ widgets.username(model.query.user) }} {{ widgets.username(model.query.user) }}
@@ -27,7 +27,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ 'date'|trans }}</th> <th class="ps-0">{{ 'date'|trans }}</th>
<td contenteditable="true"> <td contenteditable="true">
{% if model.query.begin|date('m') != model.query.end|date('m') or model.query.begin|date('Y') != model.query.end|date('Y') %} {% 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 }} {{ model.query.begin|date_short }} - {{ model.query.end|date_short }}
@@ -37,7 +37,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ 'customer'|trans }}</th> <th class="ps-0">{{ 'customer'|trans }}</th>
<td contenteditable="true"> <td contenteditable="true">
{% if model.customer.number is not empty %}[{{ model.customer.number }}]{% endif %} {% 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 %} {{ model.customer.name }}{% if model.customer.contact is not empty %} / {{ model.customer.contact }}{% endif %}
@@ -45,7 +45,7 @@
</tr> </tr>
{% if project is not null %} {% if project is not null %}
<tr> <tr>
<th>{{ 'project'|trans }}</th> <th class="ps-0">{{ 'project'|trans }}</th>
<td contenteditable="true"> <td contenteditable="true">
{{ project.name }} {{ project.name }}
{% if project.orderNumber is not empty %} {% if project.orderNumber is not empty %}
@@ -56,7 +56,7 @@
{% endif %} {% endif %}
{% if activity is not null %} {% if activity is not null %}
<tr> <tr>
<th>{{ 'activity'|trans }}</th> <th class="ps-0">{{ 'activity'|trans }}</th>
<td contenteditable="true"> <td contenteditable="true">
{{ activity.name }} {{ activity.name }}
</td> </td>
@@ -66,7 +66,7 @@
</div> </div>
</div> </div>
<div class="row invoice-items"> <div class="row invoice-items mt-2 mb-3">
<div class="col-xs-12 table-responsive"> <div class="col-xs-12 table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@@ -122,16 +122,8 @@
{% endif %} {% endif %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <p class="bt-1 pb-4 pt-1">{{ 'invoice.signature_user'|trans }}</p>
<tbody> <p class="bt-1 pb-4 pt-1">{{ 'invoice.signature_customer'|trans }}</p>
<tr>
<th style="padding-bottom: 60px">{{ 'invoice.signature_user'|trans }}</th>
</tr>
<tr>
<th>{{ 'invoice.signature_customer'|trans }}</th>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,44 +2,68 @@
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% set checkOverlappingDesc = false %} {% block datatable_outer %}
{% set checkOverlappingAsc = false %} {% set checkOverlappingDesc = false %}
{% set query = dataTable.getQuery() %} {% set checkOverlappingAsc = false %}
{% if query.orderBy == 'begin' or query.orderBy == 'end' %} {% set query = dataTable.getQuery() %}
{% set checkOverlappingDesc = (query.order == 'DESC') %} {% if query.orderBy == 'begin' or query.orderBy == 'end' %}
{% set checkOverlappingAsc = not checkOverlappingDesc %} {% set checkOverlappingDesc = (query.order == 'DESC') %}
{% endif %} {% set checkOverlappingAsc = not checkOverlappingDesc %}
{% endif %}
{% set day = null %} {% set day = null %}
{% set dayDuration = 0 %} {% set dayDuration = 0 %}
{% set dayRate = {} %} {% set dayRate = {} %}
{% set dayHourlyRate = 0 %} {% set dayHourlyRate = 0 %}
{% set lastEntry = null %} {% set lastEntry = null %}
{% for entry in dataTable %}
{%- if day is same as(null) -%}
{% set day = entry.begin|date_short %}
{% endif %}
{%- if showSummary and day is not same as(entry.begin|date_short) -%}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, sortedColumns, dataTable) }}
{% set day = entry.begin|date_short %}
{% set dayDuration = 0 %}
{% set dayRate = {} %}
{% set dayHourlyRate = 0 %}
{%- endif -%}
{%- set customerCurrency = entry.project.customer.currency -%}
{%- set entryHourlyRate = entry.hourlyRate|money(customerCurrency) -%}
{% block datatable_row %}
<tr{{ block('datatable_row_attr') }}>
{% for column, data in sortedColumns %}
{{ block('datatable_column') }}
{% endfor %}
</tr>
{% endblock %}
{%- if entry.end -%}
{% if dayRate[customerCurrency] is not defined %}
{% set dayRate = dayRate|merge({(customerCurrency): 0}) %}
{% endif %}
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %}
{%- endif -%}
{% if dayHourlyRate is not null %}
{% if dayHourlyRate == 0 %}
{% set dayHourlyRate = entryHourlyRate %}
{% elseif dayHourlyRate != entryHourlyRate %}
{% set dayHourlyRate = null %}
{% endif %}
{% endif %}
{%- set dayDuration = dayDuration + entry.duration -%}
{% set lastEntry = entry %}
{% endfor %}
{% if showSummary %}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, sortedColumns, dataTable) }}
{% endif %}
{% endblock %}
{% block status %} {% block status %}
{% from "macros/status.html.twig" import status_duration %} {% from "macros/status.html.twig" import status_duration %}
{{ status_duration(stats.duration|duration) }} {{ status_duration(stats.duration|duration) }}
{% endblock %} {% endblock %}
{% block datatable_after %} {% block datatable_row_attr %}
{% if showSummary %}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }}
{% endif %}
{% endblock %}
{% block datatable_row %}
{%- set customerCurrency = entry.project.customer.currency -%}
{%- set entryHourlyRate = entry.hourlyRate|money(customerCurrency) -%}
{%- if day is same as(null) -%}
{% set day = entry.begin|date_short %}
{% endif %}
{%- if showSummary and day is not same as(entry.begin|date_short) -%}
{{ _self.summary(day, dayDuration, dayHourlyRate, dayRate, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) }}
{% set day = entry.begin|date_short %}
{% set dayDuration = 0 %}
{% set dayRate = {} %}
{% set dayHourlyRate = 0 %}
{%- endif -%}
{% set class = '' %} {% set class = '' %}
{% if checkOverlappingDesc or checkOverlappingAsc %} {% if checkOverlappingDesc or checkOverlappingAsc %}
{% if lastEntry is not null and entry.end is not null and entry.user is same as (lastEntry.user) %} {% if lastEntry is not null and entry.end is not null and entry.user is same as (lastEntry.user) %}
@@ -53,144 +77,94 @@
{% if not entry.end %} {% if not entry.end %}
{% set class = class ~ ' recording' %} {% set class = class ~ ' recording' %}
{% endif %} {% endif %}
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit{{ class }}" data-href="{{ path(editRoute, {'id': entry.id}) }}"{% endif %}> {% if is_granted('edit', entry) %} class="modal-ajax-form open-edit{{ class }}" data-href="{{ path(editRoute, {'id': entry.id}) }}"{% endif %}
<td class="text-nowrap"> {% endblock %}
{% block datatable_column %}
<td class="{{ tables.class(dataTable, column) }}{% if column == 'description' %} timesheet-description{% endif %}">
{% if column == 'id' %}
{% if is_granted('edit', entry) or is_granted('delete', entry) %} {% if is_granted('edit', entry) or is_granted('delete', entry) %}
{{ tables.datatable_multiupdate_row(entry.id) }} {{ tables.datatable_multiupdate_row(entry.id) }}
{% endif %} {% endif %}
</td> {% elseif column == 'date' %}
<td class="text-nowrap {{ tables.class(dataTable, 'date') }}">{{ entry.begin|date_short }}</td> {{ entry.begin|date_short }}
{% elseif column == 'starttime' %}
{% if showStartEndTime %} {{ entry.begin|time }}
<td class="text-nowrap {{ tables.class(dataTable, 'starttime') }}">{{ entry.begin|time }}</td> {% elseif column == 'endtime' %}
<td class="text-nowrap {{ tables.class(dataTable, 'endtime') }}">
{% if entry.end %}
{{ entry.end|time }}
{% else %}
&dash;
{% endif %}
</td>
{% endif %}
{% if entry.end %} {% if entry.end %}
<td class="text-nowrap {{ tables.class(dataTable, 'duration') }}">{{ entry.duration|duration }}</td> {{ entry.end|time }}
{% else %} {% else %}
<td class="text-nowrap {{ tables.class(dataTable, 'duration') }}"> &dash;
<i data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</i>
</td>
{% endif %} {% endif %}
{% elseif column == 'duration' %}
{% if canSeeRate %} {% if entry.end %}
<td class="text-nowrap {{ tables.class(dataTable, 'hourlyRate') }}"> {{ entry.duration|duration }}
{{ entryHourlyRate }} {% else %}
</td> <i data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</i>
<td class="text-nowrap {{ tables.class(dataTable, 'rate') }}">
{% if not entry.end or not is_granted('view_rate', entry) %}
&dash;
{% else %}
{{ entry.rate|money(customerCurrency) }}
{% endif %}
</td>
{% endif %} {% endif %}
{% elseif column == 'hourlyRate' %}
<td class="{{ tables.class(dataTable, 'customer') }}"> {{ entryHourlyRate }}
{{ widgets.label_customer(entry.project.customer) }} {% elseif column == 'rate' %}
</td> {% if not entry.end or not is_granted('view_rate', entry) %}
<td class="{{ tables.class(dataTable, 'project') }}"> &dash;
{{ widgets.label_project(entry.project) }} {% else %}
</td> {{ entry.rate|money(customerCurrency) }}
<td class="{{ tables.class(dataTable, 'activity') }}">
{% if entry.activity is not null %}
{{ widgets.label_activity(entry.activity) }}
{% endif %}
</td>
<td class="{{ tables.class(dataTable, 'description') }} timesheet-description">
{% if allowMarkdown %}
{{ entry.description|desc2html }}
{% else %}
{{ entry.description|nl2br }}
{% endif %}
</td>
<td class="{{ tables.class(dataTable, 'tags') }}">{{ widgets.tag_list(entry.tags) }}</td>
{% for field in metaColumns %}
<td class="{{ tables.class(dataTable, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }}
</td>
{% endfor %}
{% if canSeeUsername %}
<td class="{{ tables.class(dataTable, 'username') }}">
{{ widgets.label_user(entry.user) }}
</td>
{% endif %} {% endif %}
{% elseif column == 'customer' %}
<td class="{{ tables.class(dataTable, 'billable') }}"> {{ widgets.label_customer(entry.project.customer) }}
{{ widgets.label_boolean(entry.billable) }} {% elseif column == 'project' %}
</td> {{ widgets.label_project(entry.project) }}
<td class="{{ tables.class(dataTable, 'exported') }}"> {% elseif column == 'activity' %}
{{ widgets.label_boolean(entry.exported) }} {% if entry.activity is not null %}
</td> {{ widgets.label_activity(entry.activity) }}
<td class="{{ tables.class(dataTable, 'actions') }}">
{% set event = actions(app.user, action_single, 'index', {'timesheet': entry}) %}
{{ widgets.table_actions(event.actions) }}
</td>
</tr>
{%- if entry.end -%}
{% if dayRate[customerCurrency] is not defined %}
{% set dayRate = dayRate|merge({(customerCurrency): 0}) %}
{% endif %} {% endif %}
{% set dayRate = dayRate|merge({(customerCurrency): dayRate[customerCurrency] + entry.rate}) %} {% elseif column == 'description' %}
{%- endif -%} {% if allowMarkdown %}
{% if dayHourlyRate is not null %} {{ entry.description|desc2html }}
{% if dayHourlyRate == 0 %} {% else %}
{% set dayHourlyRate = entryHourlyRate %} {{ entry.description|nl2br }}
{% elseif dayHourlyRate != entryHourlyRate %}
{% set dayHourlyRate = null %}
{% endif %} {% endif %}
{% elseif column == 'tags' %}
{{ widgets.tag_list(entry.tags) }}
{% elseif column == 'billable' %}
{{ widgets.label_boolean(entry.billable) }}
{% elseif column == 'exported' %}
{{ widgets.label_boolean(entry.exported) }}
{% elseif column == 'username' %}
{{ widgets.label_user(entry.user) }}
{% elseif column == 'actions' %}
{% set event = actions(app.user, action_single, 'index', {'timesheet': entry}) %}
{{ widgets.table_actions(event.actions) }}
{% elseif column starts with 'mf_' %}
{{ widgets.meta_field_value(entry, data) }}
{% endif %} {% endif %}
{%- set dayDuration = dayDuration + entry.duration -%} </td>
{% set lastEntry = entry %}
{% endblock %} {% endblock %}
{% macro summary(day, duration, dayHourlyRate, dayRates, columns, canSeeRate, canSeeUsername, showStartEndTime, tableName, metaColumns) %} {% macro summary(day, duration, dayHourlyRate, dayRates, sortedColumns, dataTable) %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
<tr class="summary info"> <tr class="summary info">
<td></td> {% for column, data in sortedColumns %}
<td class="text-nowrap">{{ day }}</td> <td class="{{ tables.class(dataTable, column) }}">
{% if showStartEndTime %} {% if column == 'date' %}
<td class="{{ tables.class(dataTable, 'starttime') }}"></td> {{ day }}
<td class="{{ tables.class(dataTable, 'endtime') }}"></td> {% elseif column == 'duration' %}
{% endif %} {{ duration|duration }}
<td class="text-nowrap {{ tables.class(dataTable, 'duration') }}">{{ duration|duration }}</td> {% elseif column == 'hourlyRate' %}
{% if canSeeRate %} {% if dayHourlyRate is not null and dayHourlyRate != 0 %}
<td class="text-nowrap {{ tables.class(dataTable, 'hourlyRate') }}"> {{ dayHourlyRate }}
{% if dayHourlyRate is not null and dayHourlyRate != 0 %} {% endif %}
{{ dayHourlyRate }} {% elseif column == 'rate' %}
{% for currency, rate in dayRates %}
{{ rate|money(currency) }}
{% if not loop.last %}
<br>
{% endif %} {% endif %}
</td> {% endfor %}
<td class="text-nowrap {{ tables.class(dataTable, 'rate') }}"> {% else %}
{% for currency, rate in dayRates %}
{{ rate|money(currency) }}
{% if not loop.last %}
<br>
{% endif %}
{% endfor %}
</td>
{% endif %} {% endif %}
<td class="{{ tables.class(dataTable, 'customer') }}"></td> </td>
<td class="{{ tables.class(dataTable, 'project') }}"></td> {% endfor %}
<td class="{{ tables.class(dataTable, 'activity') }}"></td>
<td class="{{ tables.class(dataTable, 'description') }}"></td>
<td class="{{ tables.class(dataTable, 'tags') }}"></td>
{% for field in metaColumns %}
<td class="{{ tables.class(dataTable, 'mf_' ~ field.name) }}"></td>
{% endfor %}
{% if canSeeUsername %}
<td class="{{ tables.class(dataTable, 'username') }}"></td>
{% endif %}
<td class="{{ tables.class(dataTable, 'billable') }}"></td>
<td class="{{ tables.class(dataTable, 'exported') }}"></td>
<td class="actions"></td>
</tr> </tr>
{% endmacro %} {% endmacro %}

View File

@@ -25,6 +25,14 @@
document.body.classList.add('theme-' + skinChooser.value); document.body.classList.add('theme-' + skinChooser.value);
}); });
} }
const localeChooser = document.getElementById('form_language');
if (localeChooser !== null) {
localeChooser.addEventListener('change', (ev) => {
document.getElementById('form_reload').value = '1';
ev.target.form.submit();
});
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -99,7 +99,7 @@ class CreateUserCommandTest extends KernelTestCase
$commandTester = $this->createUser('MyTestUser2', 'user@example.com', 'ROLE_USER', 'foobar'); $commandTester = $this->createUser('MyTestUser2', 'user@example.com', 'ROLE_USER', 'foobar');
$output = $commandTester->getDisplay(); $output = $commandTester->getDisplay();
$this->assertStringContainsString('[ERROR] email: The email is already used.', $output); $this->assertStringContainsString('[ERROR] email: This e-mail address is already in use.', $output);
} }
public function testUserEmail(): void public function testUserEmail(): void

View File

@@ -1,48 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\KimaiImporterCommand;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @covers \App\Command\KimaiImporterCommand
* @group integration
*/
class KimaiImporterCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected function setUp(): void
{
parent::setUp();
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$encoder = $this->createMock(UserPasswordHasherInterface::class);
$registry = $this->createMock(ManagerRegistry::class);
$validator = $this->createMock(ValidatorInterface::class);
$this->application->add(new KimaiImporterCommand($encoder, $registry, $validator));
}
public function testCommandName()
{
$command = $this->application->find('kimai:import-v1');
self::assertInstanceOf(KimaiImporterCommand::class, $command);
}
}

View File

@@ -116,7 +116,7 @@ class SelfRegistrationControllerTest extends ControllerBaseTest
$content = $client->getResponse()->getContent(); $content = $client->getResponse()->getContent();
$this->assertStringContainsString('<title>Kimai Time Tracking</title>', $content); $this->assertStringContainsString('<title>Kimai Time Tracking</title>', $content);
$this->assertStringContainsString('An email has been sent to register@example.com. It contains an activation link you must click to activate your account.', $content); $this->assertStringContainsString('An e-mail has been sent to register@example.com. It contains a link you must click to activate your account.', $content);
$this->assertStringContainsString('<a href="/en/login">', $content); $this->assertStringContainsString('<a href="/en/login">', $content);
} }

View File

@@ -5,8 +5,7 @@
"type": "kimai-plugin", "type": "kimai-plugin",
"version": "1.0", "version": "1.0",
"require": { "require": {
"kimai/kimai2-composer": "*", "kimai/kimai": "*"
"kevinpapst/kimai2": "*"
}, },
"keywords": [ "keywords": [
"kimai", "kimai",

View File

@@ -49,6 +49,7 @@ class RuntimeExtensionsTest extends TestCase
'encore_entry_css_source', 'encore_entry_css_source',
'render_widget', 'render_widget',
'icon', 'icon',
'qr_code_data_uri',
]; ];
$i = 0; $i = 0;

View File

@@ -2302,11 +2302,6 @@ parameters:
count: 1 count: 1
path: Command/InvoiceCreateCommandTest.php path: Command/InvoiceCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\KimaiImporterCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/KimaiImporterCommandTest.php
- -
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has no return type specified\\.$#" message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has no return type specified\\.$#"
count: 1 count: 1