Compare commits
23 Commits
2.20.0
...
api-pagina
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233c0e792e | ||
|
|
896e8b42c1 | ||
|
|
bf288bf115 | ||
|
|
13649e146c | ||
|
|
0f9b964b11 | ||
|
|
4076e1c3d3 | ||
|
|
8de54e1fa7 | ||
|
|
1965c35b43 | ||
|
|
3fd770047e | ||
|
|
036b297a76 | ||
|
|
537c120ad9 | ||
|
|
3e0dadc0c8 | ||
|
|
004ac5a6b9 | ||
|
|
3204dcb03e | ||
|
|
b1903ba183 | ||
|
|
75fa7a20b8 | ||
|
|
a19426147a | ||
|
|
9d933f62c0 | ||
|
|
9e3d243b4b | ||
|
|
62047c7c01 | ||
|
|
0bcd45babe | ||
|
|
2a72815481 | ||
|
|
3e3cdc5e69 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: [kevinpapst]
|
||||
custom: ["https://www.kimai.org/", "https://www.kimai.tw/", "https://www.kimai.co.il/"]
|
||||
custom: ["https://www.kimai.org/", "https://www.kimai.cloud/", "https://www.kevinpapst.de/"]
|
||||
|
||||
@@ -54,7 +54,6 @@ LABEL org.opencontainers.image.title="Kimai" \
|
||||
org.opencontainers.image.documentation="https://www.kimai.org/documentation/" \
|
||||
org.opencontainers.image.source="https://github.com/kimai/kimai" \
|
||||
org.opencontainers.image.version="${KIMAI}" \
|
||||
org.opencontainers.image.revision="${{github.sha}}" \
|
||||
org.opencontainers.image.vendor="Kevin Papst" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||
org.opencontainers.image.base.name="docker.io/library/alpine"
|
||||
|
||||
26
README.md
26
README.md
@@ -26,9 +26,7 @@ and so much more.
|
||||
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+)
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — EOL since mid of 2023 (PHP 7.4)
|
||||
|
||||
Do **NOT** use Version 1, it won't get any more updates!
|
||||
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — do **NOT** use, EOL since mid of 2023 (PHP 7.4)
|
||||
|
||||
### Links
|
||||
|
||||
@@ -38,23 +36,20 @@ Do **NOT** use Version 1, it won't get any more updates!
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 8.1.3 minimum
|
||||
- PHP 8.1.3 minimum (support for PHP 8.2 and 8.3)
|
||||
- MariaDB or MySQL
|
||||
- A webserver and subdomain (subdirectory is not supported)
|
||||
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
|
||||
|
||||
Support for PHP 8.2 and 8.3.
|
||||
|
||||
## Installation
|
||||
|
||||
- [Native setup](https://www.kimai.org/documentation/installation.html) — with Git and Composer
|
||||
- [Docker](https://hub.docker.com/r/kimai/kimai2) — containerized by @tobybatch
|
||||
- Caddy with Docker-Compose at [Hetzner](https://www.kimai.org/documentation/hosting-hetzner-cloud.html) and [DigitalOcean](https://www.kimai.org/documentation/hosting-digital-ocean.html)
|
||||
- [SSH setup](https://www.kimai.org/documentation/installation.html) with Git and Composer
|
||||
- [Docker images](https://hub.docker.com/r/kimai/kimai2) with FPM only or incl. Apache
|
||||
- [Synology](https://www.kimai.org/documentation/synology.html) user can host the Docker version
|
||||
- [Developer setups](https://www.kimai.org/documentation/developers.html) if you want to create Kimai integrations
|
||||
|
||||
There are also documentations for:
|
||||
- [developer setups](https://www.kimai.org/documentation/developers.html) — on your local machine
|
||||
- [shared hostings](https://www.kimai.org/documentation/installation.html#shared-hosting) — the least favorable option
|
||||
- [Synology](https://www.kimai.org/documentation/synology.html) — you could try to host the Docker version instead
|
||||
- [1-click installer](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations) — hosted environments
|
||||
There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html).
|
||||
|
||||
And if you don't want to host Kimai, you can use [the Cloud version](https://www.kimai.cloud/) of it.
|
||||
|
||||
@@ -65,13 +60,12 @@ And if you don't want to host Kimai, you can use [the Cloud version](https://www
|
||||
|
||||
### Plugins
|
||||
|
||||
- [Plugin marketplace](https://www.kimai.org/store/) — find existing plugins here
|
||||
- [Plugins](https://www.kimai.org/store/) — paid and free plugin marketplace
|
||||
- [Developer documentation](https://www.kimai.org/documentation/developers.html) — how to create a plugin
|
||||
|
||||
## Roadmap and releases
|
||||
|
||||
You can see a rough development roadmap in the [Milestones](https://github.com/kimai/kimai/milestones) sections.
|
||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kimai/kimai/issues) are welcome.
|
||||
You can see a rough development [roadmap](https://github.com/orgs/kimai/projects/2), which is open for changes and input from the community, your [ideas](https://github.com/kimai/kimai/issues) are welcome.
|
||||
|
||||
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.
|
||||
|
||||
20
SECURITY.md
20
SECURITY.md
@@ -1,27 +1,11 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As announced in the [README](README.md) I only support the latest available release and `main` branch.
|
||||
As announced in the [README](README.md) security fixes will only be added to the `main` branch.
|
||||
|
||||
| Version | Supported |
|
||||
|----------------------|--------------------|
|
||||
| main branch | :white_check_mark: |
|
||||
| latest minor release | :white_check_mark: |
|
||||
| older releases | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please read the [Bughunter](https://www.kimai.org/documentation/bughunter.html) documentation before posting.
|
||||
You can report any security related vulnerability in the [advisory section at GitHub](https://github.com/kimai/kimai/security/advisories)
|
||||
or via email to [support@kimai.org](mailto:support@kimai.org).
|
||||
|
||||
I will work as fast as I can to fix the problem and publish a bugfix release / security update.
|
||||
Depending on the size of the required fixes, this might take a couple of hours or a couple of days.
|
||||
|
||||
You can expect that your message will be answered ASAP.
|
||||
|
||||
You will be mentioned in the release notes if your issue is valid.
|
||||
I am grateful for any (discrete) disclosure of vulnerabilities!
|
||||
|
||||
Please note: if you are asking for money, I will not reply. I receive these scam messages every day...
|
||||
You find all information in our [Bughunter documentation](https://www.kimai.org/documentation/bughunter.html).
|
||||
|
||||
12
UPGRADING.md
12
UPGRADING.md
@@ -8,6 +8,18 @@ you can upgrade your Kimai installation to the latest stable release.
|
||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||
|
||||
## [2.14.0](https://github.com/kimai/kimai/releases/tag/2.13.0)
|
||||
|
||||
**DEVELOPERS ONLY**
|
||||
|
||||
The API now uses pagination globally for all collection endpoints. This might break current implementations
|
||||
|
||||
All collections are limited to 50 entries by default (changes in Customer, Project, Activity, User).
|
||||
|
||||
The parameters to influence pagination, page size and page number are `size` (and `pageSize`) and `page`.
|
||||
|
||||
Read the [API pagination docs](https://www.kimai.org/documentation/api-pagination.html) for more infos.
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
|
||||
@@ -85,7 +85,7 @@ export default class KimaiLoader {
|
||||
kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable'));
|
||||
kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
|
||||
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
|
||||
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form', ['td.multiCheckbox', 'td.actions']));
|
||||
kimai.registerPlugin(new KimaiRemoteModal());
|
||||
kimai.registerPlugin(new KimaiActiveRecords());
|
||||
kimai.registerPlugin(new KimaiAPILink('api-link'));
|
||||
|
||||
@@ -17,9 +17,10 @@ import { Modal } from 'bootstrap';
|
||||
|
||||
export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
|
||||
constructor(selector) {
|
||||
constructor(selector, stopSelector) {
|
||||
super();
|
||||
this._selector = selector;
|
||||
this._stopSelector = stopSelector;
|
||||
}
|
||||
|
||||
getId()
|
||||
@@ -63,7 +64,7 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
||||
|
||||
this.addClickHandler(this._selector, (href) => {
|
||||
this.openUrlInModal(href);
|
||||
});
|
||||
}, this._stopSelector);
|
||||
}
|
||||
|
||||
_getModal()
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class KimaiAlternativeLinks extends KimaiReducedClickHandler {
|
||||
init() {
|
||||
this.addClickHandler(this._selector, function(href) {
|
||||
window.location = href;
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
|
||||
/**
|
||||
* No _underscore naming for now, as it would be mangled otherwise
|
||||
* @param selector
|
||||
* @param callback
|
||||
* @param {string} selector
|
||||
* @param {callback} callback
|
||||
* @param {array<string>} stopSelector
|
||||
*/
|
||||
addClickHandler(selector, callback) {
|
||||
addClickHandler(selector, callback, stopSelector) {
|
||||
document.body.addEventListener('click', (event) => {
|
||||
// event.currentTarget is ALWAYS the body
|
||||
|
||||
@@ -38,6 +39,12 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x of stopSelector) {
|
||||
if (target.matches(x)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
target = target.parentNode;
|
||||
}
|
||||
|
||||
@@ -54,6 +61,12 @@ export default class KimaiReducedClickHandler extends KimaiPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x of stopSelector) {
|
||||
if (target.matches(x)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ fieldset legend.col-form-label {
|
||||
}
|
||||
|
||||
/* See project assignment to teams */
|
||||
fieldset.form-fieldset legend {
|
||||
fieldset.form-fieldset > legend {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ table.dataTable {
|
||||
}
|
||||
th.weekend,
|
||||
td.weekend {
|
||||
background-color: var(--tblr-bg-surface-secondary);
|
||||
background-color: var(--tblr-gray-50);
|
||||
}
|
||||
/* order is important, "today” should overwrite "weekend" therefor later in the file */
|
||||
th.today {
|
||||
|
||||
@@ -35,14 +35,14 @@
|
||||
"friendsofsymfony/rest-bundle": "^3.0",
|
||||
"gedmo/doctrine-extensions": "^3.6",
|
||||
"jms/serializer-bundle": "^5.0",
|
||||
"kevinpapst/tabler-bundle": "^1.2",
|
||||
"kevinpapst/tabler-bundle": "1.4.2",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"nelmio/api-doc-bundle": "^4.0",
|
||||
"nelmio/cors-bundle": "^2.0",
|
||||
"onelogin/php-saml": "^4.0",
|
||||
"pagerfanta/pagerfanta": "^3.0",
|
||||
"phpoffice/phpspreadsheet": "^1.16",
|
||||
"phpoffice/phpspreadsheet": "^2.0",
|
||||
"phpoffice/phpword": "^1.0",
|
||||
"psr/container": "^2.0",
|
||||
"psr/log": "^3.0",
|
||||
|
||||
839
composer.lock
generated
839
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@ framework:
|
||||
csrf_protection: true
|
||||
annotations: false
|
||||
handle_all_throwables: true
|
||||
disallow_search_engine_index: true
|
||||
set_content_language_from_locale: true
|
||||
|
||||
serializer:
|
||||
enable_attributes: true
|
||||
|
||||
@@ -117,10 +117,10 @@ kimai:
|
||||
ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING', 'EVERYONE']
|
||||
# mapping a "role name" to an array of "permission names"
|
||||
roles:
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile']
|
||||
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting','hours_own_profile']
|
||||
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile','hours_own_profile']
|
||||
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile','hours_own_profile']
|
||||
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile','hours_own_profile']
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -19,12 +19,6 @@ when@prod:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!deprecation"]
|
||||
# TODO reactivate once the many Doctrine deprecations are fixed
|
||||
# deprecation:
|
||||
# type: stream
|
||||
# channels: ["deprecation"]
|
||||
# path: "%kernel.logs_dir%/deprecations.log"
|
||||
# formatter: monolog.formatter.deprecation
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
|
||||
@@ -40,6 +40,8 @@ nelmio_api_doc:
|
||||
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
|
||||
- { alias: TeamMember, type: App\Entity\TeamMember, groups: [Team_Entity] }
|
||||
- { alias: TeamMembership, type: App\Entity\TeamMember, groups: [User_Entity] }
|
||||
- { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] }
|
||||
- { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] }
|
||||
areas:
|
||||
path_patterns:
|
||||
- ^/api(?!/doc)
|
||||
|
||||
@@ -17,9 +17,10 @@ nelmio_cors:
|
||||
hosts: []
|
||||
origin_regex: true
|
||||
forced_allow_origin_value: ~
|
||||
# allow_private_network: true
|
||||
paths:
|
||||
'^/api/':
|
||||
allow_origin: ['*']
|
||||
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type']
|
||||
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type', 'Authorization']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
max_age: 3600
|
||||
|
||||
@@ -89,12 +89,12 @@ security:
|
||||
access_control:
|
||||
- { path: '^/auth/2fa', role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
|
||||
- { path: '^/auth', roles: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)$', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/auth', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/login', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/register', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/resetting', role: PUBLIC_ACCESS }
|
||||
- { path: '^/(%app_locales%)/', roles: ROLE_USER }
|
||||
- { path: '^/{_locale}$', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/auth', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/login', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/register', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/resetting', role: PUBLIC_ACCESS }
|
||||
- { path: '^/{_locale}/', roles: ROLE_USER }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED }
|
||||
|
||||
when@test:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
framework:
|
||||
# https://symfony.com/doc/current/reference/configuration/framework.html#enabled-locales
|
||||
enabled_locales: '%kimai_locales%'
|
||||
default_locale: '%locale%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
|
||||
@@ -2,8 +2,6 @@ controllers:
|
||||
resource: ../src/Controller/
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -31,8 +29,6 @@ security:
|
||||
resource: ../src/Controller/Security/
|
||||
type: attribute
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -49,8 +45,6 @@ home:
|
||||
|
||||
homeLocale:
|
||||
path: /{_locale}
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /{_locale}/_error
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
app.test.active_entries:
|
||||
path: /{_locale}/layou/active_entries
|
||||
controller: App\Controller\LayoutController::activeEntries
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
parameters:
|
||||
locale: en
|
||||
# can be regenerated with "bin/console kimai:reset:locales"
|
||||
app_locales: ar|cs|da|de|de_CH|el|en|eo|es|eu|fa|fi|fo|fr|he|hr|hu|id|it|ja|ko|nb_NO|nl|pa|pl|pt|pt_BR|ro|ru|sk|sl|sv|tr|uk|vi|zh_CN|zh_Hant|zh_Hant_TW|cs_CZ|da_DK|da_GL|de_AT|de_BE|de_CH|de_DE|de_IT|de_LI|de_LU|el_CY|el_GR|en_AE|en_AG|en_AI|en_AS|en_AT|en_AU|en_BB|en_BE|en_BI|en_BM|en_BS|en_BW|en_BZ|en_CA|en_CC|en_CH|en_CK|en_CM|en_CX|en_CY|en_DE|en_DG|en_DK|en_DM|en_ER|en_FI|en_FJ|en_FK|en_FM|en_GB|en_GD|en_GG|en_GH|en_GI|en_GM|en_GU|en_GY|en_HK|en_ID|en_IE|en_IL|en_IM|en_IN|en_IO|en_JE|en_JM|en_KE|en_KI|en_KN|en_KY|en_LC|en_LR|en_LS|en_MG|en_MH|en_MO|en_MP|en_MS|en_MT|en_MU|en_MV|en_MW|en_MY|en_NA|en_NF|en_NG|en_NH|en_NL|en_NR|en_NU|en_NZ|en_PG|en_PH|en_PK|en_PN|en_PR|en_PW|en_RH|en_RW|en_SB|en_SC|en_SD|en_SE|en_SG|en_SH|en_SI|en_SL|en_SS|en_SX|en_SZ|en_TC|en_TK|en_TO|en_TT|en_TV|en_TZ|en_UG|en_UM|en_US|en_VC|en_VG|en_VI|en_VU|en_WS|en_ZA|en_ZM|en_ZW|es_AR|es_BO|es_BR|es_BZ|es_CL|es_CO|es_CR|es_CU|es_DO|es_EA|es_EC|es_ES|es_GQ|es_GT|es_HN|es_IC|es_MX|es_NI|es_PA|es_PE|es_PH|es_PR|es_PY|es_SV|es_US|es_UY|es_VE|eu_ES|fa_AF|fa_IR|fi_FI|fo_DK|fo_FO|fr_BE|fr_BF|fr_BI|fr_BJ|fr_BL|fr_CA|fr_CD|fr_CF|fr_CG|fr_CH|fr_CI|fr_CM|fr_DJ|fr_DZ|fr_FR|fr_GA|fr_GF|fr_GN|fr_GP|fr_GQ|fr_HT|fr_KM|fr_LU|fr_MA|fr_MC|fr_MF|fr_MG|fr_ML|fr_MQ|fr_MR|fr_MU|fr_NC|fr_NE|fr_PF|fr_PM|fr_RE|fr_RW|fr_SC|fr_SN|fr_SY|fr_TD|fr_TG|fr_TN|fr_VU|fr_WF|fr_YT|he_IL|hr_BA|hr_HR|hu_HU|it_CH|it_IT|it_SM|it_VA|ja_JP|ko_CN|ko_KP|ko_KR|nl_AW|nl_BE|nl_BQ|nl_CW|nl_NL|nl_SR|nl_SX|pl_PL|pt_AO|pt_BR|pt_CH|pt_CV|pt_GQ|pt_GW|pt_LU|pt_MO|pt_MZ|pt_PT|pt_ST|pt_TL|ro_MD|ro_RO|ru_BY|ru_KG|ru_KZ|ru_MD|ru_RU|ru_UA|sk_SK|sv_AX|sv_FI|sv_SE|tr_CY|tr_TR|uk_UA|vi_VN
|
||||
# can be regenerated with: bin/console kimai:reset:locales
|
||||
kimai_locales: ['ar', 'ca', 'cs', 'da', 'de', 'de_CH', 'el', 'en', 'eo', 'es', 'eu', 'fa', 'fi', 'fo', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'nb_NO', 'nl', 'pa', 'pl', 'pt', 'pt_BR', 'ro', 'ru', 'sk', 'sl', 'sv', 'tr', 'uk', 'vi', 'zh_CN', 'zh_Hant', 'zh_Hant_TW', 'ca_AD', 'ca_ES', 'ca_FR', 'ca_IT', 'cs_CZ', 'da_DK', 'da_GL', 'de_AT', 'de_BE', 'de_CH', 'de_DE', 'de_IT', 'de_LI', 'de_LU', 'el_CY', 'el_GR', 'en_AE', 'en_AG', 'en_AI', 'en_AS', 'en_AT', 'en_AU', 'en_BB', 'en_BE', 'en_BI', 'en_BM', 'en_BS', 'en_BW', 'en_BZ', 'en_CA', 'en_CC', 'en_CH', 'en_CK', 'en_CM', 'en_CX', 'en_CY', 'en_DE', 'en_DG', 'en_DK', 'en_DM', 'en_ER', 'en_FI', 'en_FJ', 'en_FK', 'en_FM', 'en_GB', 'en_GD', 'en_GG', 'en_GH', 'en_GI', 'en_GM', 'en_GU', 'en_GY', 'en_HK', 'en_ID', 'en_IE', 'en_IL', 'en_IM', 'en_IN', 'en_IO', 'en_JE', 'en_JM', 'en_KE', 'en_KI', 'en_KN', 'en_KY', 'en_LC', 'en_LR', 'en_LS', 'en_MG', 'en_MH', 'en_MO', 'en_MP', 'en_MS', 'en_MT', 'en_MU', 'en_MV', 'en_MW', 'en_MY', 'en_NA', 'en_NF', 'en_NG', 'en_NH', 'en_NL', 'en_NR', 'en_NU', 'en_NZ', 'en_PG', 'en_PH', 'en_PK', 'en_PN', 'en_PR', 'en_PW', 'en_RH', 'en_RW', 'en_SB', 'en_SC', 'en_SD', 'en_SE', 'en_SG', 'en_SH', 'en_SI', 'en_SL', 'en_SS', 'en_SX', 'en_SZ', 'en_TC', 'en_TK', 'en_TO', 'en_TT', 'en_TV', 'en_TZ', 'en_UG', 'en_UM', 'en_US', 'en_VC', 'en_VG', 'en_VI', 'en_VU', 'en_WS', 'en_ZA', 'en_ZM', 'en_ZW', 'es_AR', 'es_BO', 'es_BR', 'es_BZ', 'es_CL', 'es_CO', 'es_CR', 'es_CU', 'es_DO', 'es_EA', 'es_EC', 'es_ES', 'es_GQ', 'es_GT', 'es_HN', 'es_IC', 'es_MX', 'es_NI', 'es_PA', 'es_PE', 'es_PH', 'es_PR', 'es_PY', 'es_SV', 'es_US', 'es_UY', 'es_VE', 'eu_ES', 'fa_AF', 'fa_IR', 'fi_FI', 'fo_DK', 'fo_FO', 'fr_BE', 'fr_BF', 'fr_BI', 'fr_BJ', 'fr_BL', 'fr_CA', 'fr_CD', 'fr_CF', 'fr_CG', 'fr_CH', 'fr_CI', 'fr_CM', 'fr_DJ', 'fr_DZ', 'fr_FR', 'fr_GA', 'fr_GF', 'fr_GN', 'fr_GP', 'fr_GQ', 'fr_HT', 'fr_KM', 'fr_LU', 'fr_MA', 'fr_MC', 'fr_MF', 'fr_MG', 'fr_ML', 'fr_MQ', 'fr_MR', 'fr_MU', 'fr_NC', 'fr_NE', 'fr_PF', 'fr_PM', 'fr_RE', 'fr_RW', 'fr_SC', 'fr_SN', 'fr_SY', 'fr_TD', 'fr_TG', 'fr_TN', 'fr_VU', 'fr_WF', 'fr_YT', 'he_IL', 'hr_BA', 'hr_HR', 'hu_HU', 'it_CH', 'it_IT', 'it_SM', 'it_VA', 'ja_JP', 'ko_CN', 'ko_KP', 'ko_KR', 'nl_AW', 'nl_BE', 'nl_BQ', 'nl_CW', 'nl_NL', 'nl_SR', 'nl_SX', 'pl_PL', 'pt_AO', 'pt_BR', 'pt_CH', 'pt_CV', 'pt_GQ', 'pt_GW', 'pt_LU', 'pt_MO', 'pt_MZ', 'pt_PT', 'pt_ST', 'pt_TL', 'ro_MD', 'ro_RO', 'ru_BY', 'ru_KG', 'ru_KZ', 'ru_MD', 'ru_RU', 'ru_UA', 'sk_SK', 'sv_AX', 'sv_FI', 'sv_SE', 'tr_CY', 'tr_TR', 'uk_UA', 'vi_VN']
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@@ -23,12 +23,14 @@ services:
|
||||
- '../src/API/Model/'
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Event/'
|
||||
- '../src/Form/Model/'
|
||||
- '../src/Model/'
|
||||
- '../src/Repository/Loader/'
|
||||
- '../src/Repository/Paginator/'
|
||||
- '../src/Repository/Query/'
|
||||
- '../src/Repository/Result/'
|
||||
- '../src/Event/'
|
||||
- '../src/Model/'
|
||||
- '../src/WorkingTime/Calculator/'
|
||||
- '../src/Kernel.php'
|
||||
- '../src/Constants.php'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: ar|cs|de|de_CH|da|en|eo|es|eu|fr|he|hu|it|ja|ko|nl|pl|pt_BR|ro|ru|sk|sv|tr|vi|zh_CN
|
||||
kimai_locales: ['ar', 'cs', 'de', 'de_CH', 'da', 'en', 'eo', 'es', 'eu', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pl', 'pt_BR', 'ro', 'ru', 'sk', 'sv', 'tr', 'vi', 'zh_CN']
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
@@ -23,7 +23,3 @@ services:
|
||||
App\User\UserService:
|
||||
public: true
|
||||
arguments: ['@App\Repository\UserRepository', '@event_dispatcher', '@validator', '@App\Configuration\SystemConfiguration', '@security.user_password_hasher']
|
||||
|
||||
App\Plugin\PluginManager:
|
||||
public: true
|
||||
arguments: [!tagged kimai.plugin]
|
||||
|
||||
@@ -24,11 +24,15 @@ final class <className> extends AbstractMigration
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->abortIf(true, 'Migration was auto-generated, adapt to your needs before running it.');
|
||||
|
||||
<up>
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->abortIf(true, 'Migration was auto-generated, adapt to your needs before running it.');
|
||||
|
||||
<down>
|
||||
}<override>
|
||||
}
|
||||
|
||||
61
migrations/Version20240920105524.php
Normal file
61
migrations/Version20240920105524.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* @version 2.22
|
||||
*/
|
||||
final class Version20240920105524 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Updates the user preferences for configurable work contract type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$ids = $this->connection->fetchFirstColumn("
|
||||
SELECT DISTINCT user_id
|
||||
FROM kimai2_user_preferences AS kp
|
||||
WHERE kp.value > 0
|
||||
AND kp.name IN ('work_monday', 'work_tuesday', 'work_wednesday', 'work_thursday', 'work_friday', 'work_saturday', 'work_sunday')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM kimai2_user_preferences AS kp2
|
||||
WHERE kp2.user_id = kp.user_id
|
||||
AND kp2.name = 'work_contract_type'
|
||||
);");
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$this->addSql('INSERT INTO kimai2_user_preferences (`user_id`, `name`, `value`) VALUES (:id, :name, :value)', [
|
||||
'id' => $id,
|
||||
'name' => 'work_contract_type',
|
||||
'value' => 'day',
|
||||
]);
|
||||
}
|
||||
|
||||
if (\count($ids) === 0) {
|
||||
$this->preventEmptyMigrationWarning();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql("DELETE FROM kimai2_user_preferences WHERE `name` = 'work_contract_type'");
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
641
phpstan.neon
641
phpstan.neon
@@ -34,6 +34,7 @@ parameters:
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
excludePaths:
|
||||
- %rootDir%/../../../src/Ldap/LdapDriver.php
|
||||
- %rootDir%/../../../src/Event/PageActionsEvent.php
|
||||
treatPhpDocTypesAsCertain: false
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
doctrine:
|
||||
@@ -153,16 +154,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/API/UserController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Activity\\\\ActivityStatisticService\\:\\:createStatisticQueryBuilder\\(\\) has parameter \\$activities with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Activity/ActivityStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Activity\\\\ActivityStatisticService\\:\\:getBudgetStatistic\\(\\) should return array\\<int, App\\\\Model\\\\ActivityStatistic\\> but returns array\\<int\\|string, App\\\\Model\\\\ActivityStatistic\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Activity/ActivityStatisticService.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$statistic of class App\\\\Event\\\\ActivityStatisticEvent constructor expects App\\\\Model\\\\ActivityStatistic, App\\\\Model\\\\ActivityStatistic\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -283,11 +274,6 @@ parameters:
|
||||
count: 2
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$customerIDs of method App\\\\Repository\\\\CustomerRepository\\:\\:findByIds\\(\\) expects array\\<int\\>, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$datetime of method App\\\\Timesheet\\\\DateTimeFactory\\:\\:createDateTime\\(\\) expects string, mixed given\\.$#"
|
||||
count: 2
|
||||
@@ -308,11 +294,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$projectIds of method App\\\\Repository\\\\ProjectRepository\\:\\:findByIds\\(\\) expects array\\<int\\>, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function rtrim expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -323,26 +304,11 @@ parameters:
|
||||
count: 2
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$teamIds of method App\\\\Repository\\\\TeamRepository\\:\\:findByIds\\(\\) expects array\\<int\\>, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timezone of class DateTimeZone constructor expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$userIds of method App\\\\Repository\\\\UserRepository\\:\\:findByIds\\(\\) expects array\\<int\\>, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#"
|
||||
count: 4
|
||||
path: src/Command/ExportCreateCommand.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$content of method Symfony\\\\Component\\\\Filesystem\\\\Filesystem\\:\\:dumpFile\\(\\) expects resource\\|string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
@@ -783,11 +749,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\EntityWithMetaFieldsExporter\\:\\:export\\(\\) expects array, iterable\\<App\\\\Entity\\\\Activity\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ActivityController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$replace of method App\\\\Repository\\\\ActivityRepository\\:\\:deleteActivity\\(\\) expects App\\\\Entity\\\\Activity\\|null, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -833,11 +794,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\EntityWithMetaFieldsExporter\\:\\:export\\(\\) expects array, iterable\\<App\\\\Entity\\\\Customer\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/CustomerController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$replace of method App\\\\Repository\\\\CustomerRepository\\:\\:deleteCustomer\\(\\) expects App\\\\Entity\\\\Customer\\|null, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -973,11 +929,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\EntityWithMetaFieldsExporter\\:\\:export\\(\\) expects array, iterable\\<App\\\\Entity\\\\Project\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/ProjectController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$replace of method App\\\\Repository\\\\ProjectRepository\\:\\:deleteProject\\(\\) expects App\\\\Entity\\\\Project\\|null, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -1133,11 +1084,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/TimesheetAbstractController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$page of method App\\\\Controller\\\\TimesheetAbstractController\\:\\:getEditForm\\(\\) expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/TimesheetAbstractController.php
|
||||
|
||||
-
|
||||
message: "#^PHPDoc tag @var for variable \\$teams contains generic class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection but does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
@@ -1413,11 +1359,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Entity/Tag.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Entity\\\\Tag\\:\\:\\$timesheets with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
path: src/Entity/Tag.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method removeMembership\\(\\) on App\\\\Entity\\\\User\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -1548,11 +1489,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Entity/User.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\User\\:\\:getTeams\\(\\) should return iterable\\<App\\\\Entity\\\\Team\\> but returns array\\<int, App\\\\Entity\\\\Team\\|null\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Entity/User.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\User\\:\\:getTimezone\\(\\) should return string but returns bool\\|float\\|int\\|string\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -1663,26 +1599,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Event/CalendarConfigurationEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:addAction\\(\\) has parameter \\$action with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:addActionToSubmenu\\(\\) has parameter \\$action with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:getActions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PageActionsEvent\\:\\:replaceAction\\(\\) has parameter \\$action with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Event/PageActionsEvent.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Event\\\\PermissionsEvent\\:\\:getPermissions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -2148,21 +2064,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/CustomerEditForm.php
|
||||
|
||||
-
|
||||
message: "#^Class App\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformer implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#"
|
||||
count: 1
|
||||
path: src/Form/DataTransformer/DurationStringToSecondsTransformer.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$formatToInt \\(string\\|null\\) of method App\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformer\\:\\:reverseTransform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\<mixed,mixed\\>\\:\\:reverseTransform\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Form/DataTransformer/DurationStringToSecondsTransformer.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$intToFormat \\(int\\) of method App\\\\Form\\\\DataTransformer\\\\DurationStringToSecondsTransformer\\:\\:transform\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Form\\\\DataTransformerInterface\\<mixed,mixed\\>\\:\\:transform\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Form/DataTransformer/DurationStringToSecondsTransformer.php
|
||||
|
||||
-
|
||||
message: "#^Class App\\\\Form\\\\DataTransformer\\\\SearchTermTransformer implements generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#"
|
||||
count: 1
|
||||
@@ -2183,36 +2084,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/DataTransformer/StringToArrayTransformer.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Extension\\\\DocumentationLinkExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/DocumentationLinkExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Extension\\\\EnhancedChoiceTypeExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/EnhancedChoiceTypeExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Extension\\\\IconExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/IconExtension.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getName\\(\\) on Symfony\\\\Component\\\\Form\\\\FormInterface\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/SelectWithApiDataExtension.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getParent\\(\\) on Symfony\\\\Component\\\\Form\\\\FormInterface\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/SelectWithApiDataExtension.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Form\\\\Extension\\\\SelectWithApiDataExtension\\:\\:buildView\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Extension/SelectWithApiDataExtension.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$name of method Symfony\\\\Component\\\\Routing\\\\Generator\\\\UrlGeneratorInterface\\:\\:generate\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -2223,16 +2099,6 @@ parameters:
|
||||
count: 2
|
||||
path: src/Form/Helper/ActivityHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/ActivityHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/ActivityHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
@@ -2248,16 +2114,6 @@ parameters:
|
||||
count: 2
|
||||
path: src/Form/Helper/CustomerHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/CustomerHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/CustomerHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
@@ -2273,16 +2129,6 @@ parameters:
|
||||
count: 2
|
||||
path: src/Form/Helper/ProjectHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/ProjectHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Helper/ProjectHelper.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, bool\\|float\\|int\\|string given\\.$#"
|
||||
count: 1
|
||||
@@ -3338,11 +3184,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Form/Type/SystemConfigurationType.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$teams of method App\\\\Repository\\\\Query\\\\BaseQuery\\:\\:setTeams\\(\\) expects array\\<App\\\\Entity\\\\Team\\>\\|null, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Form/Type/TeamType.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$format of static method DateTime\\:\\:createFromFormat\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -3823,16 +3664,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Model/ActivityStatistic.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Model\\\\BudgetStatisticModel\\:\\:setStatistic\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Model/BudgetStatisticModel.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Model\\\\BudgetStatisticModel\\:\\:setStatisticTotal\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Model/BudgetStatisticModel.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#"
|
||||
count: 1
|
||||
@@ -4058,311 +3889,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Reporting/ProjectDetails/ProjectDetailsModel.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ActivityRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ActivityRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Repository/ActivityRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ActivityRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ActivityRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ActivityRepository\\:\\:deleteActivity\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ActivityRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ActivityRepository\\:\\:getPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ActivityRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ActivityRepository\\:\\:saveActivity\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ActivityRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ActivityRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\BookmarkRepository\\:\\:deleteBookmark\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/BookmarkRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\BookmarkRepository\\:\\:saveBookmark\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/BookmarkRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$string of function mb_substr expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/BookmarkRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method App\\\\Repository\\\\BookmarkRepository\\:\\:clearCache\\(\\) expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/Repository/BookmarkRepository.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Repository\\\\BookmarkRepository\\:\\:\\$userCache type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/BookmarkRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/CustomerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/CustomerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Repository/CustomerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\CustomerRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/CustomerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\CustomerRepository\\:\\:getComments\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/CustomerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\CustomerRepository\\:\\:getPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/CustomerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/CustomerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'counter' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Repository/InvoiceRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\InvoiceRepository\\:\\:addPermissionCriteria\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\InvoiceRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\InvoiceRepository\\:\\:getCounterFor\\(\\) should return int but returns mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/InvoiceRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/ActivityLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results \\(array\\<App\\\\Entity\\\\Activity\\|int\\>\\) of method App\\\\Repository\\\\Loader\\\\ActivityLoader\\:\\:loadResults\\(\\) should be contravariant with parameter \\$results \\(array\\) of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/ActivityLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results \\(array\\<App\\\\Entity\\\\Customer\\|int\\>\\) of method App\\\\Repository\\\\Loader\\\\CustomerLoader\\:\\:loadResults\\(\\) should be contravariant with parameter \\$results \\(array\\) of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/CustomerLoader.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Loader\\\\DefaultLoader\\:\\:loadResults\\(\\) has parameter \\$results with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/DefaultLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results \\(array\\<App\\\\Entity\\\\Invoice\\|int\\>\\) of method App\\\\Repository\\\\Loader\\\\InvoiceLoader\\:\\:loadResults\\(\\) should be contravariant with parameter \\$results \\(array\\) of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/InvoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\) has parameter \\$results with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/LoaderInterface.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/ProjectLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results \\(array\\<App\\\\Entity\\\\Project\\|int\\>\\) of method App\\\\Repository\\\\Loader\\\\ProjectLoader\\:\\:loadResults\\(\\) should be contravariant with parameter \\$results \\(array\\) of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/ProjectLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results \\(array\\<App\\\\Entity\\\\Team\\|int\\>\\) of method App\\\\Repository\\\\Loader\\\\TeamLoader\\:\\:loadResults\\(\\) should be contravariant with parameter \\$results \\(array\\) of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/TeamLoader.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/TimesheetLoader.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Project\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/TimesheetLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results \\(array\\<App\\\\Entity\\\\Timesheet\\|int\\>\\) of method App\\\\Repository\\\\Loader\\\\TimesheetLoader\\:\\:loadResults\\(\\) should be contravariant with parameter \\$results \\(array\\) of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/TimesheetLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results \\(array\\<App\\\\Entity\\\\User\\|int\\>\\) of method App\\\\Repository\\\\Loader\\\\UserLoader\\:\\:loadResults\\(\\) should be contravariant with parameter \\$results \\(array\\) of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/Loader/UserLoader.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Paginator\\\\LoaderPaginator\\:\\:getAll\\(\\) return type has no value type specified in iterable type iterable\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/LoaderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Paginator\\\\LoaderPaginator\\:\\:getNbResults\\(\\) should return int\\<0, max\\> but returns int\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/LoaderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\LoaderInterface\\:\\:loadResults\\(\\) expects array, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/LoaderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Interface App\\\\Repository\\\\Paginator\\\\PaginatorInterface extends generic interface Pagerfanta\\\\Adapter\\\\AdapterInterface but does not specify its types\\: T$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/PaginatorInterface.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Paginator\\\\PaginatorInterface\\:\\:getAll\\(\\) return type has no value type specified in iterable type iterable\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/PaginatorInterface.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Paginator\\\\QueryBuilderPaginator\\:\\:getAll\\(\\) return type has no value type specified in iterable type iterable\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/QueryBuilderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Paginator\\\\QueryBuilderPaginator\\:\\:getNbResults\\(\\) should return int\\<0, max\\> but returns int\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Paginator/QueryBuilderPaginator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:addPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:deleteComment\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:deleteProject\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:getComments\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:getPermissionCriteria\\(\\) has parameter \\$teams with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:getProjectsForQuery\\(\\) should return iterable\\<App\\\\Entity\\\\Project\\> but returns mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:saveComment\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\ProjectRepository\\:\\:saveProject\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\ProjectLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\Project\\|int\\>, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/ProjectRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Query\\\\BaseQuery\\:\\:getOrderGroups\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -4408,46 +3939,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/Query/TimesheetQuery.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'counter' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Result/TimesheetResult.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'duration' on mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Result/TimesheetResult.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Result\\\\TimesheetResult\\:\\:getResults\\(\\) should return array\\<App\\\\Entity\\\\Timesheet\\> but returns mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Result/TimesheetResult.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\Result\\\\TimesheetResult\\:\\:toIterable\\(\\) return type has no value type specified in iterable type iterable\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Result/TimesheetResult.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$count of class App\\\\Repository\\\\Result\\\\TimesheetResultStatistic constructor expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Result/TimesheetResult.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\TimesheetLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\Timesheet\\|int\\>, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Result/TimesheetResult.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$duration of class App\\\\Repository\\\\Result\\\\TimesheetResultStatistic constructor expects int, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Result/TimesheetResult.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Repository\\\\Result\\\\TimesheetResult\\:\\:\\$resultCache \\(array\\<App\\\\Entity\\\\Timesheet\\>\\|null\\) does not accept mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/Result/TimesheetResult.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\RoleRepository\\:\\:deleteRole\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -4458,46 +3949,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/RoleRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TagRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TagRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TeamRepository\\:\\:addPermissionCriteria\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TeamRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TeamRepository\\:\\:deleteTeam\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TeamRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TeamRepository\\:\\:removeTeamMember\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TeamRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TeamRepository\\:\\:saveTeam\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TeamRepository.php
|
||||
|
||||
-
|
||||
message: "#^Return type \\(array\\<App\\\\Entity\\\\Team\\>\\) of method App\\\\Repository\\\\TeamRepository\\:\\:findAll\\(\\) should be covariant with return type \\(array\\<int, App\\\\Entity\\\\Team\\>\\) of method Doctrine\\\\Persistence\\\\ObjectRepository\\<App\\\\Entity\\\\Team\\>\\:\\:findAll\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/TeamRepository.php
|
||||
|
||||
-
|
||||
message: "#^Return type \\(array\\<App\\\\Entity\\\\Team\\>\\) of method App\\\\Repository\\\\TeamRepository\\:\\:findAll\\(\\) should be covariant with return type \\(list\\<App\\\\Entity\\\\Team\\>\\) of method Doctrine\\\\ORM\\\\EntityRepository\\<App\\\\Entity\\\\Team\\>\\:\\:findAll\\(\\)$#"
|
||||
count: 1
|
||||
path: src/Repository/TeamRepository.php
|
||||
|
||||
-
|
||||
message: "#^Argument of an invalid type array\\<string\\>\\|string supplied for foreach, only iterables are supported\\.$#"
|
||||
count: 1
|
||||
@@ -4543,41 +3994,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 2
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot clone DateTime\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TimesheetRepository\\:\\:findTimesheetsById\\(\\) should return array\\<App\\\\Entity\\\\Timesheet\\> but returns iterable\\<App\\\\Entity\\\\Timesheet\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TimesheetRepository\\:\\:getActiveEntries\\(\\) should return array\\<App\\\\Entity\\\\Timesheet\\> but returns iterable\\<App\\\\Entity\\\\Timesheet\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TimesheetRepository\\:\\:getHydratedResultsByQuery\\(\\) should return iterable\\<App\\\\Entity\\\\Timesheet\\> but returns mixed\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TimesheetRepository\\:\\:getRawData\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -4588,11 +4009,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\TimesheetRepository\\:\\:setExported\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$amountThisMonth of method App\\\\Model\\\\TimesheetStatistic\\:\\:setAmountThisMonth\\(\\) expects float\\|int, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -4623,56 +4039,11 @@ parameters:
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\TimesheetLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\Timesheet\\|int\\>, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$amount of class App\\\\Model\\\\Revenue constructor expects float, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/TimesheetRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchFields\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/UserRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/UserRepository.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method hasSearchTerm\\(\\) on App\\\\Utils\\\\SearchTerm\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/UserRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\UserRepository\\:\\:addPermissionCriteria\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/UserRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\UserRepository\\:\\:deleteUser\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/UserRepository.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Repository\\\\UserRepository\\:\\:findByUsername\\(\\) has parameter \\$username with no type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/UserRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$identifier of method App\\\\Repository\\\\UserRepository\\:\\:loadUserByIdentifier\\(\\) expects string, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Repository/UserRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$message of class Symfony\\\\Component\\\\Security\\\\Core\\\\Exception\\\\AuthenticationException constructor expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -5488,16 +4859,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Widget/Type/PaginatedWorkingTimeChart.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\TeamLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\Team\\|int\\>, iterable\\<App\\\\Entity\\\\Team\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/Type/UserTeamProjects.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\UserLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\User\\|int\\>, array\\<int, int\\|null\\> given\\.$#"
|
||||
count: 1
|
||||
path: src/Widget/Type/UserTeams.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Widget\\\\WidgetService\\:\\:\\$widgets type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
public/build/app.3611eec9.js
Normal file
2
public/build/app.3611eec9.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/app.41121fe4.js"
|
||||
"/build/app.3611eec9.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.a7ba9fd6.css"
|
||||
"/build/app.c4fc7cc8.css"
|
||||
]
|
||||
},
|
||||
"app-rtl": {
|
||||
@@ -15,7 +15,7 @@
|
||||
"/build/app-rtl.97153087.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app-rtl.6516df2d.css"
|
||||
"/build/app-rtl.83358c8a.css"
|
||||
]
|
||||
},
|
||||
"export-pdf": {
|
||||
@@ -54,7 +54,7 @@
|
||||
"calendar": {
|
||||
"js": [
|
||||
"/build/runtime.74179306.js",
|
||||
"/build/calendar.bb629e51.js"
|
||||
"/build/calendar.e52c9d34.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/calendar.bb473428.css"
|
||||
@@ -72,10 +72,10 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG",
|
||||
"/build/app.41121fe4.js": "sha384-4v3lVToJIcpMJ++4WaIZ5r9rXR5Vb4waSoHcEzx3vOfcLL+1+TN70di7XPzYSW7I",
|
||||
"/build/app.a7ba9fd6.css": "sha384-GfMYf1vYI9zkkywMoxYk6wpC4UyK185zjDBi16qa0237oztrtOuWdBZsKjWOkyeY",
|
||||
"/build/app.3611eec9.js": "sha384-uBLSZpUnOLWU2TLPGZ0gZDqmiAEa2xrKhWVkHheU+vBjcStIq8ZQg3/iqxLS9Su0",
|
||||
"/build/app.c4fc7cc8.css": "sha384-icbl3mA5CyeJXjF1YzLObStpJHF5qZ5GdqALtvDYN4KPOCqREwGdFWDu27GYkI+U",
|
||||
"/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr",
|
||||
"/build/app-rtl.6516df2d.css": "sha384-5hF5akg98Hnj/bihpbsiTjMyZ3BsAC4+Z1kzqi1O3/U+tgpnQ+JmwsOADDpMld6d",
|
||||
"/build/app-rtl.83358c8a.css": "sha384-eKnlBDnocNJ68nIc2l4AWCLn3oUEEbsRJoZ1TUP3mii+ToGt9xs08ih8wEmvO+Dq",
|
||||
"/build/export-pdf.1442bee7.js": "sha384-C6agvjJnQUsMCxaZ/J7dUZU3cpC7uHKI9ZtnfGpRqYbjyw91YLy1oV3iNYgwYyCF",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
"/build/invoice.7ef8a0c8.js": "sha384-z4lZ1Ig3+NPigrRyGPZoff0gG3n5PnCjaDJ73ATnzdUYk0lOCEtNV9fg35VRD0vG",
|
||||
@@ -83,7 +83,7 @@
|
||||
"/build/invoice-pdf.6091a528.js": "sha384-yJz/MDBgBpNHNxF7DuSyFbJN9EL7oDJjraoPRB7VYlHMdb1T0ZfTcI0L8VMDB46U",
|
||||
"/build/invoice-pdf.2b749265.css": "sha384-DXXgkz2WWnrWnfBnXX5fmfPQSPb98upMnWxYKwTGYS04EhrPIWfDCutB2unIrWh7",
|
||||
"/build/chart.80ee190c.js": "sha384-KrQszq859PR0u+KHpGy+RO6K1rcMqCFZx+sNiLVNVZHwEi7RwTXqnYbu8zvfWzUI",
|
||||
"/build/calendar.bb629e51.js": "sha384-uihkC7VSrD8l4gSiveJxM2g8dLFCR9+VQwVmcIb2LlNmA2PKDQ/uV0TN5gzmgpo+",
|
||||
"/build/calendar.e52c9d34.js": "sha384-a6P/nyvUkmPZglNqIfSKEyon791oFKB2+zgfUZHpy1awKi/FBchr5TqeRYupRDQn",
|
||||
"/build/calendar.bb473428.css": "sha384-900W9o8666Qpw21rLP7hn5Ql8tWd40k0IcE3QpFm7E7V6bKva7xQgGlyzVuuh4GK",
|
||||
"/build/dashboard.10118fb0.js": "sha384-pasZR1GYf8/7lMFS9OrsxX8nJ9tSlRYr32fWKUhGoF2mNAGrokLEWdgtbYMRHgFB",
|
||||
"/build/dashboard.18f5a8b7.css": "sha384-PBD8ftb2yBoSjRe2sN5Xb4dEz045kOEUfcIufdSis+tWoWdAx5j4Yic+F/6CYbrP"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"build/app.css": "/build/app.a7ba9fd6.css",
|
||||
"build/app.js": "/build/app.41121fe4.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.6516df2d.css",
|
||||
"build/app.css": "/build/app.c4fc7cc8.css",
|
||||
"build/app.js": "/build/app.3611eec9.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.83358c8a.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.97153087.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.1442bee7.js",
|
||||
@@ -11,7 +11,7 @@
|
||||
"build/invoice-pdf.js": "/build/invoice-pdf.6091a528.js",
|
||||
"build/chart.js": "/build/chart.80ee190c.js",
|
||||
"build/calendar.css": "/build/calendar.bb473428.css",
|
||||
"build/calendar.js": "/build/calendar.bb629e51.js",
|
||||
"build/calendar.js": "/build/calendar.e52c9d34.js",
|
||||
"build/dashboard.css": "/build/dashboard.18f5a8b7.css",
|
||||
"build/dashboard.js": "/build/dashboard.10118fb0.js",
|
||||
"build/runtime.js": "/build/runtime.74179306.js",
|
||||
|
||||
@@ -4,6 +4,9 @@ use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
// TODO remove once PARTIAL usage was replaced entirely
|
||||
\Doctrine\Deprecations\Deprecation::ignoreDeprecations('https://github.com/doctrine/orm/issues/8471');
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
|
||||
@@ -62,13 +62,13 @@ final class ActivityController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name|project', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name, project (default: name)')]
|
||||
#[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: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, ProjectRepository $projectRepository): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($user);
|
||||
$query->loadTeams();
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
if (\is_string($order) && $order !== '') {
|
||||
@@ -109,9 +109,9 @@ final class ActivityController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getActivitiesForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\Pagination;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
@@ -52,6 +53,46 @@ abstract class BaseApiController extends AbstractController
|
||||
return DateTimeFactory::createByUser($user);
|
||||
}
|
||||
|
||||
protected function prepareQuery(BaseQuery $query, ParamFetcherInterface $paramFetcher): void
|
||||
{
|
||||
$query->setIsApiCall(true);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
// there is no function has() in ParamFetcherInterface, so we need to use all() and check for the key
|
||||
$all = $paramFetcher->all(true);
|
||||
|
||||
if (\array_key_exists('page', $all)) {
|
||||
$page = $all['page'];
|
||||
if (is_numeric($page)) {
|
||||
$query->setPage((int) $page);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('size', $all)) {
|
||||
$size = $all['size'];
|
||||
if (is_numeric($size)) {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('pageSize', $all)) {
|
||||
$size = $all['pageSize'];
|
||||
if (is_numeric($size)) {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createPaginatedView(Pagination $pagination): View
|
||||
{
|
||||
$results = (array) $pagination->getCurrentPageResults();
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $pagination);
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
protected function addPagination(View $view, Pagination $pagination): void
|
||||
{
|
||||
$view->setHeader('X-Page', (string) $pagination->getCurrentPage());
|
||||
|
||||
@@ -59,13 +59,13 @@ final class CustomerController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name (default: name)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($user);
|
||||
$query->loadTeams();
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
if (\is_string($order) && $order !== '') {
|
||||
@@ -87,9 +87,9 @@ final class CustomerController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getCustomersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
106
src/API/InvoiceController.php
Normal file
106
src/API/InvoiceController.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?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\API;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\InvoiceRepository;
|
||||
use App\Repository\Query\InvoiceArchiveQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
|
||||
#[Route(path: '/invoices')]
|
||||
#[IsGranted('API')]
|
||||
#[OA\Tag(name: 'Invoice')]
|
||||
final class InvoiceController extends BaseApiController
|
||||
{
|
||||
public const GROUPS_ENTITY = ['Default', 'Entity', 'Invoice', 'Invoice_Entity'];
|
||||
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Invoice'];
|
||||
|
||||
public function __construct(
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly InvoiceRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a paginated collection of invoices.
|
||||
*
|
||||
* Needs permission: view_invoice
|
||||
*/
|
||||
#[IsGranted('view_invoice')]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of invoices', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/InvoiceCollection')))]
|
||||
#[Route(methods: ['GET'], path: '', name: 'get_invoices')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'end', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records before this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
#[Rest\QueryParam(name: 'customers', map: true, requirements: '\d+', strict: true, nullable: true, default: [], description: 'List of customer IDs to filter, e.g.: customers[]=1&customers[]=2')]
|
||||
#[Rest\QueryParam(name: 'status', map: true, requirements: 'pending|paid|canceled|new', strict: true, nullable: true, default: [], description: 'Invoice status: pending, paid, canceled, new. Default: all')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository): Response
|
||||
{
|
||||
$query = new InvoiceArchiveQuery();
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
$factory = $this->getDateTimeFactory();
|
||||
|
||||
$begin = $paramFetcher->get('begin');
|
||||
if (\is_string($begin) && $begin !== '') {
|
||||
$query->setBegin($factory->createDateTime($begin));
|
||||
}
|
||||
|
||||
$end = $paramFetcher->get('end');
|
||||
if (\is_string($end) && $end !== '') {
|
||||
$query->setEnd($factory->createDateTime($end));
|
||||
}
|
||||
|
||||
/** @var array<string> $status */
|
||||
$status = $paramFetcher->get('status');
|
||||
if (\is_array($status)) {
|
||||
foreach ($status as $s) {
|
||||
$query->addStatus($s);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<int> $customers */
|
||||
$customers = $paramFetcher->get('customers');
|
||||
foreach ($customerRepository->findByIds(array_unique($customers)) as $customer) {
|
||||
$query->addCustomer($customer);
|
||||
}
|
||||
|
||||
$data = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($data);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one invoice.
|
||||
*
|
||||
* Needs permission: view_invoice
|
||||
*/
|
||||
#[IsGranted('view_invoice')]
|
||||
#[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))]
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_invoice', requirements: ['id' => '\d+'])]
|
||||
public function getAction(Invoice $invoice): Response
|
||||
{
|
||||
$view = new View($invoice, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
@@ -68,13 +68,13 @@ final class ProjectController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
|
||||
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name|customer', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name, customer (default: name)')]
|
||||
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($user);
|
||||
$query->loadTeams();
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$order = $paramFetcher->get('order');
|
||||
if (\is_string($order) && $order !== '') {
|
||||
@@ -141,9 +141,9 @@ final class ProjectController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getProjectsForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
@@ -46,6 +46,7 @@ final class TagController extends BaseApiController
|
||||
#[Rest\QueryParam(name: 'name', strict: true, nullable: true, description: 'Search term to filter tag list')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
// not using a query, as these are too expensive to fetch, the simple "name" list is easier to use
|
||||
$filter = $paramFetcher->get('name');
|
||||
|
||||
$data = $this->repository->findAllTagNames($filter);
|
||||
@@ -57,7 +58,7 @@ final class TagController extends BaseApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch tags by filter as entities
|
||||
* Fetch tags by filter (as full entities)
|
||||
*/
|
||||
#[OA\Response(response: 200, description: 'Find the collection of all matching tags', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/TagEntity')))]
|
||||
#[Route(path: '/find', name: 'get_tags_full', methods: ['GET'])]
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Form\API\TeamApiEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
@@ -53,7 +54,10 @@ final class TeamController extends BaseApiController
|
||||
#[Route(methods: ['GET'], path: '', name: 'get_teams')]
|
||||
public function cgetAction(): Response
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
$query = new TeamQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$data = $this->repository->getTeamsForQuery($query);
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
|
||||
|
||||
@@ -96,7 +96,7 @@ final class TimesheetController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||
{
|
||||
$query = new TimesheetQuery(false);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
$seeAll = false;
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
@@ -168,16 +168,6 @@ final class TimesheetController extends BaseApiController
|
||||
$query->addActivity($activity);
|
||||
}
|
||||
|
||||
$page = $paramFetcher->get('page');
|
||||
if (\is_string($page) && $page !== '') {
|
||||
$query->setPage((int) $page);
|
||||
}
|
||||
|
||||
$size = $paramFetcher->get('size');
|
||||
if (\is_string($size) && $size !== '') {
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
|
||||
/** @var array<string> $tags */
|
||||
$tags = $paramFetcher->get('tags');
|
||||
if (\is_array($tags) && \count($tags) > 0) {
|
||||
@@ -248,12 +238,8 @@ final class TimesheetController extends BaseApiController
|
||||
$query->setModifiedAfter($factory->createDateTime($modifiedAfter));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getPagerfantaForQuery($query);
|
||||
$results = (array) $data->getCurrentPageResults();
|
||||
|
||||
$view = new View($results, 200);
|
||||
$this->addPagination($view, $data);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
|
||||
@@ -57,10 +57,12 @@ final class UserController extends BaseApiController
|
||||
#[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)')]
|
||||
#[Rest\QueryParam(name: 'page', requirements: '\d+', strict: true, nullable: true, description: 'The page to display, renders a 404 if not found (default: 1)')]
|
||||
#[Rest\QueryParam(name: 'size', requirements: '\d+', strict: true, nullable: true, description: 'The amount of entries for each page (default: 50)')]
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$this->prepareQuery($query, $paramFetcher);
|
||||
|
||||
$visible = $paramFetcher->get('visible');
|
||||
if (\is_string($visible) && $visible !== '') {
|
||||
@@ -82,9 +84,8 @@ final class UserController extends BaseApiController
|
||||
$query->setSearchTerm(new SearchTerm($term));
|
||||
}
|
||||
|
||||
$query->setIsApiCall(true);
|
||||
$data = $this->repository->getUsersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$pagination = $this->repository->getPagerfantaForQuery($query);
|
||||
$view = $this->createPaginatedView($pagination);
|
||||
|
||||
$full = $paramFetcher->get('full');
|
||||
if ($full === '1' || $full === 'true') {
|
||||
|
||||
@@ -117,7 +117,7 @@ class ActivityStatisticService
|
||||
|
||||
/**
|
||||
* @param Activity[] $activities
|
||||
* @return array<int, ActivityStatistic>
|
||||
* @return array<int|string, ActivityStatistic>
|
||||
*/
|
||||
private function getBudgetStatistic(array $activities, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): array
|
||||
{
|
||||
@@ -159,6 +159,9 @@ class ActivityStatisticService
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity[] $activities
|
||||
*/
|
||||
private function createStatisticQueryBuilder(array $activities, \DateTimeInterface $begin = null, ?\DateTimeInterface $end = null): QueryBuilder
|
||||
{
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
|
||||
@@ -106,25 +106,25 @@ final class ExportCreateCommand extends Command
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
$dateFactory = new DateTimeFactory($timezone);
|
||||
|
||||
$customerIDs = $input->getOption('customer');
|
||||
$customerIDs = $this->optionToIntArray($input, 'customer');
|
||||
$customers = [];
|
||||
if (\count($customerIDs) > 0) {
|
||||
$customers = $this->customerRepository->findByIds($customerIDs);
|
||||
}
|
||||
|
||||
$projectIDs = $input->getOption('project');
|
||||
$projectIDs = $this->optionToIntArray($input, 'project');
|
||||
$projects = [];
|
||||
if (\count($projectIDs) > 0) {
|
||||
$projects = $this->projectRepository->findByIds($projectIDs);
|
||||
}
|
||||
|
||||
$teamIDs = $input->getOption('team');
|
||||
$teamIDs = $this->optionToIntArray($input, 'team');
|
||||
$teams = [];
|
||||
if (\count($teamIDs) > 0) {
|
||||
$teams = $this->teamRepository->findByIds($teamIDs);
|
||||
}
|
||||
|
||||
$userIDs = $input->getOption('user');
|
||||
$userIDs = $this->optionToIntArray($input, 'user');
|
||||
$users = [];
|
||||
if (\count($userIDs) > 0) {
|
||||
$users = $this->userRepository->findByIds($userIDs);
|
||||
@@ -287,6 +287,25 @@ final class ExportCreateCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
private function optionToIntArray(InputInterface $input, string $name): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$options = $input->getOption($name);
|
||||
if (\is_array($options) && \count($options) > 0) {
|
||||
foreach ($options as $option) {
|
||||
if (is_numeric($option)) {
|
||||
$results[] = (int) $option;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function savePreview(Response $response, string $directory): string
|
||||
{
|
||||
$filename = uniqid('invoice_');
|
||||
|
||||
@@ -39,13 +39,19 @@ final class RegenerateLocalesCommand extends Command
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $noRegionCode = ['ar', 'id', 'pa', 'sl'];
|
||||
private array $noRegionCode = ['ar', 'id', 'pa', 'sl', 'ca'];
|
||||
/**
|
||||
* A list of locales that will be activated, no matter if translation files exist for them.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $addLocaleToList = ['zh_Hant_TW'];
|
||||
/**
|
||||
* A list of locales that will NOT be activated, as no translations exist by now.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $skipLocale = ['ca'];
|
||||
|
||||
public function __construct(
|
||||
private readonly string $projectDirectory,
|
||||
@@ -78,7 +84,11 @@ final class RegenerateLocalesCommand extends Command
|
||||
}
|
||||
$firstLevelLocales = [];
|
||||
foreach ($translationFilenames as $file) {
|
||||
$firstLevelLocales[] = explode('.', basename($file))[1];
|
||||
$l = explode('.', basename($file))[1];
|
||||
if (\in_array($l, $this->skipLocale, true)) {
|
||||
continue;
|
||||
}
|
||||
$firstLevelLocales[] = $l;
|
||||
}
|
||||
$firstLevelLocales = array_unique(array_merge($firstLevelLocales, $this->addLocaleToList));
|
||||
$io->title('First level locales found');
|
||||
@@ -185,8 +195,8 @@ final class RegenerateLocalesCommand extends Command
|
||||
|
||||
// in the future this list should be reduced to the list of available translations, but for a long time users
|
||||
// could choose from the entire list of all locales, so we likely have to keep that forever ...
|
||||
$io->title('List of app_locales for services.yaml');
|
||||
$io->writeln(implode('|', $locales));
|
||||
$io->title('List of "kimai_locales" for services.yaml');
|
||||
$io->writeln("['" . implode("', '", $locales) . "']");
|
||||
|
||||
ksort($appLocales);
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.20.0';
|
||||
public const VERSION = '2.22.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 22000;
|
||||
public const VERSION_ID = 22200;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -51,7 +51,12 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[Route(path: '/admin/activity')]
|
||||
final class ActivityController extends AbstractController
|
||||
{
|
||||
public function __construct(private ActivityRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ActivityService $activityService)
|
||||
public function __construct(
|
||||
private readonly ActivityRepository $repository,
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ActivityService $activityService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -61,6 +66,7 @@ final class ActivityController extends AbstractController
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
@@ -443,7 +449,6 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ActivityQuery $query
|
||||
* @return FormInterface<ActivityQuery>
|
||||
*/
|
||||
private function getToolbarForm(ActivityQuery $query): FormInterface
|
||||
@@ -456,7 +461,7 @@ final class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface<ActivityEditForm>
|
||||
* @return FormInterface<mixed>
|
||||
*/
|
||||
private function createEditForm(Activity $activity): FormInterface
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ final class ContractController extends AbstractController
|
||||
|
||||
/** @var User $profile */
|
||||
$profile = $values->getUser();
|
||||
if ($this->getUser() !== $profile && !$canChangeUser) {
|
||||
if (!$this->isGranted('hours', $profile)) {
|
||||
throw $this->createAccessDeniedException('Cannot access user contract settings');
|
||||
}
|
||||
|
||||
@@ -79,9 +79,10 @@ final class ContractController extends AbstractController
|
||||
|
||||
$boxConfiguration = new BoxConfiguration();
|
||||
$boxConfiguration->setDecimal(false);
|
||||
$boxConfiguration->setCollapsed($profile->hasWorkHourConfiguration() && $summary->count() > 0);
|
||||
$boxConfiguration->setCollapsed($summary->count() > 0);
|
||||
|
||||
return $this->render('contract/status.html.twig', [
|
||||
'withWorkHourConfiguration' => $profile->hasWorkHourConfiguration(),
|
||||
'box_configuration' => $boxConfiguration,
|
||||
'page_setup' => $page,
|
||||
'decimal' => $boxConfiguration->isDecimal(),
|
||||
|
||||
@@ -54,7 +54,10 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[Route(path: '/admin/customer')]
|
||||
final class CustomerController extends AbstractController
|
||||
{
|
||||
public function __construct(private CustomerRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
public function __construct(
|
||||
private readonly CustomerRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -64,6 +67,7 @@ final class CustomerController extends AbstractController
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
@@ -126,7 +130,6 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
private function findMetaColumns(CustomerQuery $query): array
|
||||
@@ -539,7 +542,7 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface<CustomerComment>
|
||||
* @return FormInterface<mixed>
|
||||
*/
|
||||
private function getCommentForm(CustomerComment $comment): FormInterface
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ final class FavoriteController extends AbstractController
|
||||
{
|
||||
$favoriteRecordService->addFavorite($timesheet);
|
||||
|
||||
return $this->render('favorite/index.html.twig');
|
||||
return $this->redirectToRoute('favorites_timesheets');
|
||||
}
|
||||
|
||||
#[Route(path: '/timesheet/remove/{id}', name: 'favorites_timesheets_remove', methods: ['GET'])]
|
||||
@@ -41,6 +41,6 @@ final class FavoriteController extends AbstractController
|
||||
{
|
||||
$favoriteRecordService->removeFavorite($timesheet);
|
||||
|
||||
return $this->render('favorite/index.html.twig');
|
||||
return $this->redirectToRoute('favorites_timesheets');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,14 @@ final class PermissionController extends AbstractController
|
||||
{
|
||||
public const TOKEN_NAME = 'user_role_permissions';
|
||||
|
||||
public function __construct(private RolePermissionManager $manager, private RoleRepository $roleRepository)
|
||||
public function __construct(
|
||||
private readonly RolePermissionManager $manager,
|
||||
private readonly RoleRepository $roleRepository
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route(path: '', name: 'admin_user_permissions', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('role_permissions')]
|
||||
public function permissions(EventDispatcherInterface $dispatcher, CsrfTokenManagerInterface $csrfTokenManager, RoleService $roleService, UserRepository $userRepository): Response
|
||||
{
|
||||
$all = $this->roleRepository->findAll();
|
||||
@@ -167,7 +169,6 @@ final class PermissionController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route(path: '/roles/create', name: 'admin_user_roles', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('role_permissions')]
|
||||
public function createRole(Request $request): Response
|
||||
{
|
||||
$role = new Role();
|
||||
@@ -200,8 +201,7 @@ final class PermissionController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/roles/{id}/delete/{csrfToken}', name: 'admin_user_role_delete', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('role_permissions')]
|
||||
#[Route(path: '/roles/{role}/delete/{csrfToken}', name: 'admin_user_role_delete', methods: ['GET', 'POST'])]
|
||||
public function deleteRole(Role $role, string $csrfToken, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {
|
||||
@@ -230,8 +230,7 @@ final class PermissionController extends AbstractController
|
||||
return $this->redirectToRoute('admin_user_permissions');
|
||||
}
|
||||
|
||||
#[Route(path: '/roles/{id}/{name}/{value}/{csrfToken}', name: 'admin_user_permission_save', methods: ['POST'])]
|
||||
#[IsGranted('role_permissions')]
|
||||
#[Route(path: '/roles/{role}/{name}/{value}/{csrfToken}', name: 'admin_user_permission_save', methods: ['POST'])]
|
||||
public function savePermission(Role $role, string $name, bool $value, string $csrfToken, PermissionService $permissionService, CsrfTokenManagerInterface $csrfTokenManager): Response
|
||||
{
|
||||
if (!$this->isCsrfTokenValid(self::TOKEN_NAME, $csrfToken)) {
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Form\AccessTokenForm;
|
||||
use App\Form\Model\TotpActivation;
|
||||
use App\Form\Model\UserContractModel;
|
||||
use App\Form\UserApiPasswordType;
|
||||
use App\Form\UserContractType;
|
||||
use App\Form\UserEditType;
|
||||
@@ -267,7 +268,7 @@ final class ProfileController extends AbstractController
|
||||
#[IsGranted('contract', 'profile')]
|
||||
public function contractAction(User $profile, Request $request, UserRepository $userRepository): Response
|
||||
{
|
||||
$form = $this->createForm(UserContractType::class, $profile, [
|
||||
$form = $this->createForm(UserContractType::class, new UserContractModel($profile), [
|
||||
'action' => $this->generateUrl('user_profile_contract', ['username' => $profile->getUserIdentifier()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
@@ -73,6 +73,7 @@ final class ProjectController extends AbstractController
|
||||
public function indexAction(int $page, Request $request): Response
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->loadTeams();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ final class QuickEntryController extends AbstractController
|
||||
$result = $this->repository->getTimesheetResult($query);
|
||||
|
||||
$rows = [];
|
||||
foreach ($result->getResults(true) as $timesheet) {
|
||||
foreach ($result->getResults() as $timesheet) {
|
||||
$i = 0;
|
||||
$id = $timesheet->getProject()->getId() . '_' . $timesheet->getActivity()->getId();
|
||||
$day = $timesheet->getBegin()->format('Y-m-d');
|
||||
|
||||
@@ -19,7 +19,6 @@ use App\Reporting\YearByUser\YearByUser;
|
||||
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;
|
||||
@@ -31,11 +30,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('report:user')]
|
||||
final class UserYearController extends AbstractUserReportController
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route(path: '/year', name: 'report_user_year', methods: ['GET', 'POST'])]
|
||||
public function yearByUser(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
|
||||
@@ -61,9 +61,8 @@ final class TeamController extends AbstractController
|
||||
$table->setReloadEvents('kimai.teamUpdate');
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('teamlead', ['class' => 'd-none badges', 'orderBy' => false]);
|
||||
$table->addColumn('teamlead_avatar', ['title' => 'team.member', 'translation_domain' => 'teams', 'class' => 'd-none d-lg-table-cell avatars avatar-list avatar-list-stacked', 'orderBy' => false]);
|
||||
$table->addColumn('user', ['class' => 'd-none badges', 'orderBy' => false, 'title' => 'user']);
|
||||
$table->addColumn('avatar', ['title' => 'team.member', 'translation_domain' => 'teams', 'class' => 'd-none d-sm-table-cell avatars avatar-list avatar-list-stacked', 'orderBy' => false]);
|
||||
$table->addColumn('amount', ['title' => 'amount', 'class' => 'd-sm-none text-center', 'orderBy' => false]);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = new PageSetup('teams');
|
||||
|
||||
@@ -36,6 +36,7 @@ use App\Utils\PageSetup;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
@@ -144,7 +145,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
$editForm = $this->getEditForm($entry, $request->get('page'));
|
||||
$page = $request->get('page');
|
||||
$page = is_numeric($page) ? (int) $page : 1;
|
||||
$editForm = $this->getEditForm($entry, $page);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
@@ -267,7 +270,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
if (null === $exporter) {
|
||||
$form->addError(new FormError('Invalid timesheet exporter given'));
|
||||
} else {
|
||||
return $exporter->render($entries->getResults(true), $query);
|
||||
return $exporter->render($entries->getResults(), $query);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,6 +487,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<FormTypeInterface> $formClass
|
||||
*/
|
||||
protected function generateCreateForm(Timesheet $entry, string $formClass, string $action): FormInterface
|
||||
{
|
||||
$mode = $this->getTrackingMode();
|
||||
@@ -504,12 +510,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param int $page
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getEditForm(Timesheet $entry, $page): FormInterface
|
||||
private function getEditForm(Timesheet $entry, int $page): FormInterface
|
||||
{
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
@@ -568,6 +569,9 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
return 'edit_rate_own_timesheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<FormTypeInterface>
|
||||
*/
|
||||
protected function getEditFormClassName(): string
|
||||
{
|
||||
return TimesheetEditForm::class;
|
||||
|
||||
@@ -107,7 +107,9 @@ final class AppExtension extends Extension
|
||||
|
||||
private function setLanguageFormats(ContainerBuilder $container): void
|
||||
{
|
||||
$locales = explode('|', $container->getParameter('app_locales'));
|
||||
$locales = $container->getParameter('kimai_locales');
|
||||
// @deprecated since 2.21.0
|
||||
$container->setParameter('app_locales', implode('|', $locales));
|
||||
|
||||
$directory = $container->getParameter('kernel.project_dir');
|
||||
$settings = include $directory . DIRECTORY_SEPARATOR . 'config/locales.php';
|
||||
|
||||
@@ -17,7 +17,7 @@ use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
final class Date extends FunctionNode
|
||||
{
|
||||
private Node|string|null $value;
|
||||
private Node|string $value;
|
||||
|
||||
public function getSql(SqlWalker $sqlWalker): string
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
final class Day extends FunctionNode
|
||||
{
|
||||
private Node|string|null $value;
|
||||
private Node|string $value;
|
||||
|
||||
public function getSql(SqlWalker $sqlWalker): string
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
final class Month extends FunctionNode
|
||||
{
|
||||
private Node|string|null $value;
|
||||
private Node|string $value;
|
||||
|
||||
public function getSql(SqlWalker $sqlWalker): string
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
final class Year extends FunctionNode
|
||||
{
|
||||
private Node|string|null $value;
|
||||
private Node|string $value;
|
||||
|
||||
public function getSql(SqlWalker $sqlWalker): string
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\CustomerRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'teams', 'comment', 'billable'])]
|
||||
#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'comment', 'billable'])]
|
||||
#[Constraints\Customer]
|
||||
class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('invoiceNumber')]
|
||||
#[UniqueEntity('invoiceFilename')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Exporter\Order(['id', 'createdAt', 'invoiceNumber', 'status', 'customer', 'subtotal', 'total', 'tax', 'currency', 'vat', 'dueDays', 'dueDate', 'paymentDate', 'user', 'invoiceFilename', 'customerNumber', 'comment'])]
|
||||
#[Exporter\Expose(name: 'customer', label: 'customer', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()')]
|
||||
#[Exporter\Expose(name: 'customerNumber', label: 'number', exp: 'object.getCustomer() === null ? null : object.getCustomer().getNumber()')]
|
||||
@@ -44,56 +45,78 @@ class Invoice implements EntityWithMetaFields
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\Column(name: 'invoice_number', type: 'string', length: 50, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'invoice.number', type: 'string')]
|
||||
private ?string $invoiceNumber = null;
|
||||
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?Customer $customer = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'date', type: 'datetime')]
|
||||
private ?\DateTime $createdAt = null;
|
||||
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)]
|
||||
private ?string $timezone = null;
|
||||
#[ORM\Column(name: 'total', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'total_rate', type: 'float')]
|
||||
private float $total = 0.00;
|
||||
#[ORM\Column(name: 'tax', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'invoice.tax', type: 'float')]
|
||||
private float $tax = 0.00;
|
||||
#[ORM\Column(name: 'currency', type: 'string', length: 3, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(max: 3)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'currency', type: 'string')]
|
||||
private ?string $currency = null;
|
||||
#[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Range(min: 0, max: 999)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Exporter\Expose(label: 'due_days', type: 'integer')]
|
||||
private int $dueDays = 30;
|
||||
#[ORM\Column(name: 'vat', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Range(min: 0.0, max: 99.99)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'tax_rate', type: 'float')]
|
||||
private float $vat = 0.00;
|
||||
#[ORM\Column(name: 'status', type: 'string', length: 20, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'status', type: 'string')]
|
||||
private string $status = self::STATUS_NEW;
|
||||
#[ORM\Column(name: 'invoice_filename', type: 'string', length: 150, nullable: false)]
|
||||
@@ -103,6 +126,8 @@ class Invoice implements EntityWithMetaFields
|
||||
private ?string $invoiceFilename = null;
|
||||
private bool $localized = false;
|
||||
#[ORM\Column(name: 'payment_date', type: 'date', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?\DateTime $paymentDate = null;
|
||||
/**
|
||||
* Meta fields registered with the invoice
|
||||
@@ -191,7 +216,6 @@ class Invoice implements EntityWithMetaFields
|
||||
public function setModel(InvoiceModel $model): Invoice
|
||||
{
|
||||
$template = $model->getTemplate();
|
||||
|
||||
if ($template === null) {
|
||||
throw new \InvalidArgumentException('Missing invoice template');
|
||||
}
|
||||
@@ -200,19 +224,25 @@ class Invoice implements EntityWithMetaFields
|
||||
throw new \InvalidArgumentException('Missing due-days or vat setting');
|
||||
}
|
||||
|
||||
$this->customer = $model->getCustomer();
|
||||
$this->user = $model->getUser();
|
||||
$this->total = $model->getCalculator()->getTotal();
|
||||
$this->tax = $model->getCalculator()->getTax();
|
||||
$this->invoiceNumber = $model->getInvoiceNumber();
|
||||
$this->currency = $model->getCurrency();
|
||||
$customer = $model->getCustomer();
|
||||
if ($customer === null) {
|
||||
throw new \InvalidArgumentException('Missing invoice customer');
|
||||
}
|
||||
|
||||
$createdAt = $model->getInvoiceDate();
|
||||
$this->createdAt = \DateTime::createFromInterface($createdAt);
|
||||
$this->timezone = $createdAt->getTimezone()->getName();
|
||||
$user = $model->getUser();
|
||||
if ($user === null) {
|
||||
throw new \InvalidArgumentException('Missing invoice user');
|
||||
}
|
||||
|
||||
$this->dueDays = $template->getDueDays();
|
||||
$this->vat = $template->getVat();
|
||||
$this->setCustomer($customer);
|
||||
$this->setUser($user);
|
||||
$this->setTotal($model->getCalculator()->getTotal());
|
||||
$this->setTax($model->getCalculator()->getTax());
|
||||
$this->setInvoiceNumber($model->getInvoiceNumber());
|
||||
$this->setCurrency($model->getCurrency());
|
||||
$this->setCreatedAt($model->getInvoiceDate());
|
||||
$this->setDueDays($template->getDueDays());
|
||||
$this->setVat($template->getVat());
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -380,6 +410,52 @@ class Invoice implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setVat(float $vat): void
|
||||
{
|
||||
$this->vat = $vat;
|
||||
}
|
||||
|
||||
public function setInvoiceNumber(string $invoiceNumber): void
|
||||
{
|
||||
$this->invoiceNumber = $invoiceNumber;
|
||||
}
|
||||
|
||||
public function setCustomer(Customer $customer): void
|
||||
{
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTimeInterface $createdAt): void
|
||||
{
|
||||
$this->createdAt = \DateTime::createFromInterface($createdAt);
|
||||
$this->timezone = $createdAt->getTimezone()->getName();
|
||||
}
|
||||
|
||||
public function setTotal(float $total): void
|
||||
{
|
||||
$this->total = $total;
|
||||
}
|
||||
|
||||
public function setTax(float $tax): void
|
||||
{
|
||||
$this->tax = $tax;
|
||||
}
|
||||
|
||||
public function setCurrency(string $currency): void
|
||||
{
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
public function setDueDays(int $dueDays): void
|
||||
{
|
||||
$this->dueDays = $dueDays;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->id) {
|
||||
|
||||
@@ -26,7 +26,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Serializer\VirtualProperty('CustomerName', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()', options: [new Serializer\SerializedName('parentTitle'), new Serializer\Type(name: 'string'), new Serializer\Groups(['Project'])])]
|
||||
#[Serializer\VirtualProperty('CustomerAsId', exp: 'object.getCustomer() === null ? null : object.getCustomer().getId()', options: [new Serializer\SerializedName('customer'), new Serializer\Type(name: 'integer'), new Serializer\Groups(['Project', 'Team', 'Not_Expanded'])])]
|
||||
#[Exporter\Order(['id', 'name', 'customer', 'orderNumber', 'orderDate', 'start', 'end', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'teams', 'comment', 'billable', 'number'])]
|
||||
#[Exporter\Order(['id', 'name', 'customer', 'orderNumber', 'orderDate', 'start', 'end', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'comment', 'billable', 'number'])]
|
||||
#[Exporter\Expose(name: 'customer', label: 'customer', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()')]
|
||||
#[Constraints\Project]
|
||||
class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Utils\Color;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
@@ -53,17 +51,8 @@ class Tag
|
||||
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* This is ONLY here, so we can count the amount of timesheets.
|
||||
*
|
||||
* @var Collection<Timesheet>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Timesheet::class, mappedBy: 'tags', fetch: 'EXTRA_LAZY')]
|
||||
private Collection $timesheets;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->timesheets = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Validator\Constraints as Constraints;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -20,7 +21,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Table(name: 'kimai2_teams')]
|
||||
#[ORM\UniqueConstraint(columns: ['name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\TeamRepository')]
|
||||
#[ORM\Entity(repositoryClass: TeamRepository::class)]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('name')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
@@ -310,6 +311,7 @@ class Team
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return Collection<Customer>
|
||||
*/
|
||||
public function getCustomers(): iterable
|
||||
@@ -343,6 +345,7 @@ class Team
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return Collection<Project>
|
||||
*/
|
||||
public function getProjects(): iterable
|
||||
@@ -376,6 +379,7 @@ class Team
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return Collection<Activity>
|
||||
*/
|
||||
public function getActivities(): iterable
|
||||
|
||||
@@ -200,7 +200,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
#[ORM\JoinTable(name: 'kimai2_timesheet_tags')]
|
||||
#[ORM\JoinColumn(name: 'timesheet_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'timesheets', cascade: ['persist'])]
|
||||
#[ORM\ManyToMany(targetEntity: Tag::class, cascade: ['persist'])]
|
||||
#[Assert\Valid]
|
||||
private Collection $tags;
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Entity;
|
||||
use App\Export\Annotation as Exporter;
|
||||
use App\Utils\StringHelper;
|
||||
use App\Validator\Constraints as Constraints;
|
||||
use App\WorkingTime\Mode\WorkingTimeModeNone;
|
||||
use DateTime;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -128,7 +129,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
* @var Collection<UserPreference>|null
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserPreference::class, cascade: ['persist'])]
|
||||
private ?Collection $preferences = null;
|
||||
private ?Collection $preferences;
|
||||
/**
|
||||
* List of all team memberships.
|
||||
*
|
||||
@@ -663,11 +664,13 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
#[Serializer\SerializedName('teams')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
public function getTeams(): iterable
|
||||
public function getTeams(): array
|
||||
{
|
||||
$teams = [];
|
||||
foreach ($this->memberships as $membership) {
|
||||
$teams[] = $membership->getTeam();
|
||||
if ($membership->getTeam() !== null) {
|
||||
$teams[] = $membership->getTeam();
|
||||
}
|
||||
}
|
||||
|
||||
return $teams;
|
||||
@@ -1115,9 +1118,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
$initial = mb_substr($initial, 0, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
$initial = mb_strtoupper($initial);
|
||||
|
||||
return $initial;
|
||||
return mb_strtoupper($initial);
|
||||
}
|
||||
|
||||
public function getAccountNumber(): ?string
|
||||
@@ -1232,36 +1233,57 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursMonday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_MONDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursTuesday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursWednesday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursThursday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursFriday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursSaturday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursSunday(): int
|
||||
{
|
||||
return (int) $this->getPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, 0);
|
||||
@@ -1302,39 +1324,60 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this->getFormattedHoliday(is_numeric($holidays) ? $holidays : 0.0);
|
||||
}
|
||||
|
||||
public function setWorkHoursMonday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursMonday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_MONDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_MONDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursTuesday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursTuesday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_TUESDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursWednesday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursWednesday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_WEDNESDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursThursday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursThursday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_THURSDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursFriday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursFriday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_FRIDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursSaturday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursSaturday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SATURDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setWorkHoursSunday(int $seconds): void
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function setWorkHoursSunday(?int $seconds): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, $seconds);
|
||||
$this->setPreferenceValue(UserPreference::WORK_HOURS_SUNDAY, $seconds ?? 0);
|
||||
}
|
||||
|
||||
public function setPublicHolidayGroup(null|string $group = null): void
|
||||
@@ -1361,6 +1404,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return (float) number_format((round($holidays * 2) / 2), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function hasContractSettings(): bool
|
||||
{
|
||||
return $this->hasWorkHourConfiguration() || $this->getHolidaysPerYear() !== 0.0;
|
||||
@@ -1368,15 +1414,12 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
|
||||
public function hasWorkHourConfiguration(): bool
|
||||
{
|
||||
return $this->getWorkHoursMonday() !== 0 ||
|
||||
$this->getWorkHoursTuesday() !== 0 ||
|
||||
$this->getWorkHoursWednesday() !== 0 ||
|
||||
$this->getWorkHoursThursday() !== 0 ||
|
||||
$this->getWorkHoursFriday() !== 0 ||
|
||||
$this->getWorkHoursSaturday() !== 0 ||
|
||||
$this->getWorkHoursSunday() !== 0;
|
||||
return $this->getWorkContractMode() !== WorkingTimeModeNone::ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function getWorkHoursForDay(\DateTimeInterface $dateTime): int
|
||||
{
|
||||
return match ($dateTime->format('N')) {
|
||||
@@ -1391,6 +1434,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22.0
|
||||
*/
|
||||
public function isWorkDay(\DateTimeInterface $dateTime): bool
|
||||
{
|
||||
return $this->getWorkHoursForDay($dateTime) > 0;
|
||||
@@ -1410,4 +1456,14 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
{
|
||||
$this->supervisor = $supervisor;
|
||||
}
|
||||
|
||||
public function getWorkContractMode(): string
|
||||
{
|
||||
return (string) $this->getPreferenceValue(UserPreference::WORK_CONTRACT_TYPE, WorkingTimeModeNone::ID);
|
||||
}
|
||||
|
||||
public function setWorkContractMode(string $mode): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::WORK_CONTRACT_TYPE, $mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
@@ -32,16 +33,24 @@ class UserPreference
|
||||
public const LOCALE = 'locale';
|
||||
public const TIMEZONE = 'timezone';
|
||||
public const FIRST_WEEKDAY = 'first_weekday';
|
||||
public const WORK_HOURS_MONDAY = 'work_monday';
|
||||
public const WORK_HOURS_TUESDAY = 'work_tuesday';
|
||||
public const WORK_HOURS_WEDNESDAY = 'work_wednesday';
|
||||
public const WORK_HOURS_THURSDAY = 'work_thursday';
|
||||
public const WORK_HOURS_FRIDAY = 'work_friday';
|
||||
public const WORK_HOURS_SATURDAY = 'work_saturday';
|
||||
public const WORK_HOURS_SUNDAY = 'work_sunday';
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_MONDAY = WorkingTimeCalculatorDay::WORK_HOURS_MONDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_TUESDAY = WorkingTimeCalculatorDay::WORK_HOURS_TUESDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_WEDNESDAY = WorkingTimeCalculatorDay::WORK_HOURS_WEDNESDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_THURSDAY = WorkingTimeCalculatorDay::WORK_HOURS_THURSDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_FRIDAY = WorkingTimeCalculatorDay::WORK_HOURS_FRIDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_SATURDAY = WorkingTimeCalculatorDay::WORK_HOURS_SATURDAY;
|
||||
/** @deprecated since 2.22*/
|
||||
public const WORK_HOURS_SUNDAY = WorkingTimeCalculatorDay::WORK_HOURS_SUNDAY;
|
||||
public const WORK_STARTING_DAY = 'work_start_day';
|
||||
public const PUBLIC_HOLIDAY_GROUP = 'public_holiday_group';
|
||||
public const HOLIDAYS_PER_YEAR = 'holidays';
|
||||
public const WORK_CONTRACT_TYPE = 'work_contract_type';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
|
||||
@@ -71,6 +71,9 @@ final class ConfigureMainMenuEvent extends Event
|
||||
return $this->menu->getChild('reporting');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.22 - use getMenu() or getAdminMenu() instead
|
||||
*/
|
||||
public function getAppsMenu(): MenuItemModel
|
||||
{
|
||||
return $this->apps;
|
||||
|
||||
@@ -13,8 +13,6 @@ use App\Repository\Query\CustomerQuery;
|
||||
|
||||
/**
|
||||
* Dynamically find possible meta fields for a customer query.
|
||||
*
|
||||
* @method CustomerQuery getQuery()
|
||||
*/
|
||||
final class CustomerMetaDisplayEvent extends AbstractMetaDisplayEvent
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class EmailEvent extends Event
|
||||
{
|
||||
public function __construct(private Email $email)
|
||||
public function __construct(private readonly Email $email)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,17 @@ use App\Entity\User;
|
||||
* - once per side load for page actions
|
||||
* - once for every entity item (table row)
|
||||
*
|
||||
* @property array{'actions': array, 'view': string} $payload
|
||||
* @property array{actions: array<string, string|array<mixed>>, view: string} $payload
|
||||
*/
|
||||
class PageActionsEvent extends ThemeEvent
|
||||
{
|
||||
private int $divider = 0;
|
||||
private ?string $locale = null;
|
||||
|
||||
public function __construct(User $user, array $payload, private string $action, private string $view)
|
||||
/**
|
||||
* @param array<mixed> $payload
|
||||
*/
|
||||
public function __construct(User $user, array $payload, private readonly string $action, private readonly string $view)
|
||||
{
|
||||
// only for BC reasons, do not access it directly!
|
||||
if (!\array_key_exists('actions', $payload)) {
|
||||
@@ -58,8 +61,6 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
/**
|
||||
* Custom view can only be table listings.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isCustomView(): bool
|
||||
{
|
||||
@@ -71,6 +72,9 @@ class PageActionsEvent extends ThemeEvent
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<mixed>>
|
||||
*/
|
||||
public function getActions(): array
|
||||
{
|
||||
$actions = $this->payload['actions'];
|
||||
@@ -99,28 +103,38 @@ class PageActionsEvent extends ThemeEvent
|
||||
|
||||
public function hasSubmenu(string $submenu): bool
|
||||
{
|
||||
if (!$this->hasAction($submenu)) {
|
||||
if (!\array_key_exists($submenu, $this->payload['actions'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \array_key_exists('children', $this->payload['actions'][$submenu]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $action
|
||||
*/
|
||||
public function addActionToSubmenu(string $submenu, string $key, array $action): void
|
||||
{
|
||||
if ($this->hasAction($submenu)) {
|
||||
if (!\array_key_exists('children', $this->payload['actions'][$submenu])) {
|
||||
$this->payload['actions'][$submenu]['children'] = [];
|
||||
}
|
||||
if (!\array_key_exists($submenu, $this->payload['actions'])) {
|
||||
$this->payload['actions'][$submenu] = ['children' => []];
|
||||
}
|
||||
if (!\array_key_exists('children', $this->payload['actions'][$submenu])) {
|
||||
$this->payload['actions'][$submenu]['children'] = [];
|
||||
}
|
||||
$this->payload['actions'][$submenu]['children'][$key] = $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $action
|
||||
*/
|
||||
public function replaceAction(string $key, array $action): void
|
||||
{
|
||||
$this->payload['actions'][$key] = $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $action
|
||||
*/
|
||||
public function addAction(string $key, array $action): void
|
||||
{
|
||||
if (!$this->hasAction($key)) {
|
||||
|
||||
@@ -28,7 +28,7 @@ class ThemeEvent extends Event
|
||||
/**
|
||||
* @param array<string, mixed|array<mixed>> $payload
|
||||
*/
|
||||
public function __construct(private ?User $user = null, protected array $payload = [])
|
||||
public function __construct(private readonly ?User $user = null, protected array $payload = [])
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,17 @@ namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class UserEmailEvent extends EmailEvent
|
||||
class UserEmailEvent extends Event
|
||||
{
|
||||
public function __construct(private User $user, Email $email)
|
||||
public function __construct(private readonly User $user, private readonly Email $email)
|
||||
{
|
||||
parent::__construct($email);
|
||||
}
|
||||
|
||||
public function getEmail(): Email
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
|
||||
@@ -32,13 +32,12 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
|
||||
|
||||
$allowCreate = $this->isGranted('create_invoice');
|
||||
$allowView = $this->isGranted('view_invoice');
|
||||
$allowCustomer = $this->isGranted('access', $invoice->getCustomer());
|
||||
|
||||
if ($allowCustomer && $allowCreate) {
|
||||
if ($allowCreate) {
|
||||
$event->addEdit($this->path('admin_invoice_edit', ['id' => $invoice->getId()]));
|
||||
}
|
||||
|
||||
if ($allowCustomer && $allowView) {
|
||||
if ($allowView) {
|
||||
$event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']);
|
||||
}
|
||||
|
||||
@@ -46,7 +45,7 @@ final class InvoiceSubscriber extends AbstractActionsSubscriber
|
||||
$event->addDivider();
|
||||
}
|
||||
|
||||
if ($allowCustomer && $allowCreate) {
|
||||
if ($allowCreate) {
|
||||
if (!$invoice->isPending()) {
|
||||
$event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending', 'token' => $payload['token']])]);
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,11 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class UserSubscriber extends AbstractActionsSubscriber
|
||||
{
|
||||
public function __construct(AuthorizationCheckerInterface $auth, UrlGeneratorInterface $urlGenerator, private EventDispatcherInterface $eventDispatcher)
|
||||
public function __construct(
|
||||
AuthorizationCheckerInterface $auth,
|
||||
UrlGeneratorInterface $urlGenerator,
|
||||
private readonly EventDispatcherInterface $eventDispatcher
|
||||
)
|
||||
{
|
||||
parent::__construct($auth, $urlGenerator);
|
||||
}
|
||||
@@ -47,6 +51,10 @@ final class UserSubscriber extends AbstractActionsSubscriber
|
||||
$event->addActionToSubmenu('edit', $id, $action);
|
||||
}
|
||||
|
||||
if ($this->isGranted('hours', $user)) {
|
||||
$event->addActionToSubmenu('report', 'work_times', ['url' => $this->path('user_contract', ['user' => $user->getId()]), 'title' => 'work_times']);
|
||||
}
|
||||
|
||||
if (($event->getUser()->getId() === $user->getId() && $this->isGranted('report:user')) || $this->isGranted('report:other')) {
|
||||
$event->addActionToSubmenu('report', 'weekly', ['url' => $this->path('report_user_week', ['user' => $user->getId()]), 'translation_domain' => 'reporting', 'title' => 'report_user_week']);
|
||||
$event->addActionToSubmenu('report', 'monthly', ['url' => $this->path('report_user_month', ['user' => $user->getId()]), 'translation_domain' => 'reporting', 'title' => 'report_user_month']);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Event\EmailEvent;
|
||||
use App\Event\UserEmailEvent;
|
||||
use App\Mail\KimaiMailer;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
@@ -26,6 +27,7 @@ final class EmailSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
return [
|
||||
EmailEvent::class => ['onMailEvent', 100],
|
||||
UserEmailEvent::class => ['onUserMailEvent', 100],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -33,4 +35,9 @@ final class EmailSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
$this->mailer->send($event->getEmail());
|
||||
}
|
||||
|
||||
public function onUserMailEvent(UserEmailEvent $event): void
|
||||
{
|
||||
$this->mailer->sendToUser($event->getUser(), $event->getEmail());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ final class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
$event->addItem($child);
|
||||
}
|
||||
|
||||
if ($menuEvent->getAppsMenu()->hasChildren()) {
|
||||
$event->addItem($menuEvent->getAppsMenu());
|
||||
if ($menuEvent->getAppsMenu()->hasChildren()) { // @phpstan-ignore-line
|
||||
$event->addItem($menuEvent->getAppsMenu()); // @phpstan-ignore-line
|
||||
}
|
||||
if ($menuEvent->getAdminMenu()->hasChildren()) {
|
||||
$event->addItem($menuEvent->getAdminMenu());
|
||||
|
||||
@@ -94,7 +94,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
$contract = new MenuItemModel('contract', 'work_contract', null, [], 'contract');
|
||||
if ($user->hasContractSettings() || $auth->isGranted('hours_other_profile')) {
|
||||
if ($auth->isGranted('hours', $user)) {
|
||||
$contract->addChild(new MenuItemModel('contract_status', 'work_times', 'user_contract', [], 'work_times'));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\ExportFilename;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use App\Utils\StringHelper;
|
||||
@@ -461,9 +463,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
|
||||
if (isset($columns['customer-meta'])) {
|
||||
/** @var CustomerQuery $customerQuery */
|
||||
$customerQuery = $query->copyTo(new CustomerQuery());
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($customerQuery, CustomerMetaDisplayEvent::EXPORT));
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query->copyTo(new CustomerQuery()), CustomerMetaDisplayEvent::EXPORT));
|
||||
|
||||
$columns['customer-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($customerMetaFields): int {
|
||||
@@ -491,7 +491,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
|
||||
if (isset($columns['project-meta'])) {
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query->copyTo(new ProjectQuery()), ProjectMetaDisplayEvent::EXPORT));
|
||||
$columns['project-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($projectMetaFields): int {
|
||||
foreach ($projectMetaFields as $metaField) {
|
||||
@@ -518,7 +518,7 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
|
||||
if (isset($columns['activity-meta'])) {
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query->copyTo(new ActivityQuery()), ActivityMetaDisplayEvent::EXPORT));
|
||||
$columns['activity-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($activityMetaFields): int {
|
||||
foreach ($activityMetaFields as $metaField) {
|
||||
|
||||
@@ -19,7 +19,9 @@ use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\SecurityPolicy\ExportPolicy;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -81,13 +83,10 @@ class HtmlRenderer
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
/** @var CustomerQuery $customerQuery */
|
||||
$customerQuery = $query->copyTo(new CustomerQuery());
|
||||
|
||||
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($customerQuery, CustomerMetaDisplayEvent::EXPORT));
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT));
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT));
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query->copyTo(new CustomerQuery()), CustomerMetaDisplayEvent::EXPORT));
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query->copyTo(new ProjectQuery()), ProjectMetaDisplayEvent::EXPORT));
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query->copyTo(new ActivityQuery()), ActivityMetaDisplayEvent::EXPORT));
|
||||
|
||||
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
@@ -14,42 +14,54 @@ use App\Validator\Constraints\Duration as DurationConstraint;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
|
||||
/**
|
||||
* @implements DataTransformerInterface<string|int|null, string|null>
|
||||
*/
|
||||
final class DurationStringToSecondsTransformer implements DataTransformerInterface
|
||||
{
|
||||
/**
|
||||
* @param int $intToFormat
|
||||
* @return string|null
|
||||
*/
|
||||
public function transform(mixed $intToFormat): ?string
|
||||
public function transform(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return (new Duration())->format($intToFormat);
|
||||
if (!\is_int($value) && is_numeric($value)) {
|
||||
$value = (int) $value;
|
||||
}
|
||||
|
||||
if (!\is_int($value)) {
|
||||
// do not throw an exception, that would break the frontend, make it null / empty instead
|
||||
return null;
|
||||
}
|
||||
|
||||
return (new Duration())->format($value);
|
||||
} catch (\Exception | \TypeError $e) {
|
||||
throw new TransformationFailedException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $formatToInt
|
||||
* @return int|null
|
||||
*/
|
||||
public function reverseTransform(mixed $formatToInt): ?int
|
||||
public function reverseTransform(mixed $value): ?int
|
||||
{
|
||||
if (null === $formatToInt) {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($formatToInt)) {
|
||||
if ($value === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (\is_int($value) || \is_float($value)) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
|
||||
// we need this one here, because the data transformer is executed BEFORE the constraint is called
|
||||
if (!preg_match((new DurationConstraint())->pattern, $formatToInt)) {
|
||||
if (!preg_match((new DurationConstraint())->pattern, $value)) {
|
||||
throw new TransformationFailedException('Invalid duration format given');
|
||||
}
|
||||
|
||||
try {
|
||||
$seconds = (new Duration())->parseDurationString($formatToInt);
|
||||
$seconds = (new Duration())->parseDurationString($value);
|
||||
|
||||
// DateTime throws if a duration with too many seconds is passed and an amount of so
|
||||
// many seconds is likely not required in a time-tracking application ;-)
|
||||
|
||||
@@ -25,19 +25,11 @@ final class DocumentationLinkExtension extends AbstractTypeExtension
|
||||
return [FormType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['docu_chapter'] = $options['docu_chapter'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsResolver $resolver
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined(['docu_chapter']);
|
||||
|
||||
@@ -26,11 +26,6 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
return [EntityType::class, ChoiceType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (isset($options['selectpicker']) && false === $options['selectpicker']) {
|
||||
@@ -59,7 +54,7 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
|
||||
// there is a very weird logic in vendor/symfony/twig-bridge/Resources/views/Form/form_div_layout.html.twig
|
||||
// in block "block choice_widget_collapsed" that resets "{% set required = false %}", so we fake it into the select
|
||||
if (true === $options['required'] && (!\array_key_exists('size', $options['attr']) || $options['attr']['size'] <= 1)) {
|
||||
if (true === $options['required'] && \is_array($options['attr']) && (!\array_key_exists('size', $options['attr']) || $options['attr']['size'] <= 1)) {
|
||||
$extendedOptions['required'] = 'required';
|
||||
$extendedOptions['placeholder'] = '';
|
||||
}
|
||||
|
||||
@@ -25,11 +25,6 @@ final class IconExtension extends AbstractTypeExtension
|
||||
return [TextType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['icon'] = $options['icon'] ?? null;
|
||||
|
||||
@@ -21,7 +21,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
*/
|
||||
final class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
{
|
||||
public function __construct(private UrlGeneratorInterface $router)
|
||||
public function __construct(private readonly UrlGeneratorInterface $router)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@ final class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
return [EntityType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (!isset($options['api_data'])) {
|
||||
@@ -69,7 +64,7 @@ final class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
$parent = $form->getParent();
|
||||
do {
|
||||
$formPrefixes[] = $parent->getName();
|
||||
} while (($parent = $parent->getParent()) !== null);
|
||||
} while (($parent = $parent?->getParent()) !== null);
|
||||
|
||||
$formPrefix = implode('_', array_reverse($formPrefixes));
|
||||
$formField = $apiData['select'];
|
||||
|
||||
@@ -44,7 +44,7 @@ final class ActivityHelper
|
||||
public function getChoiceLabel(Activity $activity): string
|
||||
{
|
||||
$name = $this->getChoicePattern();
|
||||
$name = str_replace(self::PATTERN_NAME, $activity->getName(), $name);
|
||||
$name = str_replace(self::PATTERN_NAME, $activity->getName() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_NUMBER, $activity->getNumber() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMMENT, $activity->getComment() ?? '', $name);
|
||||
|
||||
@@ -57,7 +57,7 @@ final class ActivityHelper
|
||||
}
|
||||
|
||||
if ($name === '' || $name === self::SPACER) {
|
||||
$name = $activity->getName();
|
||||
$name = $activity->getName() ?? '';
|
||||
}
|
||||
|
||||
return substr($name, 0, 110);
|
||||
|
||||
@@ -45,7 +45,7 @@ final class CustomerHelper
|
||||
public function getChoiceLabel(Customer $customer): string
|
||||
{
|
||||
$name = $this->getChoicePattern();
|
||||
$name = str_replace(self::PATTERN_NAME, $customer->getName(), $name);
|
||||
$name = str_replace(self::PATTERN_NAME, $customer->getName() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMMENT, $customer->getComment() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_NUMBER, $customer->getNumber() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMPANY, $customer->getCompany() ?? '', $name);
|
||||
@@ -59,7 +59,7 @@ final class CustomerHelper
|
||||
}
|
||||
|
||||
if ($name === '' || $name === self::SPACER) {
|
||||
$name = $customer->getName();
|
||||
$name = $customer->getName() ?? '';
|
||||
}
|
||||
|
||||
return substr($name, 0, 110);
|
||||
|
||||
@@ -70,7 +70,7 @@ final class ProjectHelper
|
||||
public function getChoiceLabel(Project $project): string
|
||||
{
|
||||
$name = $this->getChoicePattern();
|
||||
$name = str_replace(self::PATTERN_NAME, $project->getName(), $name);
|
||||
$name = str_replace(self::PATTERN_NAME, $project->getName() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_NUMBER, $project->getNumber() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_COMMENT, $project->getComment() ?? '', $name);
|
||||
$name = str_replace(self::PATTERN_CUSTOMER, $project->getCustomer()?->getName() ?? '', $name);
|
||||
@@ -115,7 +115,7 @@ final class ProjectHelper
|
||||
}
|
||||
|
||||
if ($name === '' || $name === self::SPACER) {
|
||||
$name = $project->getName();
|
||||
$name = $project->getName() ?? '';
|
||||
}
|
||||
|
||||
return substr($name, 0, 110);
|
||||
|
||||
52
src/Form/Model/UserContractModel.php
Normal file
52
src/Form/Model/UserContractModel.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
final class UserContractModel
|
||||
{
|
||||
public function __construct(private readonly User $user)
|
||||
{
|
||||
}
|
||||
|
||||
public function __isset(string $name): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __set(string $name, mixed $value): void
|
||||
{
|
||||
$method = 'set' . ucfirst($name);
|
||||
|
||||
if (method_exists($this->user, $method)) {
|
||||
$this->user->$method($value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_scalar($value) && $value !== null) {
|
||||
throw new \InvalidArgumentException('Invalid value passed');
|
||||
}
|
||||
|
||||
$this->user->setPreferenceValue($name, $value);
|
||||
}
|
||||
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
$method = 'get' . ucfirst($name);
|
||||
|
||||
if (method_exists($this->user, $method)) {
|
||||
return $this->user->$method();
|
||||
}
|
||||
|
||||
return $this->user->getPreferenceValue($name);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ final class MenuChoiceType extends AbstractType
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
$choices = $this->getChoicesFromMenu($event->getMenu(), $filter);
|
||||
$choices += $this->getChoicesFromMenu($event->getAppsMenu(), $filter);
|
||||
$choices += $this->getChoicesFromMenu($event->getAppsMenu(), $filter); // @phpstan-ignore-line
|
||||
$choices += $this->getChoicesFromMenu($event->getAdminMenu(), $filter);
|
||||
$choices += $this->getChoicesFromMenu($event->getSystemMenu(), $filter);
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
final class ProjectType extends AbstractType
|
||||
{
|
||||
public function __construct(private ProjectHelper $projectHelper, private CustomerHelper $customerHelper)
|
||||
public function __construct(
|
||||
private readonly ProjectHelper $projectHelper,
|
||||
private readonly CustomerHelper $customerHelper
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -9,42 +9,56 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Model\UserContractModel;
|
||||
use App\WorkingTime\Mode\WorkingTimeMode;
|
||||
use App\WorkingTime\Mode\WorkingTimeModeFactory;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<User>
|
||||
* @extends AbstractType<UserContractModel>
|
||||
*/
|
||||
final class UserContractType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly WorkingTimeModeFactory $contractModeService)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['workContractModes'] = $this->contractModeService->getAll();
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$dayOptions = [
|
||||
'translation_domain' => 'system-configuration',
|
||||
'constraints' => [
|
||||
new GreaterThanOrEqual(0)
|
||||
],
|
||||
];
|
||||
$sorted = $this->contractModeService->getAll();
|
||||
|
||||
$builder
|
||||
->add('workHoursMonday', DurationType::class, array_merge(['label' => 'Monday'], $dayOptions))
|
||||
->add('workHoursTuesday', DurationType::class, array_merge(['label' => 'Tuesday'], $dayOptions))
|
||||
->add('workHoursWednesday', DurationType::class, array_merge(['label' => 'Wednesday'], $dayOptions))
|
||||
->add('workHoursThursday', DurationType::class, array_merge(['label' => 'Thursday'], $dayOptions))
|
||||
->add('workHoursFriday', DurationType::class, array_merge(['label' => 'Friday'], $dayOptions))
|
||||
->add('workHoursSaturday', DurationType::class, array_merge(['label' => 'Saturday'], $dayOptions))
|
||||
->add('workHoursSunday', DurationType::class, array_merge(['label' => 'Sunday'], $dayOptions))
|
||||
;
|
||||
usort($sorted, function (WorkingTimeMode $a, WorkingTimeMode $b) {
|
||||
return $a->getOrder() <=> $b->getOrder();
|
||||
});
|
||||
|
||||
$modes = [];
|
||||
foreach ($sorted as $mode) {
|
||||
$modes[$mode->getName()] = $mode->getId();
|
||||
}
|
||||
|
||||
if (\count($modes) > 1) {
|
||||
$builder->add('workContractMode', ChoiceType::class, ['label' => 'work_hours_mode', 'choices' => $modes]);
|
||||
}
|
||||
|
||||
foreach ($modes as $mode) {
|
||||
$this->contractModeService->getMode($mode)->buildForm($builder, $options); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
'data_class' => UserContractModel::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'edit_user_contract',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user