Compare commits
8 Commits
2.0.33
...
customer-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1cc5f2587 | ||
|
|
6b672a23b4 | ||
|
|
900254b83a | ||
|
|
114617a052 | ||
|
|
38e37f1c2e | ||
|
|
7a5b12762a | ||
|
|
de419350b2 | ||
|
|
a6b237936e |
@@ -16,10 +16,10 @@
|
||||
# For MySQL that would be "serverVersion=5.7" as in:
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
|
||||
#
|
||||
# For MariaDB it would be "serverVersion=mariadb-10.5.8":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=mariadb-10.5.8
|
||||
# For MariaDB it would be "serverVersion=10.5.8-MariaDB":
|
||||
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
#
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
|
||||
|
||||
#================================================================================
|
||||
# The full documentation can be found at https://www.kimai.org/documentation/emails.html
|
||||
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1,45 +1,34 @@
|
||||
# some hosters require a htaccess to change the PHP version
|
||||
.htaccess
|
||||
.env-*
|
||||
.idea/
|
||||
.DS_Store
|
||||
rector.php
|
||||
phpstan.sh
|
||||
/public/.htaccess
|
||||
/.env-*
|
||||
/.idea/
|
||||
/.DS_Store
|
||||
/phpstan.sh
|
||||
|
||||
# custom apache rules e.g. to deactivate ioncube loader
|
||||
public/.user.ini
|
||||
/public/.user.ini
|
||||
|
||||
# for symfony local webserver
|
||||
php.ini
|
||||
.php-version
|
||||
/php.ini
|
||||
/.php-version
|
||||
|
||||
# YARN 2
|
||||
.yarnrc.yml
|
||||
.yarn
|
||||
/.yarnrc.yml
|
||||
/.yarn
|
||||
|
||||
# for keeping empty directories
|
||||
!.gitkeep
|
||||
|
||||
/bin/*
|
||||
!/bin/console
|
||||
*local.yaml
|
||||
/config/packages/local.yaml
|
||||
/config/bundles-local.php
|
||||
|
||||
/templates/invoice/renderer/.~lock*
|
||||
/translations/branding.*.xlf
|
||||
|
||||
/var/data/*
|
||||
/var/coverage/
|
||||
/var/cache/*
|
||||
|
||||
/var/invoices/*
|
||||
/var/invoices*
|
||||
/var/export/*
|
||||
/var/log/*
|
||||
/var/sessions/*
|
||||
/var/packages/*
|
||||
/var/plugins/*
|
||||
/var/plugins_old/
|
||||
*.disabled
|
||||
/var/plugins/*/*.disabled
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
|
||||
@@ -33,7 +33,7 @@ $fixer
|
||||
'no_break_comment' => true,
|
||||
'no_closing_tag' => true,
|
||||
'no_spaces_after_function_name' => true,
|
||||
'no_spaces_inside_parenthesis' => true,
|
||||
'spaces_inside_parentheses' => ['space' => 'none'],
|
||||
'no_trailing_whitespace' => true,
|
||||
'no_trailing_whitespace_in_comment' => true,
|
||||
'single_blank_line_at_eof' => true,
|
||||
@@ -54,7 +54,7 @@ $fixer
|
||||
'class_attributes_separation' => ['elements' => ['method' => 'one']],
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'declare_equal_normalize' => true,
|
||||
'function_typehint_space' => true,
|
||||
'type_declaration_spaces' => ['elements' => ['function', 'property']],
|
||||
'include' => true,
|
||||
'lowercase_cast' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
@@ -123,7 +123,7 @@ $fixer
|
||||
'return_type_declaration' => true,
|
||||
'semicolon_after_instruction' => true,
|
||||
'short_scalar_cast' => true,
|
||||
'single_blank_line_before_namespace' => true,
|
||||
'blank_lines_before_namespace' => ['min_line_breaks' => 2, 'max_line_breaks' => 2],
|
||||
'single_line_comment_style' => [
|
||||
'comment_types' => ['hash'],
|
||||
],
|
||||
|
||||
10
README.md
10
README.md
@@ -23,12 +23,12 @@ and so much more.
|
||||
|
||||
### Versions
|
||||
|
||||
There are two versions of Kimai existing:
|
||||
There are two [versions](https://www.kimai.org/documentation/versions.html) of Kimai existing:
|
||||
|
||||
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1 only)
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — the "old" version, in maintenance mode since Jan. 2023 (PHP 7.4, PHP 8.1)
|
||||
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1+)
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — EOL since mid of 2023 (PHP 7.4)
|
||||
|
||||
If you start fresh, do **not** use Version 1, it won't receive any more updates.
|
||||
Do **NOT** use Version 1, it won't get any more updates!
|
||||
|
||||
### Links
|
||||
|
||||
@@ -74,8 +74,6 @@ It is open for changes and input from the community, your [ideas and questions](
|
||||
Release versions will be created on a regular basis, every couple of weeks latest.
|
||||
Every code change, whether it's a new feature or a bugfix, will be done on the `main` branch.
|
||||
|
||||
Until 2.0 is widely adopted, the [1.x branch](https://github.com/kimai/kimai/tree/1.x) will receive (critical) bug and security fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
You want to contribute to this repository? This is so great!
|
||||
|
||||
@@ -11,7 +11,7 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings ([.env](https://github.com/kimai/kimai/issues/4246) or in your [Docker](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7` (examples in `.env`).
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
|
||||
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
@@ -21,9 +21,9 @@ now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql
|
||||
|
||||
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
|
||||
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
|
||||
- dashboard default config
|
||||
- removed: theme.branding.translation
|
||||
- removed: kimai.plugin_dir
|
||||
- removed: configuring the `dashboard` is not supported any longer
|
||||
- removed: custom translation files via `theme.branding.translation`
|
||||
- removed: changing the plugin directory via `kimai.plugin_dir`
|
||||
|
||||
### Developer
|
||||
|
||||
|
||||
@@ -440,8 +440,14 @@ export default class KimaiCalendar {
|
||||
calendarSource = {...calendarSource, ...{
|
||||
id: 'kimai-' + source.id,
|
||||
events: (fetchInfo, successCallback, failureCallback) => {
|
||||
let url = source.url.replace('{from}', DATES.formatForAPI(fetchInfo.start));
|
||||
url = url.replace('{to}', DATES.formatForAPI(fetchInfo.end));
|
||||
const targetFrom = DATES.formatForAPI(fetchInfo.start);
|
||||
const targetTo = DATES.formatForAPI(fetchInfo.end);
|
||||
|
||||
let url = source.url;
|
||||
url = url.replace('{from}', targetFrom);
|
||||
url = url.replace('__FROM__', targetFrom);
|
||||
url = url.replace('{to}', targetTo);
|
||||
url = url.replace('__TO__', targetTo);
|
||||
|
||||
API.get(url, {}, result => {
|
||||
let apiEvents = [];
|
||||
@@ -463,8 +469,14 @@ export default class KimaiCalendar {
|
||||
id: 'json-' + source.id,
|
||||
editable: false,
|
||||
events: (fetchInfo, successCallback, failureCallback) => {
|
||||
let url = source.url.replace('{from}', DATES.formatForAPI(fetchInfo.start));
|
||||
url = url.replace('{to}', DATES.formatForAPI(fetchInfo.end));
|
||||
const targetFrom = DATES.formatForAPI(fetchInfo.start);
|
||||
const targetTo = DATES.formatForAPI(fetchInfo.end);
|
||||
|
||||
let url = source.url;
|
||||
url = url.replace('{from}', targetFrom);
|
||||
url = url.replace('__FROM__', targetFrom);
|
||||
url = url.replace('{to}', targetTo);
|
||||
url = url.replace('__TO__', targetTo);
|
||||
|
||||
API.get(url, {}, result => {
|
||||
let apiEvents = [];
|
||||
|
||||
@@ -37,10 +37,6 @@
|
||||
border-radius: var(--tblr-border-radius);
|
||||
}
|
||||
|
||||
fieldset:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* e.g. the work contract settings is user profile render rows with mb-3 and the last row doesn't need that */
|
||||
fieldset > .mb-3.row:last-child {
|
||||
margin-bottom: 0!important;
|
||||
|
||||
@@ -15,4 +15,20 @@ fieldset.form-fieldset legend {
|
||||
}
|
||||
.dropdown-divider {
|
||||
--tblr-dropdown-divider-margin-y: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fixes translucent scroll border, e.g. in timesheet modal duration dropdown
|
||||
https://github.com/tabler/tabler/issues/1606
|
||||
*/
|
||||
.dropdown-menu {
|
||||
background-clip: border-box;
|
||||
}
|
||||
|
||||
/*
|
||||
hide empty fieldset
|
||||
https://github.com/tabler/tabler/issues/1650
|
||||
*/
|
||||
fieldset:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@
|
||||
"doctrine/doctrine-bundle": "^2.7",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.8",
|
||||
"endroid/qr-code": "^4.4.9",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^0.22",
|
||||
"kevinpapst/tabler-bundle": "^1.0",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
@@ -44,8 +44,8 @@
|
||||
"pagerfanta/pagerfanta": "^3.0",
|
||||
"phpoffice/phpspreadsheet": "^1.16",
|
||||
"phpoffice/phpword": "^1.0",
|
||||
"psr/container": "^1.0||^2.0",
|
||||
"psr/log": "^2.0||^3.0",
|
||||
"psr/container": "^2.0",
|
||||
"psr/log": "^3.0",
|
||||
"scheb/2fa-backup-code": "^6.2",
|
||||
"scheb/2fa-bundle": "^6.2",
|
||||
"scheb/2fa-totp": "^6.2",
|
||||
@@ -68,7 +68,7 @@
|
||||
"symfony/translation": "^6.0",
|
||||
"symfony/twig-bundle": "^6.0",
|
||||
"symfony/validator": "^6.0",
|
||||
"symfony/webpack-encore-bundle": "^1.16",
|
||||
"symfony/webpack-encore-bundle": "^2.0",
|
||||
"symfony/yaml": "^6.0",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
@@ -129,6 +129,7 @@
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-mbstring": "*",
|
||||
"symfony/polyfill-intl": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
@@ -179,7 +180,13 @@
|
||||
"tests-unit": "vendor/bin/phpunit --exclude-group integration tests/",
|
||||
"tests-integration": "vendor/bin/phpunit --group integration tests/",
|
||||
"phpstan": [
|
||||
"vendor/bin/phpstan analyse -c phpstan.neon",
|
||||
"@phpstan-src",
|
||||
"@phpstan-tests"
|
||||
],
|
||||
"phpstan-src": [
|
||||
"vendor/bin/phpstan analyse -c phpstan.neon"
|
||||
],
|
||||
"phpstan-tests": [
|
||||
"vendor/bin/phpstan analyse -c tests/phpstan.neon"
|
||||
],
|
||||
"codestyle": "vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=none",
|
||||
@@ -195,7 +202,7 @@
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
"require": "6.2.*"
|
||||
"require": "6.3.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1943
composer.lock
generated
1943
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ doctrine:
|
||||
default_table_options:
|
||||
charset: utf8mb4
|
||||
collate: utf8mb4_unicode_ci
|
||||
schema_manager_factory: doctrine.dbal.default_schema_manager_factory
|
||||
|
||||
types:
|
||||
datetime: App\Doctrine\UTCDateTimeType
|
||||
@@ -26,6 +27,7 @@ doctrine:
|
||||
default_entity_manager: default
|
||||
entity_managers:
|
||||
default:
|
||||
report_fields_where_declared: true
|
||||
connection: default
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
auto_mapping: true
|
||||
|
||||
@@ -17,8 +17,6 @@ framework:
|
||||
cookie_httponly: true
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
#esi: ~
|
||||
#fragments: ~
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
|
||||
@@ -151,11 +151,13 @@ kimai:
|
||||
# --------------------------------------------------------------------------------
|
||||
# INVOICES
|
||||
# --------------------------------------------------------------------------------
|
||||
# invoice:
|
||||
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
||||
# documents:
|
||||
# - 'var/invoices/'
|
||||
# - 'templates/invoice/renderer/'
|
||||
# invoice:
|
||||
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
||||
# documents:
|
||||
# - 'var/invoices/'
|
||||
# - 'templates/invoice/renderer/'
|
||||
# # allow to upload twig templates as invoice documents
|
||||
# upload_twig: true
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,12 @@ security:
|
||||
max_attempts: 5
|
||||
interval: '5 minutes'
|
||||
|
||||
login_link:
|
||||
check_route: link_login_check
|
||||
signature_properties: ['id']
|
||||
lifetime: 300
|
||||
max_uses: 1
|
||||
|
||||
access_decision_manager:
|
||||
strategy: unanimous
|
||||
allow_if_all_abstain: false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
controllers:
|
||||
resource: ../src/Controller/
|
||||
type: annotation
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
@@ -19,17 +19,17 @@ api.swagger:
|
||||
|
||||
api:
|
||||
resource: ../src/API/
|
||||
type: annotation
|
||||
type: attribute
|
||||
prefix: /api
|
||||
|
||||
auth:
|
||||
resource: ../src/Controller/Auth/
|
||||
type: annotation
|
||||
type: attribute
|
||||
prefix: /auth
|
||||
|
||||
security:
|
||||
resource: ../src/Controller/Security/
|
||||
type: annotation
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
@@ -38,7 +38,7 @@ security:
|
||||
|
||||
kernel:
|
||||
resource: ../src/Kernel.php
|
||||
type: annotation
|
||||
type: attribute
|
||||
|
||||
home:
|
||||
path: /
|
||||
|
||||
@@ -91,16 +91,10 @@ services:
|
||||
App\Doctrine\TimesheetSubscriber:
|
||||
class: App\Doctrine\TimesheetSubscriber
|
||||
arguments: [!tagged timesheet.calculator]
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber, priority: 50 }
|
||||
|
||||
# updates timestampable columns (higher priority, so the TimesheetSubscriber will be executed later)
|
||||
Gedmo\Timestampable\TimestampableListener:
|
||||
class: Gedmo\Timestampable\TimestampableListener
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber, priority: 60 }
|
||||
calls:
|
||||
- [ setAnnotationReader, [ "@annotation_reader" ] ]
|
||||
App\Doctrine\ModifiedSubscriber:
|
||||
class: App\Doctrine\ModifiedSubscriber
|
||||
|
||||
# ================================================================================
|
||||
# TIMESHEET RECORD CALCULATOR
|
||||
|
||||
686
phpstan.neon
686
phpstan.neon
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
||||
<env name="APP_ENV" value="test" force="true"/>
|
||||
<env name="APP_DEBUG" value="0" force="true"/>
|
||||
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?serverVersion=mariadb-10.7.3"/>
|
||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?serverVersion=10.5.8-MariaDB"/>
|
||||
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
||||
<env name="MAILER_URL" value="null://null"/>
|
||||
<env name="MAILER_FROM" value="kimai@example.com"/>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
"/build/app.9acfcfc8.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.405a508c.css"
|
||||
"/build/app.fc41168c.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -45,7 +45,7 @@
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/calendar.ea514f59.js"
|
||||
"/build/calendar.3f4f78fe.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.bb473428.css"
|
||||
@@ -64,7 +64,7 @@
|
||||
"integrity": {
|
||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||
"/build/app.9acfcfc8.js": "sha384-tzbF0uEmHghIo+D5IuKarLYkwB3AyrLMTEFqR5mqaXCP5VSOWx3t+ZfD81kd80T+",
|
||||
"/build/app.405a508c.css": "sha384-D8LqbrX2x0UHuxQasN8nli7f/tzVYlNeJleATqXW1o0Pjwycyo0t1jKRe1h1MeIV",
|
||||
"/build/app.fc41168c.css": "sha384-3Td9W7KogJoBiywzWOgpkPPNHEIT8/a/SkMZM5NgIRmuChj/3PeOKkOHVJQlhP2E",
|
||||
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ",
|
||||
@@ -72,7 +72,7 @@
|
||||
"/build/invoice-pdf.ce9aace0.js": "sha384-mtZkXrpsXFMX2YvmmTlCm91QgsTlZLzLJnDAFcTuBDtDZ7TNkqLYkfCeLzFLVfwb",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.f5becfac.js": "sha384-GSqETm8wULiVXyizvwRompfwu63r/C0Qd/AvrHDE4cqAKiIGCssb3QyBtGu1WN+W",
|
||||
"/build/calendar.ea514f59.js": "sha384-s162N0JRCI0twVtDROON8kABO+riWgiXM7O6TA175IOkAh94twcLqKoAfycd2BrA",
|
||||
"/build/calendar.3f4f78fe.js": "sha384-RD8enOf0Foe7IhSOGnv2DCes4QUCvLjlONH0HFq3RK9l/ozrFZakTX6SWwKOoEda",
|
||||
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
||||
"/build/dashboard.6774a712.js": "sha384-lBwkNqUPv+IBbznagiFakY2BOIueq/Bg89wHO2XeM9YAZ51rPkrvdMd+XmFupHsM",
|
||||
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"build/app.css": "/build/app.405a508c.css",
|
||||
"build/app.css": "/build/app.fc41168c.css",
|
||||
"build/app.js": "/build/app.9acfcfc8.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
|
||||
@@ -9,7 +9,7 @@
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.ce9aace0.js",
|
||||
"build/chart.js": "/build/chart.f5becfac.js",
|
||||
"build/calendar.css": "/build/calendar.bb473428.css",
|
||||
"build/calendar.js": "/build/calendar.ea514f59.js",
|
||||
"build/calendar.js": "/build/calendar.3f4f78fe.js",
|
||||
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
||||
"build/dashboard.js": "/build/dashboard.6774a712.js",
|
||||
"build/runtime.js": "/build/runtime.f0079159.js"
|
||||
|
||||
@@ -21,10 +21,10 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/actions')]
|
||||
|
||||
@@ -26,8 +26,8 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
|
||||
@@ -30,13 +30,13 @@ abstract class BaseApiController extends AbstractController
|
||||
|
||||
/**
|
||||
* @template TFormType of FormTypeInterface<TData>
|
||||
* @template TData of mixed
|
||||
* @template TData of BaseQuery
|
||||
* @param class-string<TFormType> $type
|
||||
* @param TData|null $data
|
||||
* @param TData $data
|
||||
* @param array<mixed> $options
|
||||
* @return FormInterface<TData|null>
|
||||
* @return FormInterface<BaseQuery>
|
||||
*/
|
||||
protected function createSearchForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
||||
protected function createSearchForm(string $type, BaseQuery $data, array $options = []): FormInterface
|
||||
{
|
||||
return $this->container
|
||||
->get('form.factory')
|
||||
|
||||
@@ -26,8 +26,8 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
@@ -27,8 +27,8 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
@@ -33,6 +33,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -40,7 +41,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[Route(path: '/timesheets')]
|
||||
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
|
||||
@@ -93,7 +93,7 @@ final class TimesheetController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'exported', requirements: '0|1', strict: true, nullable: true, description: 'Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)')]
|
||||
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
|
||||
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
|
||||
#[Rest\QueryParam(name: 'full', strict: true, nullable: true, description: 'Allows to fetch fully serialized objects including subresources. Allowed values: true (default: false)')]
|
||||
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5). Available since Kimai 1.10 and works only for records that were created/updated since then.')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||
@@ -257,7 +257,8 @@ final class TimesheetController extends BaseApiController
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $data);
|
||||
|
||||
if (null !== $paramFetcher->get('full')) {
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
|
||||
} else {
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
@@ -23,7 +23,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
@@ -39,6 +39,7 @@ final class UserController extends BaseApiController
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||
public const GROUPS_FORM = ['Default', 'Entity', 'User', 'User_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'User'];
|
||||
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'User', 'User_Entity'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
@@ -60,6 +61,7 @@ final class UserController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|username|alias|email', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, username, alias, email (default: username)')]
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
@@ -88,7 +90,13 @@ final class UserController extends BaseApiController
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getUsersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION_FULL);
|
||||
} else {
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
}
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use App\Event\ActivityUpdatePreEvent;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ use App\Event\CalendarSourceEvent;
|
||||
use App\Event\RecentActivityEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Utils\Color;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class CalendarService
|
||||
{
|
||||
|
||||
@@ -148,7 +148,7 @@ abstract class AbstractBundleInstallerCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function installAssets(SymfonyStyle $io, OutputInterface $output)
|
||||
protected function installAssets(SymfonyStyle $io, OutputInterface $output): void
|
||||
{
|
||||
$command = $this->getApplication()->find('assets:install');
|
||||
$cmdInput = new ArrayInput([]);
|
||||
@@ -160,12 +160,12 @@ abstract class AbstractBundleInstallerCommand extends Command
|
||||
$io->writeln('');
|
||||
}
|
||||
|
||||
protected function importMigrations(SymfonyStyle $io, OutputInterface $output)
|
||||
protected function importMigrations(SymfonyStyle $io, OutputInterface $output): void
|
||||
{
|
||||
$config = $this->getMigrationConfigFilename();
|
||||
|
||||
if (null === $config) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file_exists($config)) {
|
||||
|
||||
@@ -56,5 +56,5 @@ abstract class AbstractRoleCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role);
|
||||
abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,7 @@ final class DemoteUserCommand extends AbstractRoleCommand
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role)
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||
{
|
||||
$username = $user->getUserIdentifier();
|
||||
if ($super) {
|
||||
|
||||
@@ -46,10 +46,7 @@ final class ExportCreateCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Create exports')
|
||||
|
||||
@@ -22,6 +22,7 @@ use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\SearchTerm;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
@@ -32,7 +33,6 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[AsCommand(name: 'kimai:invoice:create')]
|
||||
final class InvoiceCreateCommand extends Command
|
||||
|
||||
@@ -33,7 +33,7 @@ final class PromoteUserCommand extends AbstractRoleCommand
|
||||
);
|
||||
}
|
||||
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role)
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||
{
|
||||
$username = $user->getUserIdentifier();
|
||||
if ($super) {
|
||||
|
||||
@@ -106,7 +106,7 @@ final class ReloadCommand extends Command
|
||||
]
|
||||
);
|
||||
|
||||
return (int) $cacheResult;
|
||||
return $cacheResult;
|
||||
}
|
||||
|
||||
$io->success(
|
||||
@@ -116,7 +116,7 @@ final class ReloadCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function rebuildCaches(string $environment, SymfonyStyle $io, InputInterface $input, OutputInterface $output)
|
||||
private function rebuildCaches(string $environment, SymfonyStyle $io, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io->text('Rebuilding your cache, please be patient ...');
|
||||
|
||||
|
||||
89
src/Command/UserLoginLinkCommand.php
Normal file
89
src/Command/UserLoginLinkCommand.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?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\Command;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
|
||||
|
||||
#[AsCommand(name: 'kimai:user:login-link', description: 'Create a URL that can be used to login as that user', hidden: true)]
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
final class UserLoginLinkCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private LoginLinkHandlerInterface $loginLink,
|
||||
private UserRepository $userRepository,
|
||||
private RequestStack $requestStack
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->addArgument('email', InputArgument::REQUIRED, 'The email of the user');
|
||||
$this->addOption('password-reset', null, InputOption::VALUE_NONE, 'Whether the user needs to reset the password afterwards');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$email = $input->getArgument('email');
|
||||
if ($email === null || $email === '') {
|
||||
$io->error('Need email to create login URL');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$user = $this->userRepository->findOneBy(['email' => $email]);
|
||||
|
||||
if ($user === null) {
|
||||
$io->error('Need username to create login URL');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (!$user->isEnabled()) {
|
||||
$io->error('User is not enabled');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (!$user->isInternalUser()) {
|
||||
$io->error('User does not use internal login');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$request = new Request();
|
||||
$request->setLocale($user->getLocale());
|
||||
$this->requestStack->push($request);
|
||||
|
||||
$loginLinkDetails = $this->loginLink->createLoginLink($user, $request);
|
||||
$loginLink = $loginLinkDetails->getUrl();
|
||||
|
||||
if ($input->getOption('password-reset') === true) {
|
||||
$user->setPasswordRequestedAt(new \DateTime());
|
||||
$user->setRequiresPasswordReset(true);
|
||||
$this->userRepository->saveUser($user);
|
||||
}
|
||||
|
||||
$output->writeln($loginLink);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,11 @@ final class SystemConfiguration
|
||||
|
||||
/**
|
||||
* Set an array item to a given value using "dot" notation.
|
||||
*
|
||||
* If no key is given to the method, the entire array will be replaced.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
private function set(string $key, $value): void
|
||||
public function set(string $key, mixed $value): void
|
||||
{
|
||||
if (\array_key_exists($key, $this->settings)) {
|
||||
if (\is_bool($this->settings[$key])) {
|
||||
@@ -125,31 +122,35 @@ final class SystemConfiguration
|
||||
|
||||
// ========== Array access methods ==========
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0.35
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return $this->has($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0.35
|
||||
*/
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->find($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
* @throws \BadMethodCallException
|
||||
* @deprecated since 2.0.35
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$this->set($offset, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @deprecated since 2.0.35
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
throw new \BadMethodCallException('SystemBundleConfiguration does not support offsetUnset()');
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.33';
|
||||
public const VERSION = '2.1.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20033;
|
||||
public const VERSION_ID = 20100;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
@@ -31,7 +31,7 @@ class Constants
|
||||
*/
|
||||
public const GITHUB = 'https://github.com/kimai/kimai/';
|
||||
/**
|
||||
* The Github repository name
|
||||
* The GitHub repository name
|
||||
*/
|
||||
public const GITHUB_REPO = 'kimai/kimai';
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,6 @@ use App\Timesheet\DateTimeFactory;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
@@ -76,13 +75,13 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
|
||||
/**
|
||||
* @template TFormType of FormTypeInterface<TData>
|
||||
* @template TData of mixed
|
||||
* @template TData of array|object
|
||||
* @param class-string<TFormType> $type
|
||||
* @param TData|null $data
|
||||
* @param TData $data
|
||||
* @param array<mixed> $options
|
||||
* @return FormInterface<TData|null>
|
||||
* @return FormInterface<TData>
|
||||
*/
|
||||
protected function createFormWithName(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
||||
protected function createFormWithName(string $name, string $type, mixed $data, array $options = []): FormInterface
|
||||
{
|
||||
return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -25,7 +25,7 @@ final class SamlController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/login', name: 'saml_login')]
|
||||
public function loginAction(Request $request)
|
||||
public function loginAction(Request $request): Response
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
@@ -50,11 +50,19 @@ final class SamlController extends AbstractController
|
||||
throw new \RuntimeException($error);
|
||||
}
|
||||
|
||||
$this->authFactory->create()->login($session->get('_security.main.target_path'));
|
||||
// this does set headers and exit as $stay is not set to true
|
||||
$url = $this->authFactory->create()->login($session->get('_security.main.target_path'));
|
||||
|
||||
if ($url === null) {
|
||||
throw new \RuntimeException('SAML login failed');
|
||||
}
|
||||
|
||||
// this line is not (yet) reached, as the previous call will exit
|
||||
return $this->redirect($url);
|
||||
}
|
||||
|
||||
#[Route(path: '/metadata', name: 'saml_metadata')]
|
||||
public function metadataAction()
|
||||
public function metadataAction(): Response
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
@@ -69,7 +77,7 @@ final class SamlController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/acs', name: 'saml_acs')]
|
||||
public function assertionConsumerServiceAction()
|
||||
public function assertionConsumerServiceAction(): Response
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
@@ -79,7 +87,7 @@ final class SamlController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'saml_logout')]
|
||||
public function logoutAction()
|
||||
public function logoutAction(): Response
|
||||
{
|
||||
if (!$this->samlConfiguration->isActivated()) {
|
||||
throw $this->createNotFoundException('SAML deactivated');
|
||||
|
||||
@@ -16,10 +16,10 @@ use App\Reporting\YearByUser\YearByUser;
|
||||
use App\Utils\PageSetup;
|
||||
use App\WorkingTime\Model\BoxConfiguration;
|
||||
use App\WorkingTime\WorkingTimeService;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Users can control their working time statistics
|
||||
@@ -68,6 +68,7 @@ final class ContractController extends AbstractController
|
||||
$page = new PageSetup('work_times');
|
||||
$page->setHelp('contract.html');
|
||||
$page->setActionName('contract');
|
||||
$page->setActionPayload(['profile' => $profile, 'year' => $yearDate]);
|
||||
$page->setPaginationForm($form);
|
||||
|
||||
// additional boxes by plugins
|
||||
|
||||
@@ -36,7 +36,7 @@ use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -16,7 +16,7 @@ use App\Repository\BookmarkRepository;
|
||||
use App\Utils\PageSetup;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetService;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Configuration\LocaleService;
|
||||
use App\Entity\User;
|
||||
use App\Event\ConfigureMainMenuEvent;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
@@ -38,6 +38,7 @@ use App\Repository\Query\InvoiceQuery;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Exception;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -48,7 +49,6 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
@@ -387,7 +387,7 @@ final class InvoiceController extends AbstractController
|
||||
|
||||
#[Route(path: '/export', name: 'invoice_export', methods: ['GET'])]
|
||||
#[IsGranted('view_invoice')]
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter): Response
|
||||
{
|
||||
$query = new InvoiceArchiveQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
@@ -468,36 +468,6 @@ final class InvoiceController extends AbstractController
|
||||
throw $this->createNotFoundException('Unknown document: ' . $document);
|
||||
}
|
||||
|
||||
#[Route(path: '/document_reload/{document}', name: 'admin_invoice_document_reload', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('upload_invoice_template')]
|
||||
public function reloadDocument(string $document, Environment $twig): Response
|
||||
{
|
||||
$event = new InvoiceDocumentsEvent($this->service->getDocuments(true));
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$reloaded = false;
|
||||
|
||||
foreach ($event->getInvoiceDocuments() as $doc) {
|
||||
if ($document === $doc->getId() && $doc->isTwig()) {
|
||||
$reloaded = true;
|
||||
try {
|
||||
$twig->enableAutoReload();
|
||||
$twig->load('@invoice/' . basename($doc->getFilename()));
|
||||
$twig->disableAutoReload();
|
||||
$this->flashSuccess('Reloaded template');
|
||||
} catch (Exception $ex) {
|
||||
$this->flashException($ex, 'Failed to reload template: ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$reloaded) {
|
||||
throw $this->createNotFoundException('Unknown document: ' . $document);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_document_upload');
|
||||
}
|
||||
|
||||
#[Route(path: '/document_upload', name: 'admin_invoice_document_upload', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('upload_invoice_template')]
|
||||
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration): Response
|
||||
|
||||
@@ -22,7 +22,7 @@ use App\Security\RolePermissionManager;
|
||||
use App\Security\RoleService;
|
||||
use App\User\PermissionService;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
@@ -46,7 +46,7 @@ final class PermissionController extends AbstractController
|
||||
|
||||
#[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('role_permissions')]
|
||||
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService)
|
||||
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService): Response
|
||||
{
|
||||
$all = $this->roleRepository->findAll();
|
||||
$existing = [];
|
||||
|
||||
@@ -32,8 +32,8 @@ use Endroid\QrCode\Encoding\Encoding;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -40,7 +40,7 @@ use App\Repository\TeamRepository;
|
||||
use App\Utils\Context;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\TimesheetService;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -33,7 +34,7 @@ final class QuickEntryController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/{begin}', name: 'quick_entry', methods: ['GET', 'POST'])]
|
||||
public function quickEntry(Request $request, ?string $begin = null)
|
||||
public function quickEntry(Request $request, ?string $begin = null): Response
|
||||
{
|
||||
$factory = $this->getDateTimeFactory();
|
||||
|
||||
|
||||
53
src/Controller/Reporting/CustomerViewController.php
Normal file
53
src/Controller/Reporting/CustomerViewController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\Controller\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Customer\CustomerStatisticService;
|
||||
use App\Reporting\CustomerView\CustomerViewForm;
|
||||
use App\Reporting\CustomerView\CustomerViewQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class CustomerViewController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/reporting/customer_view', name: 'report_customer_view', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:customer')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'customer')"))]
|
||||
public function __invoke(Request $request, CustomerStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new CustomerViewQuery($dateFactory->createDateTime(), $user);
|
||||
$form = $this->createFormForGetRequest(CustomerViewForm::class, $query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$customers = $service->findCustomersForView($query);
|
||||
$entries = $service->getCustomerView($user, $customers, $query->getToday());
|
||||
|
||||
$byCustomer = [];
|
||||
foreach ($entries as $entry) {
|
||||
$customer = $entry->getCustomer();
|
||||
$byCustomer[$customer->getId()] = $entry;
|
||||
}
|
||||
|
||||
return $this->render('reporting/customer_view.html.twig', [
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
'report_title' => 'report_customer_view',
|
||||
'tableName' => 'customer_view_reporting',
|
||||
'now' => $dateFactory->createDateTime(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use App\Reporting\ProjectDateRange\ProjectDateRangeForm;
|
||||
use App\Reporting\ProjectDateRange\ProjectDateRangeQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -24,7 +25,7 @@ final class ProjectDateRangeController extends AbstractController
|
||||
#[Route(path: '/reporting/project_daterange', name: 'report_project_daterange', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Reporting\ProjectDetails\ProjectDetailsQuery;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -24,7 +25,7 @@ final class ProjectDetailsController extends AbstractController
|
||||
#[Route(path: '/reporting/project_details', name: 'report_project_details', methods: ['GET'])]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('details', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Reporting\ProjectInactive\ProjectInactiveForm;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -23,7 +24,7 @@ final class ProjectInactiveController extends AbstractController
|
||||
#[Route(path: '/reporting/project_inactive', name: 'report_project_inactive', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Reporting\ProjectView\ProjectViewForm;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -23,7 +24,7 @@ final class ProjectViewController extends AbstractController
|
||||
#[Route(path: '/reporting/project_view', name: 'report_project_view', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('report:project')]
|
||||
#[IsGranted(new Expression("is_granted('budget_any', 'project')"))]
|
||||
public function __invoke(Request $request, ProjectStatisticService $service)
|
||||
public function __invoke(Request $request, ProjectStatisticService $service): Response
|
||||
{
|
||||
$dateFactory = $this->getDateTimeFactory();
|
||||
$user = $this->getUser();
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ReportUsersMonthController extends AbstractController
|
||||
{
|
||||
$data = $this->getData($request, $statisticService, $userRepository);
|
||||
|
||||
$content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
|
||||
$content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ReportUsersWeekController extends AbstractController
|
||||
{
|
||||
$data = $this->getData($request, $statisticService, $userRepository);
|
||||
|
||||
$content = $this->container->get('twig')->render('reporting/report_user_list_export.html.twig', $data);
|
||||
$content = $this->renderView('reporting/report_user_list_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
@@ -19,7 +19,6 @@ use App\Reporting\YearlyUserList\YearlyUserListForm;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\TimesheetStatisticService;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -30,11 +29,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('report:other')]
|
||||
final class ReportUsersYearController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year', name: 'report_yearly_users', methods: ['GET', 'POST'])]
|
||||
public function report(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
@@ -44,17 +38,12 @@ final class ReportUsersYearController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year_export', name: 'report_yearly_users_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, SystemConfiguration $systemConfiguration, TimesheetStatisticService $statisticService, UserRepository $userRepository): Response
|
||||
{
|
||||
$data = $this->getData($request, $systemConfiguration, $statisticService, $userRepository);
|
||||
|
||||
$content = $this->container->get('twig')->render('reporting/report_user_list_monthly_export.html.twig', $data);
|
||||
$content = $this->renderView('reporting/report_user_list_monthly_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Reporting\MonthByUser\MonthByUser;
|
||||
use App\Reporting\MonthByUser\MonthByUserForm;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -35,6 +38,21 @@ final class UserMonthController extends AbstractUserReportController
|
||||
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
||||
}
|
||||
|
||||
#[Route(path: '/month_export', name: 'report_user_month_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$data = $this->getData($request);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-monthly');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
@@ -95,6 +113,7 @@ final class UserMonthController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $nextMonth,
|
||||
'previous' => $previousMonth,
|
||||
'export_route' => 'report_user_month_export',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
|
||||
namespace App\Controller\Reporting;
|
||||
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\DailyStatistic;
|
||||
use App\Reporting\WeekByUser\WeekByUser;
|
||||
use App\Reporting\WeekByUser\WeekByUserForm;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -34,6 +37,21 @@ final class UserWeekController extends AbstractUserReportController
|
||||
return $this->render('reporting/report_by_user.html.twig', $this->getData($request));
|
||||
}
|
||||
|
||||
#[Route(path: '/week_export', name: 'report_user_week_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$data = $this->getData($request);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-weekly');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
@@ -88,6 +106,7 @@ final class UserWeekController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
'export_route' => 'report_user_week_export',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Controller\Reporting;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Model\DateStatisticInterface;
|
||||
use App\Model\MonthlyStatistic;
|
||||
use App\Reporting\YearByUser\YearByUser;
|
||||
@@ -18,6 +20,7 @@ use App\Reporting\YearByUser\YearByUserForm;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -39,6 +42,21 @@ final class UserYearController extends AbstractUserReportController
|
||||
return $this->render('reporting/report_by_user_year.html.twig', $this->getData($request, $systemConfiguration));
|
||||
}
|
||||
|
||||
#[Route(path: '/year_export', name: 'report_user_year_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
$data = $this->getData($request, $systemConfiguration);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_year_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->loadFromString($content);
|
||||
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-export-user-yearly');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request, SystemConfiguration $systemConfiguration): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
@@ -104,6 +122,7 @@ final class UserYearController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
'export_route' => 'report_user_year_export',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
27
src/Controller/Security/LoginLinkController.php
Normal file
27
src/Controller/Security/LoginLinkController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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\Controller\Security;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route(path: '/auth/link')]
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
final class LoginLinkController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/check', name: 'link_login_check', methods: ['GET'])]
|
||||
public function check(): Response
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use App\Form\PasswordResetForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use DateTime;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -26,7 +27,6 @@ use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/resetting')]
|
||||
@@ -108,7 +108,7 @@ final class PasswordResetController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/check_email.html.twig', [
|
||||
'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600),
|
||||
'tokenLifetime' => $this->configuration->getPasswordResetRetryLifetime(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,13 +49,13 @@ final class SecurityController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/login_check', name: 'security_check', methods: ['POST'])]
|
||||
public function checkAction()
|
||||
public function checkAction(): Response
|
||||
{
|
||||
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'logout', methods: ['GET', 'POST'])]
|
||||
public function logoutAction()
|
||||
public function logoutAction(): Response
|
||||
{
|
||||
throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Event\EmailSelfRegistrationEvent;
|
||||
use App\Form\SelfRegistrationForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -27,7 +28,6 @@ use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[Route(path: '/register')]
|
||||
|
||||
@@ -36,7 +36,7 @@ use App\Utils\PageSetup;
|
||||
use App\Validator\Constraints\ColorChoices;
|
||||
use App\Validator\Constraints\DateTimeFormat;
|
||||
use App\Validator\Constraints\TimeFormat;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
|
||||
@@ -33,7 +33,7 @@ use App\Timesheet\TimesheetService;
|
||||
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -334,6 +334,13 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$execute = false;
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($timesheets as $timesheet) {
|
||||
if ($dto->isReplaceDescription()) {
|
||||
$timesheet->setDescription($dto->getDescription());
|
||||
$execute = true;
|
||||
} elseif($dto->getDescription() !== null && $dto->getDescription() !== '') {
|
||||
$timesheet->setDescription($timesheet->getDescription() . PHP_EOL . $dto->getDescription());
|
||||
$execute = true;
|
||||
}
|
||||
if ($dto->isReplaceTags()) {
|
||||
foreach ($timesheet->getTags() as $tag) {
|
||||
$timesheet->removeTag($tag);
|
||||
@@ -418,7 +425,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function multiDelete(Request $request)
|
||||
protected function multiDelete(Request $request): Response
|
||||
{
|
||||
$form = $this->getMultiUpdateActionForm();
|
||||
$form->handleRequest($request);
|
||||
@@ -446,7 +453,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
protected function prepareQuery(TimesheetQuery $query): void
|
||||
{
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
|
||||
return $this->multiDelete($request);
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
protected function prepareQuery(TimesheetQuery $query): void
|
||||
{
|
||||
$query->setCurrentUser($this->getUser());
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use App\Repository\UserRepository;
|
||||
use App\User\UserService;
|
||||
use App\Utils\DataTable;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -193,7 +193,7 @@ final class UserController extends AbstractController
|
||||
|
||||
#[Route(path: '/export', name: 'user_export', methods: ['GET'])]
|
||||
#[IsGranted('view_user')]
|
||||
public function exportAction(Request $request, UserExporter $exporter)
|
||||
public function exportAction(Request $request, UserExporter $exporter): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Entity\UserPreference;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\SkinType;
|
||||
use App\Form\Type\TimezoneType;
|
||||
use App\Form\UserPasswordType;
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -58,6 +59,11 @@ final class WizardController extends AbstractController
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$next = 'done';
|
||||
if ($user->requiresPasswordReset()) {
|
||||
$next = 'password';
|
||||
}
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
@@ -72,13 +78,44 @@ final class WizardController extends AbstractController
|
||||
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->redirectToRoute('wizard', ['wizard' => $next, '_locale' => $data['language']]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('wizard/profile.html.twig', [
|
||||
'percent' => \intval(100 / \count(User::WIZARDS) * 1),
|
||||
'previous' => 'intro',
|
||||
'next' => $next,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($wizard === 'password' || $user->requiresPasswordReset()) {
|
||||
$form = $this->createForm(UserPasswordType::class, $user, [
|
||||
'action' => $this->generateUrl('wizard', ['wizard' => 'password']),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->setRequiresPasswordReset(false);
|
||||
$userService->updateUser($user);
|
||||
|
||||
return $this->redirectToRoute('wizard', ['wizard' => 'done']);
|
||||
}
|
||||
|
||||
$previous = 'profile';
|
||||
$percent = \intval(100 / \count(User::WIZARDS) * 1);
|
||||
|
||||
if ($user->requiresPasswordReset()) {
|
||||
$previous = null;
|
||||
$percent = null;
|
||||
}
|
||||
|
||||
return $this->render('wizard/password.html.twig', [
|
||||
'percent' => $percent,
|
||||
'previous' => $previous,
|
||||
'next' => 'done',
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
|
||||
@@ -22,7 +22,7 @@ use App\Repository\Query\CustomerQuery;
|
||||
use App\Utils\NumberGenerator;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final class CustomerService
|
||||
|
||||
@@ -11,23 +11,33 @@ namespace App\Customer;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Event\CustomerBudgetStatisticEvent;
|
||||
use App\Event\CustomerStatisticEvent;
|
||||
use App\Model\CustomerBudgetStatisticModel;
|
||||
use App\Model\CustomerStatistic;
|
||||
use App\Reporting\CustomerView\CustomerViewModel;
|
||||
use App\Reporting\CustomerView\CustomerViewQuery;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Loader\CustomerLoader;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class CustomerStatisticService
|
||||
{
|
||||
public function __construct(private TimesheetRepository $timesheetRepository, private EventDispatcherInterface $dispatcher)
|
||||
public function __construct(
|
||||
private CustomerRepository $customerRepository,
|
||||
private TimesheetRepository $timesheetRepository,
|
||||
private EventDispatcherInterface $dispatcher
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -85,6 +95,7 @@ class CustomerStatisticService
|
||||
$result = $qb->getQuery()->getResult();
|
||||
|
||||
if (null !== $result) {
|
||||
/** @var array{'id': string, 'duration': int, 'internalRate': float, 'counter': int, 'rate': float, 'billable': int, 'exported': int} $resultRow */
|
||||
foreach ($result as $resultRow) {
|
||||
$statistic = $statistics[$resultRow['id']];
|
||||
$statistic->setDuration($statistic->getDuration() + $resultRow['duration']);
|
||||
@@ -113,12 +124,16 @@ class CustomerStatisticService
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Customer> $customers
|
||||
*/
|
||||
private function createStatisticQueryBuilder(array $customers, DateTime $begin = null, ?DateTime $end = null): QueryBuilder
|
||||
{
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(p.customer) AS id')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->join(Customer::class, 'c', Query\Expr\Join::WITH, 'p.customer = c.id')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) as rate')
|
||||
->addSelect('COALESCE(SUM(t.internalRate), 0) as internalRate')
|
||||
@@ -149,4 +164,177 @@ class CustomerStatisticService
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerViewQuery $query
|
||||
* @return Customer[]
|
||||
*/
|
||||
public function findCustomersForView(CustomerViewQuery $query): array
|
||||
{
|
||||
$user = $query->getUser();
|
||||
|
||||
$qb = $this->customerRepository->createQueryBuilder('c');
|
||||
$qb
|
||||
->select('c')
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->addGroupBy('c')
|
||||
;
|
||||
|
||||
if ($query->isIncludeWithBudget()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gt('c.timeBudget', 0),
|
||||
$qb->expr()->gt('c.budget', 0)
|
||||
)
|
||||
);
|
||||
} elseif ($query->isIncludeWithoutBudget()) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('c.timeBudget', 0),
|
||||
$qb->expr()->eq('c.budget', 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->customerRepository->addPermissionCriteria($qb, $user);
|
||||
|
||||
/** @var Customer[] $customers */
|
||||
$customers = $qb->getQuery()->getResult();
|
||||
|
||||
// pre-cache customer objects instead of joining them
|
||||
$loader = new CustomerLoader($this->customerRepository->createQueryBuilder('c')->getEntityManager(), false);
|
||||
$loader->loadResults($customers);
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Customer[] $customers
|
||||
* @param DateTime $today
|
||||
* @return CustomerViewModel[]
|
||||
*/
|
||||
public function getCustomerView(User $user, array $customers, DateTime $today): array
|
||||
{
|
||||
$today = clone $today;
|
||||
|
||||
/** @var array<int, CustomerViewModel> $customerViews */
|
||||
$customerViews = [];
|
||||
foreach ($customers as $customer) {
|
||||
$customerViews[$customer->getId()] = new CustomerViewModel($customer);
|
||||
}
|
||||
|
||||
$budgetStats = $this->getBudgetStatisticModelForCustomers($customers, $today);
|
||||
foreach ($budgetStats as $model) {
|
||||
$customerViews[$model->getCustomer()->getId()]->setBudgetStatisticModel($model);
|
||||
}
|
||||
|
||||
$customerIds = array_keys($customerViews);
|
||||
|
||||
$tplQb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$tplQb
|
||||
->select('c.id AS id')
|
||||
->join(Project::class, 'p', Query\Expr\Join::WITH, 't.project = p.id')
|
||||
->join(Customer::class, 'c', Query\Expr\Join::WITH, 'p.customer = c.id')
|
||||
->addSelect('COUNT(t.id) as amount')
|
||||
->addSelect('COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->addSelect('COALESCE(SUM(t.rate), 0) AS rate')
|
||||
->andWhere($tplQb->expr()->in('c.id', ':customer'))
|
||||
->groupBy('id')
|
||||
->setParameter('customer', array_values($customerIds))
|
||||
;
|
||||
|
||||
$qb = clone $tplQb;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
/** @var array{'duration': int, 'amount': int, 'rate': float, 'id': string, 'exported': int} $row */
|
||||
foreach ($result as $row) {
|
||||
$customerViews[$row['id']]->setDurationTotal($row['duration']);
|
||||
$customerViews[$row['id']]->setRateTotal($row['rate']);
|
||||
$customerViews[$row['id']]->setTimesheetCounter($row['amount']);
|
||||
}
|
||||
|
||||
$qb = clone $tplQb;
|
||||
$qb
|
||||
->addSelect('t.exported')
|
||||
->addSelect('t.billable')
|
||||
->addGroupBy('t.exported')
|
||||
->addGroupBy('t.billable')
|
||||
;
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
/** @var array{'duration': int, 'billable': int, 'rate': float, 'exported': int, 'id': string} $row */
|
||||
foreach ($result as $row) {
|
||||
$view = $customerViews[$row['id']];
|
||||
if ($row['billable'] === 1 && $row['exported'] === 1) {
|
||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
||||
} elseif ($row['billable'] === 1 && $row['exported'] === 0) {
|
||||
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
|
||||
$view->setBillableRate($view->getBillableRate() + $row['rate']);
|
||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
||||
$view->setNotBilledDuration($view->getNotBilledDuration() + $row['duration']);
|
||||
$view->setNotBilledRate($view->getNotBilledRate() + $row['rate']);
|
||||
} elseif ($row['billable'] === 0 && $row['exported'] === 0) {
|
||||
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
|
||||
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
|
||||
}
|
||||
// the last possible case $row['billable'] === 0 && $row['exported'] === 1 is extremely unlikely and not used
|
||||
}
|
||||
|
||||
return array_values($customerViews);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer[] $customers
|
||||
* @param DateTime $today
|
||||
* @return CustomerBudgetStatisticModel[]
|
||||
*/
|
||||
public function getBudgetStatisticModelForCustomers(array $customers, DateTime $today): array
|
||||
{
|
||||
$models = [];
|
||||
$monthly = [];
|
||||
$allTime = [];
|
||||
|
||||
foreach ($customers as $customer) {
|
||||
$models[$customer->getId()] = new CustomerBudgetStatisticModel($customer);
|
||||
if ($customer->isMonthlyBudget()) {
|
||||
$monthly[] = $customer;
|
||||
} else {
|
||||
$allTime[] = $customer;
|
||||
}
|
||||
}
|
||||
|
||||
$statisticsTotal = $this->getBudgetStatistic($customers);
|
||||
foreach ($statisticsTotal as $id => $statistic) {
|
||||
$models[$id]->setStatisticTotal($statistic);
|
||||
}
|
||||
|
||||
$dateFactory = new DateTimeFactory($today->getTimezone());
|
||||
|
||||
$begin = null;
|
||||
$end = $today;
|
||||
|
||||
if (\count($monthly) > 0) {
|
||||
$begin = $dateFactory->getStartOfMonth($today);
|
||||
$end = $dateFactory->getEndOfMonth($today);
|
||||
$statistics = $this->getBudgetStatistic($monthly, $begin, $end);
|
||||
foreach ($statistics as $id => $statistic) {
|
||||
$models[$id]->setStatistic($statistic);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($allTime) > 0) {
|
||||
// display the budget at the end of the selected period and not the total sum of all times (do not include times in the future)
|
||||
$statistics = $this->getBudgetStatistic($allTime, null, $today);
|
||||
foreach ($statistics as $id => $statistic) {
|
||||
$models[$id]->setStatistic($statistic);
|
||||
}
|
||||
}
|
||||
|
||||
$event = new CustomerBudgetStatisticEvent($models, $begin, $end);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $models;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ final class Configuration implements ConfigurationInterface
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function getProjectNode()
|
||||
private function getProjectNode(): ArrayNodeDefinition
|
||||
{
|
||||
$builder = new TreeBuilder('project');
|
||||
/** @var ArrayNodeDefinition $node */
|
||||
@@ -343,7 +343,7 @@ final class Configuration implements ConfigurationInterface
|
||||
->defaultValue('{Y}/{cy,3}')
|
||||
->end()
|
||||
->booleanNode('upload_twig')
|
||||
->defaultTrue()
|
||||
->defaultFalse()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
@@ -52,7 +52,7 @@ abstract class AbstractMigration extends BaseAbstractMigration
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function abortIfPlatformNotSupported()
|
||||
protected function abortIfPlatformNotSupported(): void
|
||||
{
|
||||
$platform = $this->connection->getDatabasePlatform();
|
||||
if (!($platform instanceof MySQLPlatform)) {
|
||||
|
||||
50
src/Doctrine/ModifiedSubscriber.php
Normal file
50
src/Doctrine/ModifiedSubscriber.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?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\Doctrine;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
/**
|
||||
* Automatically set the modifiedAt field for all Timesheet entries.
|
||||
*/
|
||||
#[AsDoctrineListener(event: Events::onFlush, priority: 60)]
|
||||
final class ModifiedSubscriber implements EventSubscriber, DataSubscriberInterface
|
||||
{
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
Events::onFlush,
|
||||
];
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $args): void
|
||||
{
|
||||
$uow = $args->getObjectManager()->getUnitOfWork();
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
|
||||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if (!($entity instanceof Timesheet)) {
|
||||
continue;
|
||||
}
|
||||
$entity->setModifiedAt($now);
|
||||
}
|
||||
|
||||
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||
if (!($entity instanceof Timesheet)) {
|
||||
continue;
|
||||
}
|
||||
$entity->setModifiedAt($now);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace App\Doctrine;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\CalculatorInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
@@ -18,6 +19,7 @@ use Doctrine\ORM\Events;
|
||||
/**
|
||||
* A listener to make sure all Timesheet entries will be calculated properly (e.g. duration and rates).
|
||||
*/
|
||||
#[AsDoctrineListener(event: Events::onFlush, priority: 50)]
|
||||
final class TimesheetSubscriber implements EventSubscriber, DataSubscriberInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -44,7 +44,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
|
||||
@@ -87,7 +87,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
*
|
||||
* @var Collection<ActivityMeta>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ActivityMeta::class, mappedBy: 'activity', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'activity', targetEntity: ActivityMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\ActivityMeta>')]
|
||||
@@ -102,7 +102,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
#[ORM\JoinTable(name: 'kimai2_activities_teams')]
|
||||
#[ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'activities')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'activities', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
@@ -232,7 +232,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team)
|
||||
public function addTeam(Team $team): void
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return;
|
||||
@@ -242,7 +242,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
$team->addActivity($this);
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
public function removeTeam(Team $team): void
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
|
||||
@@ -23,7 +23,7 @@ class ActivityMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Activity::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Activity $activity = null;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class ActivityRate implements RateInterface
|
||||
use Rate;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Activity::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Activity $activity = null;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class Bookmark
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'type', type: 'string', length: 20, nullable: false)]
|
||||
|
||||
@@ -15,17 +15,17 @@ interface CommentInterface
|
||||
|
||||
public function getMessage(): ?string;
|
||||
|
||||
public function setMessage(string $message);
|
||||
public function setMessage(string $message): void;
|
||||
|
||||
public function getCreatedBy(): ?User;
|
||||
|
||||
public function setCreatedBy(User $createdBy);
|
||||
public function setCreatedBy(User $createdBy): void;
|
||||
|
||||
public function getCreatedAt(): ?\DateTime;
|
||||
|
||||
public function setCreatedAt(\DateTime $createdAt);
|
||||
public function setCreatedAt(\DateTime $createdAt): void;
|
||||
|
||||
public function isPinned(): bool;
|
||||
|
||||
public function setPinned(bool $pinned);
|
||||
public function setPinned(bool $pinned): void;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ trait CommentTableTypeTrait
|
||||
#[Assert\NotNull]
|
||||
private ?string $message = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $createdBy = null;
|
||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||
|
||||
@@ -156,7 +156,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
*
|
||||
* @var Collection<CustomerMeta>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: CustomerMeta::class, mappedBy: 'customer', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'customer', targetEntity: CustomerMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\CustomerMeta>')]
|
||||
@@ -171,7 +171,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
#[ORM\JoinTable(name: 'kimai2_customers_teams')]
|
||||
#[ORM\JoinColumn(name: 'customer_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'customers')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'customers', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
@@ -180,7 +180,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
* Default invoice template for this customer
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?InvoiceTemplate $invoiceTemplate = null;
|
||||
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
|
||||
private ?string $invoiceText = null;
|
||||
@@ -440,7 +440,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team)
|
||||
public function addTeam(Team $team): void
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return;
|
||||
@@ -450,7 +450,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
$team->addCustomer($this);
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
public function removeTeam(Team $team): void
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
|
||||
@@ -21,7 +21,7 @@ class CustomerComment implements CommentInterface
|
||||
use CommentTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private Customer $customer;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class CustomerMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class CustomerRate implements RateInterface
|
||||
use Rate;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@ class Invoice implements EntityWithMetaFields
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||
@@ -109,7 +109,7 @@ class Invoice implements EntityWithMetaFields
|
||||
*
|
||||
* @var Collection<InvoiceMeta>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: InvoiceMeta::class, mappedBy: 'invoice', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'invoice', targetEntity: InvoiceMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\InvoiceMeta>')]
|
||||
|
||||
@@ -23,7 +23,7 @@ class InvoiceMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Invoice::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Invoice $invoice = null;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
* Customer for this project
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
@@ -137,7 +137,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
*
|
||||
* @var Collection<ProjectMeta>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ProjectMeta::class, mappedBy: 'project', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\ProjectMeta>')]
|
||||
@@ -152,7 +152,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
#[ORM\JoinTable(name: 'kimai2_projects_teams')]
|
||||
#[ORM\JoinColumn(name: 'project_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'projects')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'projects', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
@@ -253,7 +253,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
* Make sure begin and end date have the correct timezone.
|
||||
* This will be called once for each item after being loaded from the database.
|
||||
*/
|
||||
protected function localizeDates()
|
||||
protected function localizeDates(): void
|
||||
{
|
||||
if ($this->localized) {
|
||||
return;
|
||||
@@ -392,7 +392,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team)
|
||||
public function addTeam(Team $team): void
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return;
|
||||
@@ -402,7 +402,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
$team->addProject($this);
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
public function removeTeam(Team $team): void
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
|
||||
@@ -21,7 +21,7 @@ class ProjectComment implements CommentInterface
|
||||
use CommentTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private Project $project;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class ProjectMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Project $project = null;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class ProjectRate implements RateInterface
|
||||
use Rate;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Project $project = null;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ trait Rate
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[OA\Property(ref: '#/components/schemas/User')]
|
||||
|
||||
@@ -25,7 +25,7 @@ class RolePermission
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: Role::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Role $role = null;
|
||||
#[ORM\Column(name: 'permission', type: 'string', length: 50, nullable: false)]
|
||||
|
||||
@@ -39,7 +39,7 @@ class Tag
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 2, max: 100, normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: '/,/', match: false, message: 'Tag name cannot contain comma')]
|
||||
#[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $name = null;
|
||||
|
||||
@@ -47,7 +47,7 @@ class Team
|
||||
*
|
||||
* @var Collection<TeamMember>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'team', fetch: 'LAZY', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'team', targetEntity: TeamMember::class, cascade: ['persist', 'remove'], fetch: 'LAZY', orphanRemoval: true)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE')]
|
||||
#[Assert\Count(min: 1)]
|
||||
#[Serializer\Expose]
|
||||
@@ -59,7 +59,7 @@ class Team
|
||||
*
|
||||
* @var Collection<Customer>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
||||
#[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Customer'))]
|
||||
@@ -69,7 +69,7 @@ class Team
|
||||
*
|
||||
* @var Collection<Project>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
||||
#[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Project'))]
|
||||
@@ -79,7 +79,7 @@ class Team
|
||||
*
|
||||
* @var Collection<Activity>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
||||
#[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Activity'))]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user