Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f8159fdc | ||
|
|
8fb0b1445a | ||
|
|
15673555a2 | ||
|
|
ceeec7b8a6 | ||
|
|
46e5650882 | ||
|
|
405ea0076b | ||
|
|
8cd7f7de3f | ||
|
|
7cd17154fc | ||
|
|
299fcb091a | ||
|
|
025c6b0e7b | ||
|
|
388c3ec188 | ||
|
|
b6d9bb0d2a | ||
|
|
561ec3b1e9 | ||
|
|
9611646bc6 | ||
|
|
b5972bb680 | ||
|
|
455861ec69 | ||
|
|
5affa0de82 | ||
|
|
e11a2a6089 | ||
|
|
614a50ce0b | ||
|
|
66594b286a | ||
|
|
8e06f94bdd | ||
|
|
b0e03bb0b8 | ||
|
|
22e30b1871 | ||
|
|
8104a67e3e | ||
|
|
654ffda454 | ||
|
|
3f51437569 | ||
|
|
1bd784eef3 | ||
|
|
0d11aded40 | ||
|
|
00925d38aa |
@@ -1,5 +1,5 @@
|
||||
unreleased=true
|
||||
future-release=1.1
|
||||
future-release=1.2
|
||||
exclude-labels=duplicate,question,invalid,wontfix,release
|
||||
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt
|
||||
issues-wo-labels=false
|
||||
|
||||
82
.travis.yml
82
.travis.yml
@@ -1,29 +1,64 @@
|
||||
dist: xenial
|
||||
language: php
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache/files
|
||||
|
||||
addons:
|
||||
mariadb: '10.2'
|
||||
|
||||
services:
|
||||
- mysql
|
||||
|
||||
env:
|
||||
global:
|
||||
- CODECOVERAGE=0
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- php: 7.2
|
||||
- php: 7.3
|
||||
env: CODECOVERAGE=1
|
||||
- stage: Code quality
|
||||
env: CODECOVERAGE=0 DB=sqlite
|
||||
php: '7.2'
|
||||
script:
|
||||
- composer validate --no-check-all --strict
|
||||
- composer kimai:codestyle
|
||||
- composer kimai:phpstan
|
||||
- stage: Test
|
||||
php: '7.2'
|
||||
env: CODECOVERAGE=0 DB=mysql
|
||||
sudo: required
|
||||
services:
|
||||
- mysql
|
||||
- stage: Test
|
||||
php: '7.3'
|
||||
env: CODECOVERAGE=0 DB=mysql
|
||||
sudo: required
|
||||
services:
|
||||
- mysql
|
||||
- stage: Test
|
||||
php: '7.2'
|
||||
env: CODECOVERAGE=0 DB=sqlite
|
||||
- stage: Test
|
||||
#php: '7.4snapshot'
|
||||
php: '7.3'
|
||||
env: CODECOVERAGE=0 DB=sqlite
|
||||
- stage: Test
|
||||
php: '7.2'
|
||||
env: CODECOVERAGE=0 DB=mariadb
|
||||
addons:
|
||||
mariadb: '10.2'
|
||||
services:
|
||||
- mysql
|
||||
before_script:
|
||||
- mysql -u root -e 'CREATE USER IF NOT EXISTS travis@localhost; GRANT ALL ON *.* TO travis@localhost;'
|
||||
- stage: Test
|
||||
php: '7.3'
|
||||
env: CODECOVERAGE=0 DB=mariadb
|
||||
addons:
|
||||
mariadb: '10.2'
|
||||
services:
|
||||
- mysql
|
||||
before_script:
|
||||
- mysql -u root -e 'CREATE USER IF NOT EXISTS travis@localhost; GRANT ALL ON *.* TO travis@localhost;'
|
||||
- stage: Code coverage
|
||||
php: '7.3'
|
||||
env: CODECOVERAGE=1 DB=sqlite
|
||||
after_success:
|
||||
- if [[ $CODECOVERAGE == 1 ]]; then bash <(curl -s https://codecov.io/bash); fi
|
||||
|
||||
before_install:
|
||||
# - phpenv config-rm xdebug.ini
|
||||
- if [[ $CODECOVERAGE == 0 ]]; then phpenv config-rm xdebug.ini; fi;
|
||||
- composer self-update
|
||||
- php -i
|
||||
- php -m
|
||||
@@ -33,18 +68,11 @@ install:
|
||||
- composer require zendframework/zend-ldap
|
||||
|
||||
script:
|
||||
- composer validate --no-check-all --strict
|
||||
- composer kimai:codestyle
|
||||
- composer kimai:phpstan
|
||||
- if [[ $CODECOVERAGE == 1 ]]; then vendor/bin/phpunit tests/ --coverage-clover=coverage.xml; else vendor/bin/phpunit tests/; fi;
|
||||
- cp tests/.env.dist.sqlite .env
|
||||
- bin/console doctrine:database:create -n
|
||||
- bin/console doctrine:migrations:migrate -n
|
||||
- bin/console doctrine:migrations:migrate first -n
|
||||
- cp tests/.env.dist.mysql .env
|
||||
- if [[ $CODECOVERAGE == 1 ]]; then vendor/bin/phpunit tests/ --coverage-clover=coverage.xml; fi;
|
||||
- if [[ $CODECOVERAGE == 0 ]]; then vendor/bin/phpunit tests/; fi;
|
||||
- if [[ $DB == 'sqlite' ]]; then cp tests/.env.dist.sqlite .env; fi;
|
||||
- if [[ $DB == "mysql" || $DB == "mariadb" ]]; then cp tests/.env.dist.mysql .env; fi;
|
||||
- bin/console doctrine:database:create -n
|
||||
- bin/console doctrine:migrations:migrate -n
|
||||
- bin/console doctrine:migrations:migrate first -n
|
||||
|
||||
after_success:
|
||||
- if [[ $CODECOVERAGE == 1 ]]; then bash <(curl -s https://codecov.io/bash); fi
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Change Log
|
||||
|
||||
## [1.2](https://github.com/kevinpapst/kimai2/tree/1.2) (2019-08-28)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.1...1.2)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
|
||||
|
||||
## [1.1](https://github.com/kevinpapst/kimai2/tree/1.1) (2019-07-24)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.0.1...1.1)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[](https://travis-ci.org/kevinpapst/kimai2)
|
||||
[](https://codecov.io/gh/kevinpapst/kimai2)
|
||||
[](https://gitter.im/kimai2/support)
|
||||
[](https://www.bountysource.com/teams/kimai2)
|
||||
|
||||
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
|
||||
It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Doctrine, AdminLTE, Webpack, ES6 etc.
|
||||
|
||||
@@ -7,8 +7,8 @@ As announced in the [README](README.md) I only support the latest available rele
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| master | :white_check_mark: |
|
||||
| 1.1 | :white_check_mark: |
|
||||
| < 1.1 | :x: |
|
||||
| 1.2 | :white_check_mark: |
|
||||
| < 1.2 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
18
UPGRADING.md
18
UPGRADING.md
@@ -5,10 +5,22 @@ _Make sure to create a backup before you start!_
|
||||
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
|
||||
you can upgrade your Kimai installation to the latest stable release.
|
||||
|
||||
**Before you leave ...**
|
||||
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.
|
||||
|
||||
There might be more steps required which are version specific and need to be executed after [the update](https://www.kimai.org/documentation/updates.html).
|
||||
Read and perform EACH version specific task below, otherwise you risk data inconsistency or a broken installation!
|
||||
## [1.2](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
|
||||
|
||||
### Possible BC breaks
|
||||
|
||||
- Deleted timezone conversion command. If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.
|
||||
- Minimum password length raised from 5 to 8 character (applies only for password changes and new users)
|
||||
- Maximum customer name length lowered to 150 character
|
||||
- Maximum project name length lowered to 150 character
|
||||
- Maximum activity name length lowered to 150 character
|
||||
- Added new permission: `manage_invoice_template`
|
||||
- Removed permissions: `view_invoice_template`, `create_invoice_template`, `edit_invoice_template`, `delete_invoice_template`
|
||||
- Removed permission: `view_invoice` (using `create_export` only)
|
||||
- Custom export renderer need to check for usage of `Timesheet::getEnd()` as running entries can now be exported as well
|
||||
|
||||
## [1.1](https://github.com/kevinpapst/kimai2/releases/tag/1.1)
|
||||
|
||||
|
||||
@@ -15,16 +15,18 @@ require('bootstrap-select');
|
||||
|
||||
const Moment = require('moment');
|
||||
global.moment = Moment;
|
||||
require('moment/locale/de');
|
||||
require('moment/locale/it');
|
||||
require('moment/locale/fr');
|
||||
require('moment/locale/es');
|
||||
require('moment/locale/ru');
|
||||
require('moment/locale/ar');
|
||||
require('moment/locale/de');
|
||||
require('moment/locale/es');
|
||||
require('moment/locale/fr');
|
||||
require('moment/locale/hu');
|
||||
require('moment/locale/pt-br');
|
||||
require('moment/locale/sv');
|
||||
require('moment/locale/it');
|
||||
require('moment/locale/ja');
|
||||
require('moment/locale/ko');
|
||||
require('moment/locale/pt-br');
|
||||
require('moment/locale/ru');
|
||||
require('moment/locale/sk');
|
||||
require('moment/locale/sv');
|
||||
|
||||
require('daterangepicker');
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ if (!isset($_SERVER['APP_ENV'])) {
|
||||
if (!class_exists(Dotenv::class)) {
|
||||
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
|
||||
}
|
||||
(new Dotenv())->load(__DIR__.'/../.env');
|
||||
(new Dotenv(true))->load(__DIR__.'/../.env');
|
||||
}
|
||||
|
||||
$input = new ArgvInput();
|
||||
|
||||
@@ -18,11 +18,9 @@
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.0",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^2.3",
|
||||
"friendsofsymfony/user-bundle": "~2.0",
|
||||
"fzaninotto/faker": "^1.8",
|
||||
"friendsofsymfony/rest-bundle": "^2.5",
|
||||
"friendsofsymfony/user-bundle": "dev-master",
|
||||
"gedmo/doctrine-extensions": "^2.4",
|
||||
"jms/metadata": "^2.0",
|
||||
"jms/serializer-bundle": "^3.2",
|
||||
@@ -45,18 +43,15 @@
|
||||
"symfony/form": "^4.0",
|
||||
"symfony/framework-bundle": "^4.0",
|
||||
"symfony/intl": "^4.0",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/monolog-bundle": "^3.4",
|
||||
"symfony/orm-pack": "^1.0",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
"symfony/security-bundle": "~4.2.0",
|
||||
"symfony/security-bundle": "^4.0",
|
||||
"symfony/security-csrf": "^4.0",
|
||||
"symfony/serializer": "^4.0",
|
||||
"symfony/swiftmailer-bundle": "^3.1",
|
||||
"symfony/swiftmailer-bundle": "^3.2",
|
||||
"symfony/translation": "^4.0",
|
||||
"symfony/twig-bundle": "^4.0",
|
||||
"symfony/validator": "^4.0",
|
||||
"symfony/web-server-bundle": "^4.0",
|
||||
"symfony/webpack-encore-bundle": "^1.5",
|
||||
"symfony/yaml": "^4.0",
|
||||
"twig/extensions": "^1.5",
|
||||
@@ -64,7 +59,9 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^5.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"friendsofphp/php-cs-fixer": "^2.10",
|
||||
"fzaninotto/faker": "^1.8",
|
||||
"phpstan/phpstan": "^0.11.7",
|
||||
"phpstan/phpstan-doctrine": "^0.11.4",
|
||||
"phpstan/phpstan-phpunit": "^0.11.2",
|
||||
@@ -72,8 +69,11 @@
|
||||
"phpunit/phpunit": "^7.0",
|
||||
"symfony/browser-kit": "^4.0",
|
||||
"symfony/css-selector": "^4.0",
|
||||
"symfony/maker-bundle": "^1.12",
|
||||
"symfony/phpunit-bridge": "^4.0",
|
||||
"symfony/thanks": "^1.0"
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
"symfony/thanks": "^1.0",
|
||||
"symfony/web-server-bundle": "~4.2.0"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
@@ -138,7 +138,7 @@
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
"require": "~4.2.0"
|
||||
"require": "4.3.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2097
composer.lock
generated
2097
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -84,23 +84,33 @@ kimai:
|
||||
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"])
|
||||
sets:
|
||||
ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','delete_activity']
|
||||
PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project']
|
||||
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer']
|
||||
ACTIVITIES_TEAM: ['view_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity']
|
||||
PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project','permissions_project']
|
||||
PROJECTS_TEAM: ['view_project','edit_teamlead_project','budget_teamlead_project','permissions_teamlead_project']
|
||||
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer']
|
||||
CUSTOMERS_TEAM: ['view_customer','edit_teamlead_customer','budget_teamlead_customer']
|
||||
INVOICE: ['view_invoice','create_invoice']
|
||||
INVOICE_TEMPLATE: ['view_invoice_template','create_invoice_template','edit_invoice_template','delete_invoice_template']
|
||||
INVOICE_TEMPLATE: ['manage_invoice_template']
|
||||
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
|
||||
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
||||
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
||||
PROFILE_OTHER: ['view_other_profile','edit_other_profile','delete_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile']
|
||||
PROFILE_OTHER: ['view_other_profile','edit_other_profile','delete_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
|
||||
TAGS: ['view_tag','delete_tag']
|
||||
USER: ['view_user','create_user','delete_user','role_permissions']
|
||||
RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
|
||||
RATE_OTHER: ['view_rate_other_timesheet','edit_rate_other_timesheet']
|
||||
EXPORT: ['view_export','create_export','edit_export_own_timesheet','edit_export_other_timesheet']
|
||||
EXPORT: ['create_export','edit_export_own_timesheet','edit_export_other_timesheet']
|
||||
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
||||
# some single default definitions for roles
|
||||
SINGLE_USER: []
|
||||
SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile']
|
||||
SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile']
|
||||
SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','delete_own_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile']
|
||||
# link above sets to one set for each user role
|
||||
ROLE_USER: ['@TIMESHEET','@PROFILE']
|
||||
ROLE_TEAMLEAD: ['@INVOICE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@EXPORT','@TAGS','@INVOICE_TEMPLATE','!delete_invoice_template','view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile']
|
||||
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@RATE','@RATE_OTHER','@EXPORT','@TAGS','hourly-rate_own_profile','edit_exported_timesheet']
|
||||
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@RATE','@RATE_OTHER','@EXPORT','@TAGS','hourly-rate_own_profile','hourly-rate_other_profile','delete_own_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet']
|
||||
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAM','@PROJECTS_TEAM','@CUSTOMERS_TEAM','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@SINGLE_TEAMLEAD']
|
||||
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_ADMIN']
|
||||
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_SUPER_ADMIN']
|
||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||
maps:
|
||||
ROLE_USER: ['ROLE_USER']
|
||||
@@ -124,6 +134,7 @@ kimai:
|
||||
# week_numbers: true
|
||||
# weekends: true
|
||||
# day_limit: 4
|
||||
# slot_duration: '00:30:00'
|
||||
# businessHours:
|
||||
# days: [1, 2, 3, 4, 5]
|
||||
# begin: '08:00'
|
||||
@@ -153,18 +164,6 @@ kimai:
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# THEME Settings
|
||||
# will be available as twig globals at "kimai_context.*".
|
||||
# See documentation at https://www.kimai.org/documentation/theme.html
|
||||
# --------------------------------------------------------------------------------
|
||||
# theme:
|
||||
# # BETA test: If you set this to 'selectpicker' the customer/project/activity select boxes will be transformed
|
||||
# # into a searchable and javascript enhanced input type
|
||||
# select_type: ~
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# DASHBOARD
|
||||
# Using the configurable widget sections.
|
||||
@@ -205,6 +204,12 @@ kimai:
|
||||
# Locale specific settings, like date formats
|
||||
# --------------------------------------------------------------------------------
|
||||
languages:
|
||||
ar:
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
date_type: 'yyyy-MM-dd'
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m h'
|
||||
de:
|
||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||
date_type: 'dd.MM.yyyy'
|
||||
@@ -217,13 +222,7 @@ kimai:
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m h'
|
||||
pt_BR:
|
||||
date_time_type: 'dd-MM-yyyy HH:mm'
|
||||
date_type: 'dd-MM-yyyy'
|
||||
date: 'd-m-Y'
|
||||
date_time: 'd-m H:i'
|
||||
duration: '%%h:%%m h'
|
||||
it:
|
||||
es:
|
||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||
date_type: 'dd.MM.yyyy'
|
||||
date: 'd.m.Y'
|
||||
@@ -235,29 +234,47 @@ kimai:
|
||||
date: 'd/m/Y'
|
||||
date_time: 'd/m H:i'
|
||||
duration: '%%h h %%m'
|
||||
es:
|
||||
hu:
|
||||
date_time_type: 'yyyy.MM.dd. HH:mm'
|
||||
date_type: 'yyyy.MM.dd.'
|
||||
date: 'Y.m.d.'
|
||||
date_time: 'm.d. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
it:
|
||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||
date_type: 'dd.MM.yyyy'
|
||||
date: 'd.m.Y'
|
||||
date_time: 'd.m. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
ja:
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
date_type: 'yyyy-MM-dd'
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m tim'
|
||||
ko:
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
date_type: 'yyyy-MM-dd'
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m h'
|
||||
pt_BR:
|
||||
date_time_type: 'dd-MM-yyyy HH:mm'
|
||||
date_type: 'dd-MM-yyyy'
|
||||
date: 'd-m-Y'
|
||||
date_time: 'd-m H:i'
|
||||
duration: '%%h:%%m h'
|
||||
ru:
|
||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||
date_type: 'dd.MM.yyyy'
|
||||
date: 'd.m.Y'
|
||||
date_time: 'd.m. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
ar:
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
date_type: 'yyyy-MM-dd'
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m h'
|
||||
hu:
|
||||
date_time_type: 'yyyy.MM.dd. HH:mm'
|
||||
date_type: 'yyyy.MM.dd.'
|
||||
date: 'Y.m.d.'
|
||||
date_time: 'm.d. H:i'
|
||||
sk:
|
||||
date_time_type: 'dd. MM. yyyy HH:mm'
|
||||
date_type: 'dd. MM. yyyy'
|
||||
date: 'd. m. Y'
|
||||
date_time: 'd. m. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
sv:
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
@@ -265,35 +282,6 @@ kimai:
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m tim'
|
||||
ja:
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
date_type: 'yyyy-MM-dd'
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m tim'
|
||||
sk:
|
||||
date_time_type: 'dd. MM. yyyy HH:mm'
|
||||
date_type: 'dd. MM. yyyy'
|
||||
date: 'd. m. Y'
|
||||
date_time: 'd. m. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# FORMS
|
||||
# Default settings used to populate forms
|
||||
# --------------------------------------------------------------------------------
|
||||
# defaults:
|
||||
# customer:
|
||||
# timezone: Europe/Berlin
|
||||
# country: DE
|
||||
# currency: EUR
|
||||
# user:
|
||||
# timezone: Europe/Berlin
|
||||
# language: de
|
||||
# theme: blue-light
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ nelmio_api_doc:
|
||||
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
|
||||
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
||||
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
|
||||
- { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team] }
|
||||
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
|
||||
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
|
||||
areas:
|
||||
path_patterns:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
security:
|
||||
encoders:
|
||||
App\Entity\User: bcrypt
|
||||
App\Entity\User: auto
|
||||
|
||||
providers:
|
||||
chain_provider:
|
||||
|
||||
@@ -4,7 +4,7 @@ security:
|
||||
encoders:
|
||||
# to make tests much faster, BCrypt cost is changed to its minimum allowed value (4)
|
||||
# See https://symfony.com/doc/current/reference/configuration/security.html#using-the-bcrypt-password-encoder
|
||||
App\Entity\User: { algorithm: bcrypt, cost: 4 }
|
||||
App\Entity\User: { algorithm: auto }
|
||||
|
||||
firewalls:
|
||||
secured_area:
|
||||
|
||||
14
config/serializer/App/Entity.Team.yml
Normal file
14
config/serializer/App/Entity.Team.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
App\Entity\Team:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, users]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
name:
|
||||
include: true
|
||||
users:
|
||||
exclude: true
|
||||
customers:
|
||||
exclude: true
|
||||
projects:
|
||||
exclude: true
|
||||
@@ -2,7 +2,7 @@
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: en|de|it|fr|es|ru|ar|hu|pt_BR|sv|ja|sk
|
||||
app_locales: ar|de|en|es|fr|hu|it|ja|ko|pt_BR|ru|sk|sv
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@@ -130,7 +130,8 @@ services:
|
||||
arguments: ['%security.role_hierarchy.roles%']
|
||||
|
||||
App\Security\RolePermissionManager:
|
||||
arguments: ['%kimai.permissions%']
|
||||
arguments:
|
||||
$permissions: '%kimai.permissions%'
|
||||
|
||||
# ================================================================================
|
||||
# LDAP
|
||||
@@ -154,6 +155,11 @@ services:
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\User']
|
||||
|
||||
App\Repository\TeamRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Team']
|
||||
|
||||
App\Repository\ActivityRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
|
||||
@@ -9,5 +9,5 @@ App\Entity\User:
|
||||
- NotBlank: ~
|
||||
- Email: ~
|
||||
plainPassword:
|
||||
- NotBlank: { groups: [registration, passwordUpdate] }
|
||||
- Length: { min: 5, max: 60, groups: [registration, passwordUpdate] }
|
||||
- NotBlank: { groups: [Registration, PasswordUpdate] }
|
||||
- Length: { min: 8, max: 60, groups: [Registration, PasswordUpdate] }
|
||||
|
||||
@@ -3,6 +3,7 @@ includes:
|
||||
- vendor/phpstan/phpstan-doctrine/extension.neon
|
||||
|
||||
parameters:
|
||||
tmpDir: %rootDir%/../../../var/cache/phpstan
|
||||
autoload_directories:
|
||||
- %rootDir%/../../../src/Migrations
|
||||
# symfony:
|
||||
@@ -16,6 +17,7 @@ parameters:
|
||||
- '#Access to an undefined property Faker\\Generator::\$stateAbbr.#'
|
||||
- '#Access to an undefined property Faker\\Generator::\$catchPhrase.#'
|
||||
- '#Access to an undefined property Faker\\Generator::\$bs.#'
|
||||
- '#Method Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface::dispatch\(\) invoked with 2 parameters, 1 required.#'
|
||||
excludes_analyse:
|
||||
- %rootDir%/../../../src/Command/KimaiImporterCommand.php
|
||||
- %rootDir%/../../../src/Ldap/LdapDriver.php
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||
DirectoryIndex index.php
|
||||
|
||||
# Do NOT use Kimai with http, this is a huge security risk!
|
||||
# Activate these lines if you configured SSL and certificates:
|
||||
|
||||
#<IfModule mod_rewrite.c>
|
||||
# RewriteEngine On
|
||||
# RewriteCond %{SERVER_PORT} 80
|
||||
# RewriteRule ^(.*)$ https://kimai.example.com/$1 [R,L]
|
||||
#</IfModule>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
public/build/app.98c93d11.js
Normal file
1
public/build/app.98c93d11.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
1
public/build/calendar.e654bdd4.js
Normal file
1
public/build/calendar.e654bdd4.js
Normal file
File diff suppressed because one or more lines are too long
@@ -2,29 +2,29 @@
|
||||
"entrypoints": {
|
||||
"app": {
|
||||
"js": [
|
||||
"build/runtime.784b5d0d.js",
|
||||
"build/0.66b5fd0b.js",
|
||||
"build/1.93f0cf2b.js",
|
||||
"build/app.3f742878.js"
|
||||
"build/runtime.4ee6be68.js",
|
||||
"build/0.a87622f3.js",
|
||||
"build/1.c1bee41f.js",
|
||||
"build/app.98c93d11.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.7f4a18a1.css"
|
||||
"build/app.e8d9cb98.css"
|
||||
]
|
||||
},
|
||||
"chart": {
|
||||
"js": [
|
||||
"build/runtime.784b5d0d.js",
|
||||
"build/0.66b5fd0b.js",
|
||||
"build/2.c06cd055.js",
|
||||
"build/chart.2bb71e2e.js"
|
||||
"build/runtime.4ee6be68.js",
|
||||
"build/0.a87622f3.js",
|
||||
"build/2.7be60d8d.js",
|
||||
"build/chart.0af3f813.js"
|
||||
]
|
||||
},
|
||||
"calendar": {
|
||||
"js": [
|
||||
"build/runtime.784b5d0d.js",
|
||||
"build/0.66b5fd0b.js",
|
||||
"build/1.93f0cf2b.js",
|
||||
"build/calendar.59417f4c.js"
|
||||
"build/runtime.4ee6be68.js",
|
||||
"build/0.a87622f3.js",
|
||||
"build/1.c1bee41f.js",
|
||||
"build/calendar.e654bdd4.js"
|
||||
],
|
||||
"css": [
|
||||
"build/calendar.b0551848.css"
|
||||
@@ -32,14 +32,14 @@
|
||||
}
|
||||
},
|
||||
"integrity": {
|
||||
"build/runtime.784b5d0d.js": "sha384-+AsC7zoefPga1AkczlNSCXcbGwD0TwokkldvNISHf1nMlOpafanLjtKvlZy9cVsm",
|
||||
"build/0.66b5fd0b.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok",
|
||||
"build/1.93f0cf2b.js": "sha384-pmnwzvj+UJhDD3uDf+ekLARMBRtMDxWGbnJDrJvwIoRZd3MW/tOGzAW5Ybhbvw20",
|
||||
"build/app.3f742878.js": "sha384-7221dBEmG8aOmSRCEjuKlrVH47VH1bdnmdEC2pb9y1d9Jtbg6d2t2HfyV3Wa34TI",
|
||||
"build/app.7f4a18a1.css": "sha384-p8Dw7CP1RxxJ2K5yuaNpCw8r1lKd64DlVA2IQeEGxBAC2QxOjweafd8w9CSMk0GC",
|
||||
"build/2.c06cd055.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q",
|
||||
"build/chart.2bb71e2e.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
"build/calendar.59417f4c.js": "sha384-16sOZQy1UmOaoLAVFPfHnF8nFqUxhc4URAg+ypJnnab8uqZAGHDfFFrnq0/6PXl1",
|
||||
"build/runtime.4ee6be68.js": "sha384-xNNrNinl64G3nCUrIskgSjU0mUXXCB9lj6XCSInBTwxSKXk8uTMafnLHtdWdIGtd",
|
||||
"build/0.a87622f3.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok",
|
||||
"build/1.c1bee41f.js": "sha384-7UVWcP6Hefp2k/CrtGSITKXx4dSZqtvpAiU8WX7dClETkzMewrUjoCRtVXQ5j3KI",
|
||||
"build/app.98c93d11.js": "sha384-z6f94h/RD/8JXo1OWkUp7g6TXXV59JW70WA/SdusZh02FcwRvvBIX21k6O6pUBfw",
|
||||
"build/app.e8d9cb98.css": "sha384-UuTUHbhvRNk3t+1gL0JNACbAl5xXH8OhD9ah7jaXDbd32z8dNCjK+5oJSQF77rmz",
|
||||
"build/2.7be60d8d.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q",
|
||||
"build/chart.0af3f813.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
"build/calendar.e654bdd4.js": "sha384-TYdXaKv0q95Z4i/TFUaFHzMOZ0N6T4iuV92ejc0BX7Rpbd0p55ZhbgriPSn5pVcK",
|
||||
"build/calendar.b0551848.css": "sha384-ta519W5+NY1a8FBh7rD4zORYTfUXCYTLci0Z25nf4BfodmuXqyPVNEe7dSe2/fRd"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"build/0.66b5fd0b.js": "build/0.66b5fd0b.js",
|
||||
"build/1.93f0cf2b.js": "build/1.93f0cf2b.js",
|
||||
"build/2.c06cd055.js": "build/2.c06cd055.js",
|
||||
"build/app.css": "build/app.7f4a18a1.css",
|
||||
"build/app.js": "build/app.3f742878.js",
|
||||
"build/0.a87622f3.js": "build/0.a87622f3.js",
|
||||
"build/1.c1bee41f.js": "build/1.c1bee41f.js",
|
||||
"build/2.7be60d8d.js": "build/2.7be60d8d.js",
|
||||
"build/app.css": "build/app.e8d9cb98.css",
|
||||
"build/app.js": "build/app.98c93d11.js",
|
||||
"build/calendar.css": "build/calendar.b0551848.css",
|
||||
"build/calendar.js": "build/calendar.59417f4c.js",
|
||||
"build/chart.js": "build/chart.2bb71e2e.js",
|
||||
"build/runtime.js": "build/runtime.784b5d0d.js",
|
||||
"build/calendar.js": "build/calendar.e654bdd4.js",
|
||||
"build/chart.js": "build/chart.0af3f813.js",
|
||||
"build/runtime.js": "build/runtime.4ee6be68.js",
|
||||
"build/fonts/fa-brands-400.eot": "build/fonts/fa-brands-400.8e49b728.eot",
|
||||
"build/fonts/fa-brands-400.woff": "build/fonts/fa-brands-400.9f018d10.woff",
|
||||
"build/fonts/fa-brands-400.woff2": "build/fonts/fa-brands-400.9f4ce3dc.woff2",
|
||||
|
||||
1
public/build/runtime.4ee6be68.js
Normal file
1
public/build/runtime.4ee6be68.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){function r(r){for(var n,l,i=r[0],f=r[1],a=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var f=t[i];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={runtime:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="build/";var i=window.webpackJsonp=window.webpackJsonp||[],f=i.push.bind(i);i.push=r,i=i.slice();for(var a=0;a<i.length;a++)r(i[a]);var p=f;t()}([]);
|
||||
@@ -1 +0,0 @@
|
||||
!function(e){function r(r){for(var n,i,l=r[0],f=r[1],a=r[2],c=0,s=[];c<l.length;c++)i=l[c],o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var f=t[l];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={runtime:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,function(r){return e[r]}.bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="build/";var l=window.webpackJsonp=window.webpackJsonp||[],f=l.push.bind(l);l.push=r,l=l.slice();for(var a=0;a<l.length;a++)r(l[a]);var p=f;t()}([]);
|
||||
@@ -12,7 +12,7 @@ if (!isset($_SERVER['APP_ENV'])) {
|
||||
if (!class_exists(Dotenv::class)) {
|
||||
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
|
||||
}
|
||||
(new Dotenv())->load(__DIR__.'/../.env');
|
||||
(new Dotenv(true))->load(__DIR__.'/../.env');
|
||||
}
|
||||
|
||||
$env = $_SERVER['APP_ENV'] ?? 'dev';
|
||||
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Event\ActivityMetaDefinitionEvent;
|
||||
use App\Form\API\ActivityApiEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
@@ -36,16 +38,21 @@ class ActivityController extends BaseApiController
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
private $viewHandler;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository)
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,14 +176,15 @@ class ActivityController extends BaseApiController
|
||||
|
||||
$activity = new Activity();
|
||||
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch($event, ActivityMetaDefinitionEvent::class);
|
||||
|
||||
$form = $this->createForm(ActivityApiEditForm::class, $activity);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
$this->repository->saveActivity($activity);
|
||||
|
||||
$view = new View($activity, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
@@ -218,6 +226,8 @@ class ActivityController extends BaseApiController
|
||||
* @param Request $request
|
||||
* @param string $id
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function patchAction(Request $request, string $id)
|
||||
{
|
||||
@@ -231,6 +241,9 @@ class ActivityController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('User cannot update activity');
|
||||
}
|
||||
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch($event, ActivityMetaDefinitionEvent::class);
|
||||
|
||||
$form = $this->createForm(ActivityApiEditForm::class, $activity);
|
||||
|
||||
$form->setData($activity);
|
||||
@@ -243,13 +256,67 @@ class ActivityController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
$this->repository->saveActivity($activity);
|
||||
|
||||
$view = new View($activity, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a meta-field for an existing activity.
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.",
|
||||
* @SWG\Schema(ref="#/definitions/ActivityEntity")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Activity record ID to set the meta-field value for",
|
||||
* required=true,
|
||||
* )
|
||||
* @Rest\RequestParam(name="name", strict=true, nullable=false, description="The meta-field name")
|
||||
* @Rest\RequestParam(name="value", strict=true, nullable=false, description="The meta-field value")
|
||||
*
|
||||
* @param int $id
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function metaAction($id, ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$activity = $this->repository->find($id);
|
||||
|
||||
if (null === $activity) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $activity)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to update this activity');
|
||||
}
|
||||
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch($event, ActivityMetaDefinitionEvent::class);
|
||||
|
||||
$name = $paramFetcher->get('name');
|
||||
$value = $paramFetcher->get('value');
|
||||
|
||||
if (null === ($meta = $activity->getMetaField($name))) {
|
||||
throw new \InvalidArgumentException('Unknown meta-field requested');
|
||||
}
|
||||
|
||||
$meta->setValue($value);
|
||||
|
||||
$this->repository->saveActivity($activity);
|
||||
|
||||
$view = new View($activity, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Form\API\CustomerApiEditForm;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
@@ -36,16 +38,21 @@ class CustomerController extends BaseApiController
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
private $viewHandler;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository)
|
||||
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,6 +75,7 @@ class CustomerController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
@@ -147,14 +155,15 @@ class CustomerController extends BaseApiController
|
||||
|
||||
$customer = new Customer();
|
||||
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event, CustomerMetaDefinitionEvent::class);
|
||||
|
||||
$form = $this->createForm(CustomerApiEditForm::class, $customer);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
$this->repository->saveCustomer($customer);
|
||||
|
||||
$view = new View($customer, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
@@ -196,6 +205,8 @@ class CustomerController extends BaseApiController
|
||||
* @param Request $request
|
||||
* @param string $id
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function patchAction(Request $request, string $id)
|
||||
{
|
||||
@@ -209,6 +220,9 @@ class CustomerController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('User cannot update customer');
|
||||
}
|
||||
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event, CustomerMetaDefinitionEvent::class);
|
||||
|
||||
$form = $this->createForm(CustomerApiEditForm::class, $customer);
|
||||
|
||||
$form->setData($customer);
|
||||
@@ -221,13 +235,67 @@ class CustomerController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
$this->repository->saveCustomer($customer);
|
||||
|
||||
$view = new View($customer, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a meta-field for an existing customer.
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.",
|
||||
* @SWG\Schema(ref="#/definitions/CustomerEntity")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Customer record ID to set the meta-field value for",
|
||||
* required=true,
|
||||
* )
|
||||
* @Rest\RequestParam(name="name", strict=true, nullable=false, description="The meta-field name")
|
||||
* @Rest\RequestParam(name="value", strict=true, nullable=false, description="The meta-field value")
|
||||
*
|
||||
* @param int $id
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function metaAction($id, ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$customer = $this->repository->find($id);
|
||||
|
||||
if (null === $customer) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $customer)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to update this customer');
|
||||
}
|
||||
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch($event, CustomerMetaDefinitionEvent::class);
|
||||
|
||||
$name = $paramFetcher->get('name');
|
||||
$value = $paramFetcher->get('value');
|
||||
|
||||
if (null === ($meta = $customer->getMetaField($name))) {
|
||||
throw new \InvalidArgumentException('Unknown meta-field requested');
|
||||
}
|
||||
|
||||
$meta->setValue($value);
|
||||
|
||||
$this->repository->saveCustomer($customer);
|
||||
|
||||
$view = new View($customer, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Form\API\ProjectApiEditForm;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
@@ -22,6 +23,7 @@ use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
@@ -36,16 +38,21 @@ class ProjectController extends BaseApiController
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
private $viewHandler;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository)
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,6 +77,7 @@ class ProjectController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
@@ -153,14 +161,15 @@ class ProjectController extends BaseApiController
|
||||
|
||||
$project = new Project();
|
||||
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event, ProjectMetaDefinitionEvent::class);
|
||||
|
||||
$form = $this->createForm(ProjectApiEditForm::class, $project);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($project);
|
||||
$entityManager->flush();
|
||||
$this->repository->saveProject($project);
|
||||
|
||||
$view = new View($project, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
@@ -202,6 +211,8 @@ class ProjectController extends BaseApiController
|
||||
* @param Request $request
|
||||
* @param string $id
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function patchAction(Request $request, string $id)
|
||||
{
|
||||
@@ -215,6 +226,9 @@ class ProjectController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('User cannot update project');
|
||||
}
|
||||
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event, ProjectMetaDefinitionEvent::class);
|
||||
|
||||
$form = $this->createForm(ProjectApiEditForm::class, $project);
|
||||
|
||||
$form->setData($project);
|
||||
@@ -227,13 +241,67 @@ class ProjectController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($project);
|
||||
$entityManager->flush();
|
||||
$this->repository->saveProject($project);
|
||||
|
||||
$view = new View($project, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a meta-field for an existing project.
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.",
|
||||
* @SWG\Schema(ref="#/definitions/ProjectEntity")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Project record ID to set the meta-field value for",
|
||||
* required=true,
|
||||
* )
|
||||
* @Rest\RequestParam(name="name", strict=true, nullable=false, description="The meta-field name")
|
||||
* @Rest\RequestParam(name="value", strict=true, nullable=false, description="The meta-field value")
|
||||
*
|
||||
* @param int $id
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function metaAction($id, ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$project = $this->repository->find($id);
|
||||
|
||||
if (null === $project) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $project)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to update this project');
|
||||
}
|
||||
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event, ProjectMetaDefinitionEvent::class);
|
||||
|
||||
$name = $paramFetcher->get('name');
|
||||
$value = $paramFetcher->get('value');
|
||||
|
||||
if (null === ($meta = $project->getMetaField($name))) {
|
||||
throw new \InvalidArgumentException('Unknown meta-field requested');
|
||||
}
|
||||
|
||||
$meta->setValue($value);
|
||||
|
||||
$this->repository->saveProject($project);
|
||||
|
||||
$view = new View($project, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,9 +104,7 @@ class TagController extends BaseApiController
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($tag);
|
||||
$entityManager->flush();
|
||||
$this->repository->deleteTag($tag);
|
||||
|
||||
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||
|
||||
|
||||
133
src/API/TeamController.php
Normal file
133
src/API/TeamController.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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\Team;
|
||||
use App\Repository\TeamRepository;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Team")
|
||||
*/
|
||||
class TeamController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var TeamRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TeamRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all existing teams
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing teams",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref="#/definitions/TeamCollection")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_team')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Team']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one team
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one team entity",
|
||||
* @SWG\Schema(ref="#/definitions/TeamEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
/** @var Team $data */
|
||||
$data = $this->repository->find($id);
|
||||
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a team
|
||||
*
|
||||
* @SWG\Delete(
|
||||
* @SWG\Response(
|
||||
* response=204,
|
||||
* description="Delete one team"
|
||||
* ),
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Team ID to delete",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('delete_team')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction($id)
|
||||
{
|
||||
$team = $this->repository->find($id);
|
||||
|
||||
if (null === $team) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$this->repository->deleteTeam($team);
|
||||
|
||||
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace App\API;
|
||||
use App\Configuration\TimesheetConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Event\TimesheetMetaDefinitionEvent;
|
||||
use App\Form\API\TimesheetApiEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
@@ -30,6 +31,7 @@ use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
@@ -47,27 +49,31 @@ class TimesheetController extends BaseApiController
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
private $viewHandler;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
private $configuration;
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
protected $dateTime;
|
||||
private $dateTime;
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
protected $tagRepository;
|
||||
private $tagRepository;
|
||||
/**
|
||||
* @var TrackingModeService
|
||||
*/
|
||||
protected $trackingModeService;
|
||||
private $trackingModeService;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(
|
||||
ViewHandlerInterface $viewHandler,
|
||||
@@ -75,7 +81,8 @@ class TimesheetController extends BaseApiController
|
||||
UserDateTimeFactory $dateTime,
|
||||
TimesheetConfiguration $configuration,
|
||||
TagRepository $tagRepository,
|
||||
TrackingModeService $trackingModeService
|
||||
TrackingModeService $trackingModeService,
|
||||
EventDispatcherInterface $dispatcher
|
||||
) {
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
@@ -83,6 +90,7 @@ class TimesheetController extends BaseApiController
|
||||
$this->dateTime = $dateTime;
|
||||
$this->tagRepository = $tagRepository;
|
||||
$this->trackingModeService = $trackingModeService;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
protected function getTrackingMode(): TrackingModeInterface
|
||||
@@ -282,6 +290,9 @@ class TimesheetController extends BaseApiController
|
||||
$timesheet->setUser($this->getUser());
|
||||
$timesheet->setBegin($this->dateTime->createDateTime());
|
||||
|
||||
$event = new TimesheetMetaDefinitionEvent($timesheet);
|
||||
$this->dispatcher->dispatch($event, TimesheetMetaDefinitionEvent::class);
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
|
||||
@@ -305,9 +316,7 @@ class TimesheetController extends BaseApiController
|
||||
);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($timesheet);
|
||||
$entityManager->flush();
|
||||
$this->repository->save($timesheet);
|
||||
|
||||
$view = new View($timesheet, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
@@ -362,6 +371,9 @@ class TimesheetController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('You are not allowed to update this timesheet');
|
||||
}
|
||||
|
||||
$event = new TimesheetMetaDefinitionEvent($timesheet);
|
||||
$this->dispatcher->dispatch($event, TimesheetMetaDefinitionEvent::class);
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
|
||||
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [
|
||||
@@ -382,9 +394,7 @@ class TimesheetController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($timesheet);
|
||||
$entityManager->flush();
|
||||
$this->repository->save($timesheet);
|
||||
|
||||
$view = new View($timesheet, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
@@ -409,8 +419,6 @@ class TimesheetController extends BaseApiController
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('delete_own_timesheet') or is_granted('delete_other_timesheet')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
@@ -523,8 +531,6 @@ class TimesheetController extends BaseApiController
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('stop_own_timesheet') or is_granted('stop_other_timesheet')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
* @throws \App\Repository\RepositoryException
|
||||
@@ -568,9 +574,7 @@ class TimesheetController extends BaseApiController
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Rest\RequestParam(name="copy", requirements="all|tags|description", strict=true, nullable=true, description="Whether description and tags are copied to the new entry. Allowed values: all, tags, description (default: nothing is copied)")
|
||||
*
|
||||
* @Security("is_granted('start_own_timesheet') or is_granted('start_other_timesheet')")
|
||||
* @Rest\RequestParam(name="copy", requirements="all|tags|rates|meta|description", strict=true, nullable=true, description="Whether data should be copied to the new entry. Allowed values: all, tags, rates, description, meta (default: nothing is copied)")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
@@ -582,8 +586,6 @@ class TimesheetController extends BaseApiController
|
||||
{
|
||||
/** @var Timesheet $timesheet */
|
||||
$timesheet = $this->repository->find($id);
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
if (null === $timesheet) {
|
||||
throw new NotFoundException();
|
||||
@@ -593,8 +595,11 @@ class TimesheetController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('You are not allowed to re-start this timesheet');
|
||||
}
|
||||
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$copyTimesheet = new Timesheet();
|
||||
$copyTimesheet
|
||||
->setBegin($this->dateTime->createDateTime())
|
||||
->setUser($user)
|
||||
->setActivity($timesheet->getActivity())
|
||||
@@ -602,18 +607,30 @@ class TimesheetController extends BaseApiController
|
||||
;
|
||||
|
||||
if (null !== ($copy = $paramFetcher->get('copy'))) {
|
||||
if (in_array($copy, ['rates', 'all'])) {
|
||||
$copyTimesheet->setHourlyRate($timesheet->getHourlyRate());
|
||||
$copyTimesheet->setFixedRate($timesheet->getFixedRate());
|
||||
}
|
||||
|
||||
if (in_array($copy, ['description', 'all'])) {
|
||||
$entry->setDescription($timesheet->getDescription());
|
||||
$copyTimesheet->setDescription($timesheet->getDescription());
|
||||
}
|
||||
|
||||
if (in_array($copy, ['tags', 'all'])) {
|
||||
foreach ($timesheet->getTags() as $tag) {
|
||||
$entry->addTag($tag);
|
||||
$copyTimesheet->addTag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($copy, ['meta', 'all'])) {
|
||||
foreach ($timesheet->getMetaFields() as $metaField) {
|
||||
$metaNew = clone $metaField;
|
||||
$copyTimesheet->setMetaField($metaNew);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$errors = $validator->validate($entry);
|
||||
$errors = $validator->validate($copyTimesheet);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
throw new BadRequestHttpException($errors[0]->getPropertyPath() . ' = ' . $errors[0]->getMessage());
|
||||
@@ -624,11 +641,9 @@ class TimesheetController extends BaseApiController
|
||||
$this->configuration->getActiveEntriesHardLimit()
|
||||
);
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
$this->repository->save($copyTimesheet);
|
||||
|
||||
$view = new View($entry, 200);
|
||||
$view = new View($copyTimesheet, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
@@ -650,10 +665,10 @@ class TimesheetController extends BaseApiController
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('edit_export_own_timesheet') or is_granted('edit_export_other_timesheet')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function exportAction($id)
|
||||
{
|
||||
@@ -672,9 +687,63 @@ class TimesheetController extends BaseApiController
|
||||
|
||||
$timesheet->setExported(!$timesheet->isExported());
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($timesheet);
|
||||
$entityManager->flush();
|
||||
$this->repository->save($timesheet);
|
||||
|
||||
$view = new View($timesheet, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a meta-field for an existing timesheet.
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Sets the value of an existing/configured meta-field. You cannot create unknown meta-fields, if the given name is not a configured meta-field, this will return an exception.",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Timesheet record ID to set the meta-field value for",
|
||||
* required=true,
|
||||
* )
|
||||
* @Rest\RequestParam(name="name", strict=true, nullable=false, description="The meta-field name")
|
||||
* @Rest\RequestParam(name="value", strict=true, nullable=false, description="The meta-field value")
|
||||
*
|
||||
* @param int $id
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function metaAction($id, ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$timesheet = $this->repository->find($id);
|
||||
|
||||
if (null === $timesheet) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $timesheet)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to update this timesheet');
|
||||
}
|
||||
|
||||
$event = new TimesheetMetaDefinitionEvent($timesheet);
|
||||
$this->dispatcher->dispatch($event, TimesheetMetaDefinitionEvent::class);
|
||||
|
||||
$name = $paramFetcher->get('name');
|
||||
$value = $paramFetcher->get('value');
|
||||
|
||||
if (null === ($meta = $timesheet->getMetaField($name))) {
|
||||
throw new \InvalidArgumentException('Unknown meta-field requested');
|
||||
}
|
||||
|
||||
$meta->setValue($value);
|
||||
|
||||
$this->repository->save($timesheet);
|
||||
|
||||
$view = new View($timesheet, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Doctrine\DBAL\Types\DateTimeType;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* This command was added with v0.8.
|
||||
* Kimai saved the DateTime before with the local timezone, which can cause big problems when used in different environments.
|
||||
* You should convert all timesheet records that were saved with Kimai 2 directly, but NOT the ones migrated from Kimai v1.
|
||||
*
|
||||
* Please read https://github.com/kevinpapst/kimai2/pull/372 to find out more!
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ConvertTimezoneCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @param TimesheetRepository $repository
|
||||
*/
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:convert-timezone')
|
||||
->setDescription('Convert timesheet dates between timezones, only made for updates from 0.7 to 0.8')
|
||||
->setHelp('This command should only be required if you imported data from Kimai v1')
|
||||
->addOption('first-id', 'f', InputArgument::OPTIONAL, 'The database ID which should be converted first')
|
||||
->addOption('last-id', 'l', InputArgument::OPTIONAL, 'The database ID which should be converted last')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $start
|
||||
* @param null|string $end
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getTimesheets($start = null, $end = null)
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('t');
|
||||
|
||||
$qb->select('t');
|
||||
if (!empty($start)) {
|
||||
$qb->andWhere($qb->expr()->gte('t.id', $start));
|
||||
}
|
||||
if (!empty($end)) {
|
||||
$qb->andWhere($qb->expr()->lte('t.id', $end));
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param \DateTimeZone $timeZone
|
||||
* @return Timesheet
|
||||
*/
|
||||
protected function convertTimesheet(Timesheet $timesheet, \DateTimeZone $timeZone): Timesheet
|
||||
{
|
||||
$oldTimezone = $timesheet->getTimezone();
|
||||
|
||||
if (null !== $timesheet->getBegin()) {
|
||||
$beginDate = clone $timesheet->getBegin();
|
||||
$beginDate->setTimezone($timeZone);
|
||||
$timesheet->setBegin($beginDate);
|
||||
}
|
||||
|
||||
if (null !== $timesheet->getEnd()) {
|
||||
$endDate = clone $timesheet->getEnd();
|
||||
$endDate->setTimezone($timeZone);
|
||||
$timesheet->setEnd($endDate);
|
||||
}
|
||||
|
||||
$timesheet->setTimezone($oldTimezone);
|
||||
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
Type::overrideType(Type::DATETIME, DateTimeType::class);
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$start = $input->getOption('first-id');
|
||||
$end = $input->getOption('last-id');
|
||||
|
||||
$result = $this->getTimesheets($start, $end);
|
||||
$amount = count($result);
|
||||
|
||||
$answer = $io->ask(sprintf('This will update %s timesheet records, continue (y/n) ? ', $amount), 'y');
|
||||
|
||||
if ('y' !== $answer) {
|
||||
$io->text('Aborting.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
$i = 0;
|
||||
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($result as $timesheet) {
|
||||
$timesheet = $this->convertTimesheet($timesheet, $utc);
|
||||
$this->repository->save($timesheet);
|
||||
|
||||
if (++$i % 80 === 0) {
|
||||
$io->writeln('. (' . $i . '/' . $amount . ')');
|
||||
} else {
|
||||
$io->write('.');
|
||||
}
|
||||
}
|
||||
|
||||
$io->writeln('. (' . $i . '/' . $amount . ')');
|
||||
$io->writeln('');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,14 @@ class CreateReleaseCommand extends Command
|
||||
->addOption('directory', null, InputOption::VALUE_OPTIONAL, 'Directory where the release package will be stored', 'var/data/')
|
||||
->addOption('release', null, InputOption::VALUE_OPTIONAL, 'The version that should be zipped', Constants::VERSION)
|
||||
;
|
||||
|
||||
/*
|
||||
* Hide this command in production.
|
||||
* Maybe it should be de-activated completely?!
|
||||
*/
|
||||
if (getenv('APP_ENV') === 'prod') {
|
||||
$this->setHidden(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,17 +114,14 @@ class CreateReleaseCommand extends Command
|
||||
$tar .= '.tar.gz';
|
||||
$zip .= '.zip';
|
||||
|
||||
// this removes the current env settings, as they might differ from the release ones
|
||||
// if we don't unset them, the .env file won't be read when executing bin/console commands
|
||||
putenv('DATABASE_URL');
|
||||
putenv('APP_ENV');
|
||||
$prefix = 'APP_ENV=prod DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/kimai.sqlite';
|
||||
|
||||
$commands = [
|
||||
'Clone repository' => $gitCmd . ' ' . $tmpDir,
|
||||
'Install composer dependencies' => 'cd ' . $tmpDir . ' && composer install --no-dev --optimize-autoloader',
|
||||
'Create database' => 'cd ' . $tmpDir . ' && bin/console doctrine:database:create -n',
|
||||
'Create tables' => 'cd ' . $tmpDir . ' && bin/console doctrine:schema:create -n',
|
||||
'Add all migrations' => 'cd ' . $tmpDir . ' && bin/console doctrine:migrations:version --add --all -n',
|
||||
'Create database' => 'cd ' . $tmpDir . ' && ' . $prefix . ' bin/console doctrine:database:create -n',
|
||||
'Create tables' => 'cd ' . $tmpDir . ' && ' . $prefix . ' bin/console doctrine:schema:create -n',
|
||||
'Add all migrations' => 'cd ' . $tmpDir . ' && ' . $prefix . ' bin/console doctrine:migrations:version --add --all -n',
|
||||
];
|
||||
|
||||
$filesToDelete = [
|
||||
|
||||
@@ -106,9 +106,6 @@ class CreateUserCommand extends Command
|
||||
->setRoles(explode(',', $role))
|
||||
;
|
||||
|
||||
$pwd = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($pwd);
|
||||
|
||||
$errors = $this->validator->validate($user, null, ['Registration']);
|
||||
if ($errors->count() > 0) {
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolation $error */
|
||||
@@ -126,6 +123,9 @@ class CreateUserCommand extends Command
|
||||
}
|
||||
|
||||
try {
|
||||
$pwd = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($pwd);
|
||||
|
||||
$entityManager = $this->doctrine->getManager();
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
|
||||
@@ -514,6 +514,10 @@ class KimaiImporterCommand extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($pref['value'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newPref = new UserPreference();
|
||||
$newPref
|
||||
->setName($prefsToImport[$key])
|
||||
@@ -830,9 +834,9 @@ class KimaiImporterCommand extends Command
|
||||
$activity = new Activity();
|
||||
$activity
|
||||
->setName($name)
|
||||
->setComment($oldActivity['comment'] ?: null)
|
||||
->setComment($oldActivity['comment'] ?? null)
|
||||
->setVisible($isActive)
|
||||
->setBudget($oldActivity['budget'] ?: 0)
|
||||
->setBudget($oldActivity['budget'] ?? 0)
|
||||
;
|
||||
|
||||
if (null !== $projectId) {
|
||||
@@ -1059,7 +1063,7 @@ class KimaiImporterCommand extends Command
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
$timesheet
|
||||
->setDescription($oldRecord['description'] ?: ($oldRecord['comment'] ?: null))
|
||||
->setDescription($oldRecord['description'] ?? ($oldRecord['comment'] ?? null))
|
||||
->setUser($this->users[$oldRecord['userID']])
|
||||
->setBegin($begin)
|
||||
->setEnd($end)
|
||||
|
||||
75
src/Command/PluginCommand.php
Normal file
75
src/Command/PluginCommand.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Plugin\PluginManager;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Command used to fetch plugin information.
|
||||
*/
|
||||
class PluginCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var PluginManager
|
||||
*/
|
||||
private $plugins;
|
||||
|
||||
public function __construct(PluginManager $plugins)
|
||||
{
|
||||
$this->plugins = $plugins;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:plugins')
|
||||
->setDescription('Receive plugin information')
|
||||
->setHelp('This command prints detailed plugin information.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$plugins = $this->plugins->getPlugins();
|
||||
if (empty($plugins)) {
|
||||
$io->warning('No plugins installed');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($plugins as $plugin) {
|
||||
$this->plugins->loadMetadata($plugin);
|
||||
$meta = $plugin->getMetadata();
|
||||
$rows[] = [
|
||||
$plugin->getName(),
|
||||
$meta->getVersion(),
|
||||
$meta->getKimaiVersion(),
|
||||
$plugin->getPath(),
|
||||
];
|
||||
}
|
||||
$io->table(['Name', 'Version', 'Requires', 'Directory'], $rows);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Exception;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
@@ -46,6 +47,17 @@ EOT
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that this command CANNOT be executed in production.
|
||||
* It can't work, as the fixtures bundle is not available in production.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return getenv('APP_ENV') !== 'prod';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
@@ -55,17 +67,11 @@ EOT
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if (getenv('APP_ENV') === 'prod') {
|
||||
$io->error('kimai:reset-dev is not allowed in production');
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($this->askConfirmation($input, $output, 'Do you want to create the database y/N ?')) {
|
||||
try {
|
||||
$command = $this->getApplication()->find('doctrine:database:create');
|
||||
$command->run(new ArrayInput([]), $output);
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$io->error('Failed to create database: ' . $ex->getMessage());
|
||||
|
||||
return 1;
|
||||
@@ -76,7 +82,7 @@ EOT
|
||||
try {
|
||||
$command = $this->getApplication()->find('doctrine:schema:drop');
|
||||
$command->run(new ArrayInput(['--force' => true]), $output);
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$io->error('Failed to drop database schema: ' . $ex->getMessage());
|
||||
|
||||
return 2;
|
||||
@@ -85,7 +91,7 @@ EOT
|
||||
try {
|
||||
$command = $this->getApplication()->find('doctrine:schema:create');
|
||||
$command->run(new ArrayInput([]), $output);
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$io->error('Failed to create database schema: ' . $ex->getMessage());
|
||||
|
||||
return 3;
|
||||
@@ -97,7 +103,7 @@ EOT
|
||||
$cmdInput = new ArrayInput([]);
|
||||
$cmdInput->setInteractive(false);
|
||||
$command->run($cmdInput, $output);
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$io->error('Failed to import fixtures: ' . $ex->getMessage());
|
||||
|
||||
return 4;
|
||||
@@ -108,7 +114,7 @@ EOT
|
||||
$cmdInput = new ArrayInput(['--add' => true, '--all' => true]);
|
||||
$cmdInput->setInteractive(false);
|
||||
$command->run($cmdInput, $output);
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$io->error('Failed to set migration status: ' . $ex->getMessage());
|
||||
|
||||
return 5;
|
||||
@@ -118,7 +124,7 @@ EOT
|
||||
$command = $this->getApplication()->find('cache:clear');
|
||||
try {
|
||||
$command->run(new ArrayInput([]), $output);
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
$io->error('Failed to clear cache: ' . $ex->getMessage());
|
||||
|
||||
return 6;
|
||||
|
||||
@@ -97,4 +97,9 @@ class CalendarConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
return $this->find('google.sources');
|
||||
}
|
||||
|
||||
public function getSlotDuration(): string
|
||||
{
|
||||
return (string) $this->find('slot_duration');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,4 +47,9 @@ class FormConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
return $this->find('user.language');
|
||||
}
|
||||
|
||||
public function getUserDefaultCurrency(): string
|
||||
{
|
||||
return $this->find('user.currency');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '1.1';
|
||||
public const VERSION = '1.2';
|
||||
/**
|
||||
* The release name, will only change for new major version
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ActivityMetaDefinitionEvent;
|
||||
@@ -40,14 +41,19 @@ class ActivityController extends AbstractController
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ActivityRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(ActivityRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
@@ -68,6 +74,7 @@ class ActivityController extends AbstractController
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
@@ -197,7 +204,7 @@ class ActivityController extends AbstractController
|
||||
protected function renderActivityForm(Activity $activity, Request $request)
|
||||
{
|
||||
$event = new ActivityMetaDefinitionEvent($activity);
|
||||
$this->dispatcher->dispatch(ActivityMetaDefinitionEvent::class, $event);
|
||||
$this->dispatcher->dispatch($event, ActivityMetaDefinitionEvent::class);
|
||||
|
||||
$editForm = $this->createEditForm($activity);
|
||||
$editForm->handleRequest($request);
|
||||
@@ -250,15 +257,20 @@ class ActivityController extends AbstractController
|
||||
*/
|
||||
private function createEditForm(Activity $activity)
|
||||
{
|
||||
if ($activity->getId() === null) {
|
||||
$url = $this->generateUrl('admin_activity_create');
|
||||
} else {
|
||||
$currency = $this->configuration->getCustomerDefaultCurrency();
|
||||
$url = $this->generateUrl('admin_activity_create');
|
||||
|
||||
if ($activity->getId() !== null) {
|
||||
$url = $this->generateUrl('admin_activity_edit', ['id' => $activity->getId()]);
|
||||
if (null !== $activity->getProject()) {
|
||||
$currency = $activity->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'action' => $url,
|
||||
'method' => 'POST',
|
||||
'currency' => $currency,
|
||||
'create_more' => true,
|
||||
'customer' => true,
|
||||
'include_budget' => $this->isGranted('budget', $activity)
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\CustomerTeamPermissionForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Repository\CustomerRepository;
|
||||
@@ -71,14 +72,11 @@ class CustomerController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_customer", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_customer')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
@@ -98,9 +96,6 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_customer')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -117,12 +112,39 @@ class CustomerController extends AbstractController
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/permissions", name="admin_customer_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('permissions', customer)")
|
||||
*/
|
||||
public function teamPermissions(Customer $customer, Request $request)
|
||||
{
|
||||
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
|
||||
'action' => $this->generateUrl('admin_customer_permissions', ['id' => $customer->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveCustomer($customer);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('customer/permissions.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/budget", name="admin_customer_budget", methods={"GET"})
|
||||
* @Security("is_granted('budget', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Customer $customer)
|
||||
{
|
||||
@@ -135,10 +157,6 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_customer_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Customer $customer, Request $request)
|
||||
{
|
||||
@@ -148,10 +166,6 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function deleteAction(Customer $customer, Request $request)
|
||||
{
|
||||
@@ -169,6 +183,7 @@ class CustomerController extends AbstractController
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
$query->setCustomerToIgnore($customer);
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
@@ -206,7 +221,7 @@ class CustomerController extends AbstractController
|
||||
protected function renderCustomerForm(Customer $customer, Request $request)
|
||||
{
|
||||
$event = new CustomerMetaDefinitionEvent($customer);
|
||||
$this->dispatcher->dispatch(CustomerMetaDefinitionEvent::class, $event);
|
||||
$this->dispatcher->dispatch($event, CustomerMetaDefinitionEvent::class);
|
||||
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
@@ -229,11 +244,7 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(CustomerQuery $query)
|
||||
protected function getToolbarForm(CustomerQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(CustomerToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_customer', [
|
||||
@@ -243,11 +254,7 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return FormInterface
|
||||
*/
|
||||
private function createEditForm(Customer $customer)
|
||||
private function createEditForm(Customer $customer): FormInterface
|
||||
{
|
||||
if ($customer->getId() === null) {
|
||||
$url = $this->generateUrl('admin_customer_create');
|
||||
|
||||
@@ -88,10 +88,7 @@ class DashboardController extends AbstractController
|
||||
$event->addSection($row);
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(
|
||||
DashboardEvent::DASHBOARD,
|
||||
$event
|
||||
);
|
||||
$this->eventDispatcher->dispatch($event, DashboardEvent::DASHBOARD);
|
||||
|
||||
$sections = $event->getSections();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -25,7 +26,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to export timesheet data.
|
||||
*
|
||||
* @Route(path="/export")
|
||||
* @Security("is_granted('view_export')")
|
||||
* @Security("is_granted('create_export')")
|
||||
*/
|
||||
class ExportController extends AbstractController
|
||||
{
|
||||
@@ -54,60 +55,45 @@ class ExportController extends AbstractController
|
||||
$this->dateFactory = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExportQuery
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDefaultQuery()
|
||||
{
|
||||
$begin = $this->dateFactory->createDateTime('first day of this month 00:00:00');
|
||||
$end = $this->dateFactory->createDateTime('last day of this month 23:59:59');
|
||||
|
||||
$query = new ExportQuery();
|
||||
$query->setOrder(ExportQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(ExportQuery::STATE_STOPPED);
|
||||
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="export", methods={"GET"})
|
||||
* @Security("is_granted('view_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
public function indexAction(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
$showPreview = false;
|
||||
$maxItemsPreview = 500;
|
||||
$entries = [];
|
||||
|
||||
$form = $this->getToolbarForm($query, 'GET');
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$entries = $this->getEntries($query);
|
||||
if ($form->isValid()) {
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
$showPreview = true;
|
||||
$query->setPageSize($maxItemsPreview);
|
||||
$entries = $this->getEntries($query);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('export/index.html.twig', [
|
||||
'query' => $query,
|
||||
'entries' => $entries,
|
||||
'form' => $form->createView(),
|
||||
'renderer' => $this->export->getRenderer(),
|
||||
'preview_max' => $maxItemsPreview,
|
||||
'preview_show' => $showPreview,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/data", name="export_data", methods={"POST"})
|
||||
* @Security("is_granted('create_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function export(Request $request)
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
|
||||
@@ -126,8 +112,29 @@ class ExportController extends AbstractController
|
||||
}
|
||||
|
||||
$entries = $this->getEntries($query);
|
||||
$response = $renderer->render($entries, $query);
|
||||
|
||||
return $renderer->render($entries, $query);
|
||||
if ($query->isMarkAsExported()) {
|
||||
$this->timesheetRepository->setExported($entries);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function getDefaultQuery(): ExportQuery
|
||||
{
|
||||
$begin = $this->dateFactory->createDateTime('first day of this month 00:00:00');
|
||||
$end = $this->dateFactory->createDateTime('last day of this month 23:59:59');
|
||||
|
||||
$query = new ExportQuery();
|
||||
$query->setOrder(ExportQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(ExportQuery::STATE_STOPPED);
|
||||
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Form\Type\InitialViewType;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -25,21 +26,41 @@ class HomepageController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="", defaults={}, name="homepage", methods={"GET"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
public function indexAction(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$route = $user->getPreferenceValue('login.initial_view', InitialViewType::DEFAULT_VIEW);
|
||||
$userRoute = $user->getPreferenceValue('login.initial_view', InitialViewType::DEFAULT_VIEW);
|
||||
$userLanguage = $user->getLocale();
|
||||
$requestLanguage = $request->getLocale();
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$locale = $request->getLocale();
|
||||
$language = $user->getPreferenceValue('language', $locale);
|
||||
if (empty($requestLanguage)) {
|
||||
$requestLanguage = User::DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
return $this->redirectToRoute($route, ['_locale' => $language]);
|
||||
if (empty($userLanguage)) {
|
||||
$userLanguage = $requestLanguage;
|
||||
}
|
||||
|
||||
$routes = [
|
||||
[$userRoute, $userLanguage],
|
||||
[$userRoute, $requestLanguage],
|
||||
[$userRoute, User::DEFAULT_LANGUAGE],
|
||||
[InitialViewType::DEFAULT_VIEW, $userLanguage],
|
||||
[InitialViewType::DEFAULT_VIEW, $requestLanguage],
|
||||
];
|
||||
|
||||
foreach ($routes as $routeSettings) {
|
||||
$route = $routeSettings[0];
|
||||
$language = $routeSettings[1];
|
||||
try {
|
||||
return $this->redirectToRoute($route, ['_locale' => $language]);
|
||||
} catch (\Exception $exception) {
|
||||
// something is wrong with the url parameters ...
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute(InitialViewType::DEFAULT_VIEW, ['_locale' => User::DEFAULT_LANGUAGE]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\InvoiceTemplateForm;
|
||||
use App\Form\Toolbar\InvoiceToolbarForm;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Model\InvoiceModel;
|
||||
use App\Repository\InvoiceTemplateRepository;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
@@ -22,14 +22,16 @@ use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage invoices.
|
||||
* Controller used to create invoices and manage invoice templates.
|
||||
*
|
||||
* @Route(path="/invoice")
|
||||
* @Security("is_granted('view_invoice') or is_granted('view_invoice_template')")
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*/
|
||||
class InvoiceController extends AbstractController
|
||||
{
|
||||
@@ -58,25 +60,7 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InvoiceQuery
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDefaultQuery()
|
||||
{
|
||||
$begin = $this->dateTimeFactory->createDateTime('first day of this month');
|
||||
$end = $this->dateTimeFactory->createDateTime('last day of this month');
|
||||
|
||||
$query = new InvoiceQuery();
|
||||
$query->setOrder(InvoiceQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="invoice", methods={"GET"})
|
||||
* @Route(path="/", name="invoice", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
@@ -86,9 +70,14 @@ class InvoiceController extends AbstractController
|
||||
public function indexAction(Request $request, TimesheetRepository $repository)
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
if ($this->isGranted('manage_invoice_template')) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
$this->flashWarning('invoice.first_template');
|
||||
}
|
||||
|
||||
$showPreview = false;
|
||||
$maxItemsPreview = 500;
|
||||
$entries = [];
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
@@ -96,8 +85,22 @@ class InvoiceController extends AbstractController
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
if ($this->isGranted('create_invoice')) {
|
||||
if ($form->isValid()) {
|
||||
/** @var SubmitButton $createButton */
|
||||
$createButton = $form->get('create');
|
||||
if ($createButton->isClicked()) {
|
||||
return $this->renderInvoice($query, $repository);
|
||||
}
|
||||
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
$showPreview = true;
|
||||
$query->setPageSize($maxItemsPreview);
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
@@ -105,34 +108,29 @@ class InvoiceController extends AbstractController
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'form' => $form->createView(),
|
||||
'preview_max' => $maxItemsPreview,
|
||||
'preview_show' => $showPreview,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/print", name="invoice_print", methods={"POST"})
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function printAction(Request $request, TimesheetRepository $repository)
|
||||
protected function getDefaultQuery(): InvoiceQuery
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
$begin = $this->dateTimeFactory->createDateTime('first day of this month');
|
||||
$end = $this->dateTimeFactory->createDateTime('last day of this month');
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query, 'POST');
|
||||
$query = new InvoiceQuery();
|
||||
$query->setOrder(InvoiceQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setExported(InvoiceQuery::STATE_NOT_EXPORTED);
|
||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form->handleRequest($request);
|
||||
return $query;
|
||||
}
|
||||
|
||||
if (!$form->isSubmitted() || !$form->isValid()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
protected function renderInvoice(InvoiceQuery $query, TimesheetRepository $repository)
|
||||
{
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
|
||||
@@ -143,23 +141,28 @@ class InvoiceController extends AbstractController
|
||||
|
||||
foreach ($this->service->getRenderer() as $renderer) {
|
||||
if ($renderer->supports($document)) {
|
||||
return $renderer->render($document, $model);
|
||||
$response = $renderer->render($document, $model);
|
||||
if ($query->isMarkAsExported()) {
|
||||
$repository->setExported($entries);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
$this->flashError('Cannot render invoice: ' . $model->getTemplate()->getRenderer() . ' (' . $document->getName() . ')');
|
||||
$this->flashError(
|
||||
sprintf('Cannot render invoice: %s (%s)', $model->getTemplate()->getRenderer(), $document->getName())
|
||||
);
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param TimesheetRepository $repository
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getEntries(InvoiceQuery $query, TimesheetRepository $repository)
|
||||
protected function getEntries(InvoiceQuery $query, TimesheetRepository $repository): iterable
|
||||
{
|
||||
// customer needs to be defined, as we need the currency for the invoice
|
||||
if (null === $query->getCustomer()) {
|
||||
@@ -180,11 +183,11 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param array $entries
|
||||
* @param Timesheet[] $entries
|
||||
* @return InvoiceModel
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function prepareModel(InvoiceQuery $query, array $entries)
|
||||
protected function prepareModel(InvoiceQuery $query, array $entries): InvoiceModel
|
||||
{
|
||||
$model = new InvoiceModel();
|
||||
$model
|
||||
@@ -213,33 +216,23 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template", defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Route(path="/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice_template')")
|
||||
*
|
||||
* @param int $page
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @Route(path="/template", name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function listTemplateAction($page)
|
||||
public function listTemplateAction(): Response
|
||||
{
|
||||
$templates = $this->invoiceRepository->findByQuery(new BaseQuery());
|
||||
|
||||
return $this->render('invoice/templates.html.twig', [
|
||||
'entries' => $templates,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/template/{id}/edit", name="admin_invoice_template_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request)
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
return $this->renderTemplateForm($template, $request);
|
||||
}
|
||||
@@ -247,14 +240,9 @@ class InvoiceController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/template/create", name="admin_invoice_template_create", methods={"GET", "POST"})
|
||||
* @Route(path="/template/create/{id}", name="admin_invoice_template_copy", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_invoice_template')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @param InvoiceTemplate|null $copyFrom
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom)
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom): Response
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
$this->flashWarning('invoice.first_template');
|
||||
@@ -280,16 +268,10 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing template.
|
||||
*
|
||||
* @Route(path="/template/{id}/delete", name="admin_invoice_template_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @Security("is_granted('manage_invoice_template')")
|
||||
*/
|
||||
public function deleteTemplate(InvoiceTemplate $template, Request $request)
|
||||
public function deleteTemplate(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
try {
|
||||
$this->invoiceRepository->removeTemplate($template);
|
||||
@@ -298,15 +280,10 @@ class InvoiceController extends AbstractController
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template_paginated', ['page' => $request->get('page')]);
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request)
|
||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request): Response
|
||||
{
|
||||
$editForm = $this->createEditForm($template);
|
||||
|
||||
@@ -340,11 +317,7 @@ class InvoiceController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(InvoiceTemplate $template)
|
||||
private function createEditForm(InvoiceTemplate $template): FormInterface
|
||||
{
|
||||
if ($template->getId() === null) {
|
||||
$url = $this->generateUrl('admin_invoice_template_create');
|
||||
|
||||
@@ -16,12 +16,14 @@ use App\Form\UserEditType;
|
||||
use App\Form\UserPasswordType;
|
||||
use App\Form\UserPreferencesForm;
|
||||
use App\Form\UserRolesType;
|
||||
use App\Form\UserTeamsType;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Voter\UserVoter;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
@@ -175,6 +177,28 @@ class ProfileController extends AbstractController
|
||||
return $this->getProfileView($profile, 'roles', null, null, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/teams", name="user_profile_teams", methods={"GET", "POST"})
|
||||
* @Security("is_granted('teams', profile)")
|
||||
*/
|
||||
public function teamsAction(User $profile, Request $request)
|
||||
{
|
||||
$form = $this->createTeamsForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_teams', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'teams', null, null, null, null, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
|
||||
* @Security("is_granted('preferences', profile)")
|
||||
@@ -183,7 +207,7 @@ class ProfileController extends AbstractController
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($profile);
|
||||
$this->dispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
$this->dispatcher->dispatch($event, PrepareUserEvent::PREPARE);
|
||||
|
||||
$original = [];
|
||||
foreach ($profile->getPreferences() as $preference) {
|
||||
@@ -236,24 +260,15 @@ class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string $tab
|
||||
* @param Form|null $editForm
|
||||
* @param Form|null $pwdForm
|
||||
* @param Form|null $rolesForm
|
||||
* @param Form|null $apiTokenForm
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
protected function getProfileView(
|
||||
User $user,
|
||||
string $tab,
|
||||
Form $editForm = null,
|
||||
Form $pwdForm = null,
|
||||
Form $rolesForm = null,
|
||||
Form $apiTokenForm = null
|
||||
) {
|
||||
FormInterface $editForm = null,
|
||||
FormInterface $pwdForm = null,
|
||||
FormInterface $rolesForm = null,
|
||||
FormInterface $apiTokenForm = null,
|
||||
FormInterface $teamsForm = null
|
||||
): Response {
|
||||
$forms = [];
|
||||
|
||||
if ($this->isGranted(UserVoter::EDIT, $user)) {
|
||||
@@ -268,6 +283,10 @@ class ProfileController extends AbstractController
|
||||
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
|
||||
$forms['api-token'] = $apiTokenForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::TEAMS, $user)) {
|
||||
$teamsForm = $teamsForm ?: $this->createTeamsForm($user);
|
||||
$forms['teams'] = $teamsForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::ROLES, $user)) {
|
||||
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
||||
$forms['roles'] = $rolesForm->createView();
|
||||
@@ -280,15 +299,11 @@ class ProfileController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createPreferencesForm(User $user)
|
||||
private function createPreferencesForm(User $user): FormInterface
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($user);
|
||||
$this->dispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
$this->dispatcher->dispatch($event, PrepareUserEvent::PREPARE);
|
||||
|
||||
return $this->createForm(
|
||||
UserPreferencesForm::class,
|
||||
@@ -300,11 +315,7 @@ class ProfileController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(User $user)
|
||||
private function createEditForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserEditType::class,
|
||||
@@ -317,11 +328,7 @@ class ProfileController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createRolesForm(User $user)
|
||||
private function createRolesForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserRolesType::class,
|
||||
@@ -333,28 +340,32 @@ class ProfileController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createPasswordForm(User $user)
|
||||
private function createTeamsForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserTeamsType::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('user_profile_teams', ['username' => $user->getUsername()]),
|
||||
'method' => 'POST',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function createPasswordForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserPasswordType::class,
|
||||
$user,
|
||||
[
|
||||
'validation_groups' => ['passwordUpdate'],
|
||||
'validation_groups' => ['PasswordUpdate'],
|
||||
'action' => $this->generateUrl('user_profile_password', ['username' => $user->getUsername()]),
|
||||
'method' => 'POST'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createApiTokenForm(User $user)
|
||||
private function createApiTokenForm(User $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserApiTokenType::class,
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Form\ProjectTeamPermissionForm;
|
||||
use App\Form\Toolbar\ProjectToolbarForm;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\ProjectRepository;
|
||||
@@ -40,14 +42,19 @@ class ProjectController extends AbstractController
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
public function __construct(ProjectRepository $repository, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(ProjectRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
@@ -60,14 +67,11 @@ class ProjectController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_project", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_project')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
@@ -85,14 +89,40 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/permissions", name="admin_project_permissions", methods={"GET", "POST"})
|
||||
* @Security("is_granted('permissions', project)")
|
||||
*/
|
||||
public function teamPermissions(Project $project, Request $request)
|
||||
{
|
||||
$form = $this->createForm(ProjectTeamPermissionForm::class, $project, [
|
||||
'action' => $this->generateUrl('admin_project_permissions', ['id' => $project->getId()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->getRepository()->saveProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_project');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('project/permissions.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_project_create", methods={"GET", "POST"})
|
||||
* @Route(path="/create/{customer}", name="admin_project_create_with_customer", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_project')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Customer|null $customer
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createAction(Request $request, ?Customer $customer = null)
|
||||
{
|
||||
@@ -108,9 +138,6 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/budget", name="admin_project_budget", methods={"GET"})
|
||||
* @Security("is_granted('budget', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
* @return Response
|
||||
*/
|
||||
public function budgetAction(Project $project)
|
||||
{
|
||||
@@ -123,10 +150,6 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_project_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Project $project, Request $request)
|
||||
{
|
||||
@@ -136,10 +159,6 @@ class ProjectController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function deleteAction(Project $project, Request $request)
|
||||
{
|
||||
@@ -158,6 +177,7 @@ class ProjectController extends AbstractController
|
||||
$query = new ProjectFormTypeQuery();
|
||||
$query->setCustomer($project->getCustomer());
|
||||
$query->setProjectToIgnore($project);
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
@@ -195,7 +215,7 @@ class ProjectController extends AbstractController
|
||||
protected function renderProjectForm(Project $project, Request $request)
|
||||
{
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch(ProjectMetaDefinitionEvent::class, $event);
|
||||
$this->dispatcher->dispatch($event, ProjectMetaDefinitionEvent::class);
|
||||
|
||||
$editForm = $this->createEditForm($project);
|
||||
$editForm->handleRequest($request);
|
||||
@@ -225,11 +245,7 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectQuery $query
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ProjectQuery $query)
|
||||
protected function getToolbarForm(ProjectQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(ProjectToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_project', [
|
||||
@@ -239,16 +255,12 @@ class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return FormInterface
|
||||
*/
|
||||
private function createEditForm(Project $project)
|
||||
private function createEditForm(Project $project): FormInterface
|
||||
{
|
||||
if ($project->getId() === null) {
|
||||
$url = $this->generateUrl('admin_project_create');
|
||||
$currency = Customer::DEFAULT_CURRENCY;
|
||||
} else {
|
||||
$currency = $this->configuration->getCustomerDefaultCurrency();
|
||||
$url = $this->generateUrl('admin_project_create');
|
||||
|
||||
if ($project->getId() !== null) {
|
||||
$url = $this->generateUrl('admin_project_edit', ['id' => $project->getId()]);
|
||||
$currency = $project->getCustomer()->getCurrency();
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ class SystemConfigurationController extends AbstractController
|
||||
$types = $this->getConfigurationTypes();
|
||||
|
||||
$event = new SystemConfigurationEvent($types);
|
||||
$this->eventDispatcher->dispatch(SystemConfigurationEvent::CONFIGURE, $event);
|
||||
$this->eventDispatcher->dispatch($event, SystemConfigurationEvent::CONFIGURE);
|
||||
|
||||
foreach ($event->getConfigurations() as $configs) {
|
||||
foreach ($configs->getConfiguration() as $config) {
|
||||
@@ -248,6 +248,10 @@ class SystemConfigurationController extends AbstractController
|
||||
->setName('defaults.user.theme')
|
||||
->setLabel('skin')
|
||||
->setType(SkinType::class),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.currency')
|
||||
->setLabel('currency')
|
||||
->setType(CurrencyType::class),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_THEME)
|
||||
|
||||
185
src/Controller/TeamController.php
Normal file
185
src/Controller/TeamController.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\TeamCustomerForm;
|
||||
use App\Form\TeamEditForm;
|
||||
use App\Form\TeamProjectForm;
|
||||
use App\Form\Toolbar\TeamToolbarForm;
|
||||
use App\Repository\Query\TeamQuery;
|
||||
use App\Repository\TeamRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/admin/teams")
|
||||
* @Security("is_granted('view_team')")
|
||||
*/
|
||||
class TeamController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var TeamRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(TeamRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_team", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_team_paginated", methods={"GET"})
|
||||
*
|
||||
* @param TeamRepository $repository
|
||||
* @param Request $request
|
||||
* @param int $page
|
||||
* @return Response
|
||||
*/
|
||||
public function listTeams(TeamRepository $repository, Request $request, $page)
|
||||
{
|
||||
$query = new TeamQuery();
|
||||
$query->setPage($page);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$teams = $repository->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('team/index.html.twig', [
|
||||
'teams' => $teams,
|
||||
'query' => $query,
|
||||
'showFilter' => $query->isDirty(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_team_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_team')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
public function createTeam(Request $request)
|
||||
{
|
||||
return $this->renderEditScreen(new Team(), $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_team_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', team)")
|
||||
*/
|
||||
public function editAction(Team $team, Request $request)
|
||||
{
|
||||
return $this->renderEditScreen($team, $request);
|
||||
}
|
||||
|
||||
private function renderEditScreen(Team $team, Request $request): Response
|
||||
{
|
||||
$customerForm = null;
|
||||
$projectForm = null;
|
||||
|
||||
if ($team->getId() === null) {
|
||||
$url = $this->generateUrl('admin_team_create');
|
||||
} else {
|
||||
$url = $this->generateUrl('admin_team_edit', ['id' => $team->getId()]);
|
||||
}
|
||||
|
||||
$editForm = $this->createForm(TeamEditForm::class, $team, [
|
||||
'action' => $url,
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
if ($request->isMethod('POST') && (null !== ($editFormValues = $request->get($editForm->getName())))) {
|
||||
$editForm->submit($editFormValues, true);
|
||||
|
||||
if ($editForm->isValid()) {
|
||||
try {
|
||||
// make sure that the teamlead is always part of the team, otherwise permission checks
|
||||
// and filtering might not work as expected!
|
||||
$team->addUser($team->getTeamLead());
|
||||
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $team->getId()) {
|
||||
$customerForm = $this->createForm(TeamCustomerForm::class, $team, [
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
if ($request->isMethod('POST') && (null !== ($customerFormValues = $request->get($customerForm->getName())))) {
|
||||
$customerForm->submit($customerFormValues, true);
|
||||
|
||||
if ($customerForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$projectForm = $this->createForm(TeamProjectForm::class, $team, [
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
if ($request->isMethod('POST') && (null !== ($projectFormValues = $request->get($projectForm->getName())))) {
|
||||
$projectForm->submit($projectFormValues, true);
|
||||
|
||||
if ($projectForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveTeam($team);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('team/edit.html.twig', [
|
||||
'team' => $team,
|
||||
'form' => $editForm->createView(),
|
||||
'customerForm' => $customerForm ? $customerForm->createView() : null,
|
||||
'projectForm' => $projectForm ? $projectForm->createView() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getToolbarForm(TeamQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(TeamToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_team', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -112,9 +112,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
|
||||
$dirtyQuery = $query->isDirty();
|
||||
|
||||
if (!$this->includeUserInForms()) {
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
$this->prepareQuery($query);
|
||||
|
||||
$pager = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
|
||||
@@ -138,7 +136,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
protected function edit(Timesheet $entry, Request $request, string $renderTemplate)
|
||||
{
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch(TimesheetMetaDefinitionEvent::class, $event);
|
||||
$this->dispatcher->dispatch($event, TimesheetMetaDefinitionEvent::class);
|
||||
|
||||
$editForm = $this->getEditForm($entry, $request->get('page'));
|
||||
$editForm->handleRequest($request);
|
||||
@@ -196,7 +194,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch(TimesheetMetaDefinitionEvent::class, $event);
|
||||
$this->dispatcher->dispatch($event, TimesheetMetaDefinitionEvent::class);
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
$mode->create($entry, $request);
|
||||
@@ -253,9 +251,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
|
||||
if (!$this->includeUserInForms()) {
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
$this->prepareQuery($query);
|
||||
|
||||
$entries = $this->getRepository()->getTimesheetsForQuery($query);
|
||||
|
||||
@@ -265,6 +261,11 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
{
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
|
||||
protected function getCreateForm(Timesheet $entry, TrackingModeInterface $mode): FormInterface
|
||||
{
|
||||
return $this->createForm($this->getCreateFormClassName(), $entry, [
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetAdminEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -76,6 +77,11 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
||||
}
|
||||
|
||||
protected function prepareQuery(TimesheetQuery $query)
|
||||
{
|
||||
$query->setCurrentUser($this->getUser());
|
||||
}
|
||||
|
||||
protected function getCreateFormClassName(): string
|
||||
{
|
||||
return TimesheetAdminEditForm::class;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use App\Timesheet\Rounding\RoundingInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
@@ -250,6 +251,7 @@ class Configuration implements ConfigurationInterface
|
||||
->children()
|
||||
->booleanNode('week_numbers')->defaultTrue()->end()
|
||||
->integerNode('day_limit')->defaultValue(4)->end()
|
||||
->scalarNode('slot_duration')->defaultValue('00:30:00')->end()
|
||||
->arrayNode('businessHours')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
@@ -438,7 +440,7 @@ class Configuration implements ConfigurationInterface
|
||||
->children()
|
||||
->scalarNode('timezone')->defaultNull()->end()
|
||||
->scalarNode('country')->defaultValue('DE')->end()
|
||||
->scalarNode('currency')->defaultValue('EUR')->end()
|
||||
->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('user')
|
||||
@@ -447,9 +449,9 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('timezone')->defaultNull()->end()
|
||||
->scalarNode('language')->defaultValue(User::DEFAULT_LANGUAGE)->end()
|
||||
->scalarNode('theme')->defaultNull()->end()
|
||||
->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end()
|
||||
->end()
|
||||
->end()
|
||||
|
||||
->end()
|
||||
;
|
||||
|
||||
|
||||
@@ -52,6 +52,13 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
return 'kimai2_' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we should deactivate foreign key support for SQLite.
|
||||
* This is required, if columns are changed.
|
||||
* SQLite will drop the table and therefor all referenced data if we don't deactivate this.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isSupportingForeignKeys(): bool
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -50,16 +50,18 @@ class Activity implements EntityWithMetaFields
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=255, nullable=false)
|
||||
* Do not increase length to more than 190 chars, otherwise "Index column size too large." will be triggered.
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=255)
|
||||
* @Assert\Length(min=2, max=150)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", length=65535, nullable=true)
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
|
||||
@@ -40,9 +40,11 @@ class Customer implements EntityWithMetaFields
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=255, nullable=false)
|
||||
* Do not increase length to more than 190 chars, otherwise "Index column size too large." will be triggered.
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=255)
|
||||
* @Assert\Length(min=2, max=150)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
@@ -50,13 +52,14 @@ class Customer implements EntityWithMetaFields
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="number", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $number;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", length=65535, nullable=true)
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
@@ -72,6 +75,7 @@ class Customer implements EntityWithMetaFields
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="company", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $company;
|
||||
|
||||
@@ -79,13 +83,14 @@ class Customer implements EntityWithMetaFields
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="contact", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $contact;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="address", type="text", length=65535, nullable=true)
|
||||
* @ORM\Column(name="address", type="text", nullable=true)
|
||||
*/
|
||||
private $address;
|
||||
|
||||
@@ -94,6 +99,7 @@ class Customer implements EntityWithMetaFields
|
||||
*
|
||||
* @ORM\Column(name="country", type="string", length=2, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=2)
|
||||
*/
|
||||
private $country;
|
||||
|
||||
@@ -102,6 +108,7 @@ class Customer implements EntityWithMetaFields
|
||||
*
|
||||
* @ORM\Column(name="currency", type="string", length=3, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=3)
|
||||
*/
|
||||
private $currency = self::DEFAULT_CURRENCY;
|
||||
|
||||
@@ -109,6 +116,7 @@ class Customer implements EntityWithMetaFields
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="phone", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $phone;
|
||||
|
||||
@@ -116,6 +124,7 @@ class Customer implements EntityWithMetaFields
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="fax", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $fax;
|
||||
|
||||
@@ -123,13 +132,17 @@ class Customer implements EntityWithMetaFields
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="mobile", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $mobile;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* Limited via RFC to 254 chars
|
||||
*
|
||||
* @ORM\Column(name="email", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=254)
|
||||
*/
|
||||
private $email;
|
||||
|
||||
@@ -137,14 +150,18 @@ class Customer implements EntityWithMetaFields
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="homepage", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $homepage;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=255, nullable=false)
|
||||
* Length was determined by a MySQL column via "use mysql;describe time_zone_name;"
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=64, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=64)
|
||||
*/
|
||||
private $timezone;
|
||||
|
||||
@@ -160,9 +177,26 @@ class Customer implements EntityWithMetaFields
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
/**
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="customers")
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_customers_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="customer_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
private $teams;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -399,6 +433,33 @@ class Customer implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team)
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->teams->add($team);
|
||||
$team->addCustomer($this);
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
}
|
||||
$this->teams->removeElement($team);
|
||||
$team->removeCustomer($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Team>
|
||||
*/
|
||||
public function getTeams(): Collection
|
||||
{
|
||||
return $this->teams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -59,7 +59,7 @@ class InvoiceTemplate
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="address", type="text", length=65535, nullable=true)
|
||||
* @ORM\Column(name="address", type="text", nullable=true)
|
||||
*/
|
||||
private $address;
|
||||
|
||||
@@ -105,7 +105,7 @@ class InvoiceTemplate
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="payment_terms", type="text", length=65535, nullable=true)
|
||||
* @ORM\Column(name="payment_terms", type="text", nullable=true)
|
||||
*/
|
||||
private $paymentTerms;
|
||||
|
||||
|
||||
@@ -49,9 +49,11 @@ class Project implements EntityWithMetaFields
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=255, nullable=false)
|
||||
* Do not increase length to more than 190 chars, otherwise "Index column size too large." will be triggered.
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Length(min=2, max=255)
|
||||
* @Assert\Length(min=2, max=150)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
@@ -66,7 +68,7 @@ class Project implements EntityWithMetaFields
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", length=65535, nullable=true)
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
@@ -90,9 +92,26 @@ class Project implements EntityWithMetaFields
|
||||
*/
|
||||
private $meta;
|
||||
|
||||
/**
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="projects")
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_projects_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="project_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
private $teams;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -223,6 +242,33 @@ class Project implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team)
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->teams->add($team);
|
||||
$team->addProject($this);
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
}
|
||||
$this->teams->removeElement($team);
|
||||
$team->removeProject($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Team>
|
||||
*/
|
||||
public function getTeams(): Collection
|
||||
{
|
||||
return $this->teams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
210
src/Entity/Team.php
Normal file
210
src/Entity/Team.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_teams",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\TeamRepository")
|
||||
* @UniqueEntity("name")
|
||||
*/
|
||||
class Team
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=100, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=100)
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $teamlead;
|
||||
/**
|
||||
* @var User[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="User", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $users;
|
||||
/**
|
||||
* @var Customer[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Customer", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $customers;
|
||||
/**
|
||||
* @var Project[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Project", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $projects;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->users = new ArrayCollection();
|
||||
$this->customers = new ArrayCollection();
|
||||
$this->projects = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $name): Team
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getTeamLead(): ?User
|
||||
{
|
||||
return $this->teamlead;
|
||||
}
|
||||
|
||||
public function isTeamlead(User $user): bool
|
||||
{
|
||||
return $this->teamlead === $user;
|
||||
}
|
||||
|
||||
public function setTeamLead(User $teamlead): Team
|
||||
{
|
||||
$this->teamlead = $teamlead;
|
||||
$this->addUser($teamlead);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasUser(User $user): bool
|
||||
{
|
||||
return $this->users->contains($user);
|
||||
}
|
||||
|
||||
public function addUser(User $user)
|
||||
{
|
||||
if ($this->users->contains($user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->users->add($user);
|
||||
$user->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeUser(User $user)
|
||||
{
|
||||
if (!$this->users->contains($user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->users->removeElement($user);
|
||||
$user->removeTeam($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<User>
|
||||
*/
|
||||
public function getUsers(): iterable
|
||||
{
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
public function addCustomer(Customer $customer)
|
||||
{
|
||||
if ($this->customers->contains($customer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->customers->add($customer);
|
||||
$customer->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeCustomer(Customer $customer)
|
||||
{
|
||||
if (!$this->customers->contains($customer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->customers->removeElement($customer);
|
||||
$customer->removeTeam($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Customer>
|
||||
*/
|
||||
public function getCustomers(): iterable
|
||||
{
|
||||
return $this->customers;
|
||||
}
|
||||
|
||||
public function addProject(Project $project)
|
||||
{
|
||||
if ($this->projects->contains($project)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->projects->add($project);
|
||||
$project->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeProject(Project $project)
|
||||
{
|
||||
if (!$this->projects->contains($project)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->projects->removeElement($project);
|
||||
$project->removeTeam($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Project>
|
||||
*/
|
||||
public function getProjects(): iterable
|
||||
{
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -48,7 +50,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var DateTime
|
||||
*
|
||||
* @ORM\Column(name="start_time", type="datetime", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
@@ -56,7 +58,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
private $begin;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var DateTime
|
||||
*
|
||||
* @ORM\Column(name="end_time", type="datetime", nullable=true)
|
||||
*/
|
||||
@@ -112,7 +114,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="description", type="text", length=65535, nullable=true)
|
||||
* @ORM\Column(name="description", type="text", nullable=true)
|
||||
*/
|
||||
private $description;
|
||||
|
||||
@@ -142,10 +144,10 @@ class Timesheet implements EntityWithMetaFields
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_timesheet_tags",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id")
|
||||
* @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
|
||||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
@@ -188,17 +190,17 @@ class Timesheet implements EntityWithMetaFields
|
||||
}
|
||||
|
||||
if (null !== $this->begin) {
|
||||
$this->begin->setTimeZone(new \DateTimeZone($this->timezone));
|
||||
$this->begin->setTimeZone(new DateTimeZone($this->timezone));
|
||||
}
|
||||
|
||||
if (null !== $this->end) {
|
||||
$this->end->setTimeZone(new \DateTimeZone($this->timezone));
|
||||
$this->end->setTimeZone(new DateTimeZone($this->timezone));
|
||||
}
|
||||
|
||||
$this->localized = true;
|
||||
}
|
||||
|
||||
public function getBegin(): ?\DateTime
|
||||
public function getBegin(): ?DateTime
|
||||
{
|
||||
$this->localizeDates();
|
||||
|
||||
@@ -206,10 +208,10 @@ class Timesheet implements EntityWithMetaFields
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $begin
|
||||
* @param DateTime $begin
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setBegin(\DateTime $begin): Timesheet
|
||||
public function setBegin(DateTime $begin): Timesheet
|
||||
{
|
||||
$this->begin = $begin;
|
||||
$this->timezone = $begin->getTimezone()->getName();
|
||||
@@ -217,7 +219,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnd(): ?\DateTime
|
||||
public function getEnd(): ?DateTime
|
||||
{
|
||||
$this->localizeDates();
|
||||
|
||||
@@ -225,10 +227,10 @@ class Timesheet implements EntityWithMetaFields
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $end
|
||||
* @param DateTime $end
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setEnd(?\DateTime $end): Timesheet
|
||||
public function setEnd(?DateTime $end): Timesheet
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
@@ -422,10 +424,10 @@ class Timesheet implements EntityWithMetaFields
|
||||
}
|
||||
|
||||
/**
|
||||
* BE WARNED: this method should NOT be used programmatically, there is very likely no reason for it!
|
||||
* BE WARNED: this method should NOT be used.
|
||||
* It was ONLY introduced for the command "kimai:import-v1".
|
||||
*
|
||||
* @internal
|
||||
* @deprecated since it was introduced, only meant for the initial migration. Will be removed with 1.0.
|
||||
* @param string $timezone
|
||||
* @return Timesheet
|
||||
*/
|
||||
|
||||
@@ -95,6 +95,22 @@ class User extends BaseUser implements UserInterface
|
||||
*/
|
||||
private $preferences;
|
||||
|
||||
/**
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", inversedBy="users", cascade={"persist"})
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_users_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
private $teams;
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
*/
|
||||
@@ -103,6 +119,7 @@ class User extends BaseUser implements UserInterface
|
||||
parent::__construct();
|
||||
$this->registeredAt = new \DateTime();
|
||||
$this->preferences = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -274,6 +291,55 @@ class User extends BaseUser implements UserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team): User
|
||||
{
|
||||
if ($this->teams->contains($team)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->teams->add($team);
|
||||
$team->addUser($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTeam(Team $team)
|
||||
{
|
||||
if (!$this->teams->contains($team)) {
|
||||
return;
|
||||
}
|
||||
$this->teams->removeElement($team);
|
||||
$team->removeUser($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Team>
|
||||
*/
|
||||
public function getTeams(): Collection
|
||||
{
|
||||
return $this->teams;
|
||||
}
|
||||
|
||||
public function isInTeam(Team $team): bool
|
||||
{
|
||||
return $this->teams->contains($team);
|
||||
}
|
||||
|
||||
public function isTeamleadOf(Team $team): bool
|
||||
{
|
||||
return $team->getTeamLead() === $this;
|
||||
}
|
||||
|
||||
public function isTeamlead(): bool
|
||||
{
|
||||
return $this->hasRole(static::ROLE_TEAMLEAD);
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->hasRole(static::ROLE_ADMIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,6 @@ class UserPreference
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
@@ -47,7 +46,6 @@ class UserPreference
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
@@ -55,28 +53,29 @@ class UserPreference
|
||||
* @Assert\Length(min=2, max=50)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="value", type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
private $type;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $enabled = true;
|
||||
|
||||
private $enabled = true;
|
||||
/**
|
||||
* @var Constraint[]
|
||||
*/
|
||||
protected $constraints = [];
|
||||
private $constraints = [];
|
||||
/**
|
||||
* An array of options for the form element
|
||||
* @var array
|
||||
*/
|
||||
private $options = [];
|
||||
|
||||
/**
|
||||
* @return int
|
||||
@@ -237,4 +236,27 @@ class UserPreference
|
||||
{
|
||||
return $this->constraints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an array of options for the FormType.
|
||||
*
|
||||
* @param array $options
|
||||
* @return UserPreference
|
||||
*/
|
||||
public function setOptions(array $options): UserPreference
|
||||
{
|
||||
$this->options = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with options for the FormType.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to activities
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemModel;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* The ConfigureAdminMenuEvent is used for populating the administration navigation.
|
||||
*/
|
||||
final class ConfigureAdminMenuEvent extends Event
|
||||
{
|
||||
public const CONFIGURE = 'app.admin_menu_configure';
|
||||
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
private $request;
|
||||
/**
|
||||
* @var MenuItemModel
|
||||
*/
|
||||
private $menu;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param MenuItemModel $menu
|
||||
*/
|
||||
public function __construct(Request $request, MenuItemModel $menu)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->menu = $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MenuItemModel
|
||||
*/
|
||||
public function getAdminMenu()
|
||||
{
|
||||
return $this->menu;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ namespace App\Event;
|
||||
|
||||
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemModel;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* The ConfigureMainMenuEvent is used for populating the main navigation.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to customers
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Widget\WidgetContainerInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class DashboardEvent extends Event
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if a user profile is loaded and want to fill the dynamic user preferences
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to projects
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Form\Model\SystemConfiguration;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if system configurations should be changed/added dynamically.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class ThemeEvent extends Event
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event can be used, to dynamically add meta fields to timesheets
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if further user preferences should added dynamically
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Event\ConfigureAdminMenuEvent;
|
||||
use App\Event\ConfigureMainMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Event\ThemeEvents;
|
||||
@@ -73,16 +72,7 @@ class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('system', 'menu.system', '')
|
||||
);
|
||||
|
||||
$this->eventDispatcher->dispatch(ConfigureMainMenuEvent::CONFIGURE, $menuEvent);
|
||||
|
||||
// @deprecated since 0.9, will be removed with 1.0
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureAdminMenuEvent::CONFIGURE,
|
||||
new ConfigureAdminMenuEvent(
|
||||
$request,
|
||||
$menuEvent->getAdminMenu()
|
||||
)
|
||||
);
|
||||
$this->eventDispatcher->dispatch($menuEvent, ConfigureMainMenuEvent::CONFIGURE);
|
||||
|
||||
if ($menuEvent->getAdminMenu()->hasChildren()) {
|
||||
$event->addItem(new MenuItemModel('admin', 'menu.admin', ''));
|
||||
|
||||
@@ -84,7 +84,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_export')) {
|
||||
if ($auth->isGranted('create_export')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('export', 'menu.export', 'export', [], $this->getIcon('export'))
|
||||
);
|
||||
@@ -142,6 +142,12 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_team')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('user_team', 'menu.admin_team', 'admin_team', [], $this->getIcon('team'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('plugins')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], $this->getIcon('plugin'))
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\EventSubscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
@@ -72,9 +72,6 @@ class RedirectToLocaleSubscriber implements EventSubscriberInterface
|
||||
$this->locales = array_unique($this->locales);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -82,10 +79,7 @@ class RedirectToLocaleSubscriber implements EventSubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GetResponseEvent $event
|
||||
*/
|
||||
public function onKernelRequest(GetResponseEvent $event)
|
||||
public function onKernelRequest(RequestEvent $event)
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
@@ -34,7 +34,7 @@ class TimezoneSubscriber implements EventSubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
public function setTimezone(GetResponseEvent $event)
|
||||
public function setTimezone(RequestEvent $event)
|
||||
{
|
||||
if (!$this->canHandleEvent()) {
|
||||
return;
|
||||
|
||||
@@ -66,6 +66,11 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
return $this->formConfig->getUserDefaultTheme();
|
||||
}
|
||||
|
||||
private function getDefaultCurrency(): ?string
|
||||
{
|
||||
return $this->formConfig->getUserDefaultCurrency();
|
||||
}
|
||||
|
||||
private function getDefaultLanguage(): string
|
||||
{
|
||||
return $this->formConfig->getUserDefaultLanguage();
|
||||
@@ -88,9 +93,11 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
public function getDefaultPreferences(User $user)
|
||||
{
|
||||
$enableHourlyRate = false;
|
||||
$hourlyRateOptions = [];
|
||||
|
||||
if ($this->voter->isGranted('hourly-rate', $user)) {
|
||||
$enableHourlyRate = true;
|
||||
$hourlyRateOptions = ['currency' => $this->getDefaultCurrency()];
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -99,6 +106,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
->setValue(0)
|
||||
->setType(MoneyType::class)
|
||||
->setEnabled($enableHourlyRate)
|
||||
->setOptions($hourlyRateOptions)
|
||||
->addConstraint(new Range(['min' => 0])),
|
||||
|
||||
(new UserPreference())
|
||||
@@ -155,7 +163,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user));
|
||||
$this->eventDispatcher->dispatch(UserPreferenceEvent::CONFIGURE, $event);
|
||||
$this->eventDispatcher->dispatch($event, UserPreferenceEvent::CONFIGURE);
|
||||
|
||||
foreach ($event->getPreferences() as $preference) {
|
||||
/* @var UserPreference[] $prefs */
|
||||
@@ -165,6 +173,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
->setType($preference->getType())
|
||||
->setConstraints($preference->getConstraints())
|
||||
->setEnabled($preference->isEnabled())
|
||||
->setOptions($preference->getOptions())
|
||||
;
|
||||
} else {
|
||||
$prefs[$preference->getName()] = $preference;
|
||||
|
||||
@@ -62,7 +62,7 @@ class UserProfileSubscriber implements EventSubscriberInterface
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
$event = new PrepareUserEvent($user);
|
||||
$this->eventDispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
$this->eventDispatcher->dispatch($event, PrepareUserEvent::PREPARE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,9 +12,12 @@ namespace App\Export\Renderer;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\DateExtensions;
|
||||
use App\Twig\Extensions;
|
||||
use DateTime;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
@@ -22,72 +25,88 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractSpreadsheetRenderer
|
||||
{
|
||||
public const DATETIME_FORMAT = 'yyyy-mm-dd hh:mm';
|
||||
public const TIME_FORMAT = 'hh:mm';
|
||||
public const DURATION_FORMAT = '[hh]:mm';
|
||||
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
|
||||
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* \(#,##0.00\);_("%1$s"* "-"??_);_(@_)';
|
||||
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
|
||||
|
||||
/**
|
||||
* @var DateExtensions
|
||||
*/
|
||||
protected $dateExtension;
|
||||
/**
|
||||
* @var Extensions
|
||||
*/
|
||||
protected $extension;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @param TranslatorInterface $translator
|
||||
* @param DateExtensions $dateExtension
|
||||
* @param Extensions $extensions
|
||||
*/
|
||||
public function __construct(
|
||||
TranslatorInterface $translator,
|
||||
DateExtensions $dateExtension,
|
||||
Extensions $extensions
|
||||
) {
|
||||
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->dateExtension = $dateExtension;
|
||||
$this->extension = $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $date
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDateTime(\DateTime $date)
|
||||
protected function setFormattedDateTime(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
return $this->dateExtension->dateShort($date) . ' ' . $this->dateExtension->time($date);
|
||||
}
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
/**
|
||||
* @param int $amount
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedMoney($amount, $currency)
|
||||
{
|
||||
return $this->extension->money($amount, $currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @return string
|
||||
*/
|
||||
protected function getUsername(Timesheet $timesheet)
|
||||
{
|
||||
if (!empty($timesheet->getUser()->getAlias())) {
|
||||
return $timesheet->getUser()->getAlias();
|
||||
return;
|
||||
}
|
||||
|
||||
return $timesheet->getUser()->getUsername();
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $seconds
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFormattedDuration($seconds)
|
||||
protected function setFormattedTime(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
return $this->extension->duration($seconds);
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||
}
|
||||
|
||||
protected function setFormattedDate(Worksheet $sheet, $column, $row, ?DateTime $date)
|
||||
{
|
||||
if (null === $date) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($date));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
||||
}
|
||||
|
||||
protected function setDurationTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
|
||||
$style = $sheet->getStyleByColumnAndRow($column, $row);
|
||||
$style->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
|
||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $duration));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
|
||||
protected function setRateTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUM(%s:%s)', $startCoordinate, $endCoordinate));
|
||||
}
|
||||
|
||||
protected function setRate(Worksheet $sheet, $column, $row, $rate, $currency)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $rate);
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(
|
||||
sprintf(self::RATE_FORMAT_LEFT, $currency)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,8 +132,11 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$recordsHeaderColumn = 1;
|
||||
$recordsHeaderRow = 1;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.date'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.begin'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.end'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.user'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.customer'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.project'));
|
||||
@@ -122,45 +144,67 @@ abstract class AbstractSpreadsheetRenderer
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.tags'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||
foreach ($publicMetaFields as $metaFieldName) {
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($metaFieldName));
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourlyRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixedRate'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||
|
||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||
|
||||
$durationTotal = 0;
|
||||
$currency = false;
|
||||
$rateTotal = 0;
|
||||
$durationColumn = null;
|
||||
$rateColumn = null;
|
||||
|
||||
foreach ($timesheets as $timesheet) {
|
||||
$entryHeaderColumn = 1;
|
||||
|
||||
$durationTotal += $timesheet->getDuration();
|
||||
$rateTotal += $timesheet->getRate();
|
||||
if ($currency === false) {
|
||||
$currency = $timesheet->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
if ($currency !== $timesheet->getProject()->getCustomer()->getCurrency()) {
|
||||
$currency = null;
|
||||
}
|
||||
|
||||
$customerCurrency = $timesheet->getProject()->getCustomer()->getCurrency();
|
||||
|
||||
$exported = $timesheet->isExported() ? 'entryState.exported' : 'entryState.not_exported';
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDateTime($timesheet->getBegin()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDateTime($timesheet->getEnd()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getUsername($timesheet));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getProject()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getActivity()->getName());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getDescription());
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->translator->trans($exported));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
|
||||
$this->setFormattedDate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getBegin());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setFormattedTime($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getEnd());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setDuration($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getDuration());
|
||||
$durationColumn = $entryHeaderColumn;
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getRate(), $customerCurrency);
|
||||
$rateColumn = $entryHeaderColumn;
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->getUsername($timesheet));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getProject()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getActivity()->getName());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $timesheet->getDescription());
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->translator->trans($exported));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, implode(',', $timesheet->getTagsAsArray()));
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getHourlyRate(), $customerCurrency);
|
||||
$entryHeaderColumn++;
|
||||
|
||||
$this->setRate($sheet, $entryHeaderColumn, $entryHeaderRow, $timesheet->getFixedRate(), $customerCurrency);
|
||||
$entryHeaderColumn++;
|
||||
|
||||
foreach ($publicMetaFields as $metaFieldName) {
|
||||
$metaField = $timesheet->getMetaField($metaFieldName);
|
||||
$metaFieldValue = '';
|
||||
@@ -169,28 +213,40 @@ abstract class AbstractSpreadsheetRenderer
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $metaFieldValue);
|
||||
}
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getHourlyRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getFixedRate(), $customerCurrency));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDuration($timesheet->getDuration()));
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $this->getFormattedMoney($timesheet->getRate(), $customerCurrency));
|
||||
|
||||
$entryHeaderRow++;
|
||||
}
|
||||
|
||||
$cellDurationTotal = $recordsHeaderColumn - 1;
|
||||
$cellRateTotal = $recordsHeaderColumn;
|
||||
if (null !== $durationColumn) {
|
||||
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
|
||||
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($cellDurationTotal, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
||||
$sheet->setCellValueByColumnAndRow($cellRateTotal, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
||||
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow($cellDurationTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
|
||||
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$sheet->getCellByColumnAndRow($cellRateTotal, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||
if (null !== $rateColumn) {
|
||||
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
|
||||
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
|
||||
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
protected function getUsername(Timesheet $timesheet): string
|
||||
{
|
||||
if (!empty($timesheet->getUser()->getAlias())) {
|
||||
return $timesheet->getUser()->getAlias();
|
||||
}
|
||||
|
||||
return $timesheet->getUser()->getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Export\Renderer;
|
||||
use App\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
@@ -72,4 +73,9 @@ final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererI
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
protected function setDuration(Worksheet $sheet, $column, $row, $duration)
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s', $duration));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Renderer;
|
||||
|
||||
use App\Export\RendererInterface;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
final class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return '.ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getContentType(): string
|
||||
{
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-ods');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Ods');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'ods';
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,11 @@ class ActivityEditForm extends AbstractType
|
||||
if ($options['customer']) {
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'data' => $customer ? $customer : null,
|
||||
'required' => false,
|
||||
@@ -80,15 +83,18 @@ class ActivityEditForm extends AbstractType
|
||||
$builder
|
||||
->add('project', ProjectType::class, [
|
||||
'required' => false,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
|
||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($project) {
|
||||
function (FormEvent $event) use ($builder, $project) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['customer']) || empty($data['customer'])) {
|
||||
return;
|
||||
@@ -96,8 +102,11 @@ class ActivityEditForm extends AbstractType
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data, $project) {
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $data['customer']));
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $data, $project) {
|
||||
$query = new ProjectFormTypeQuery($project, $data['customer']);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
51
src/Form/CustomerTeamPermissionForm.php
Normal file
51
src/Form/CustomerTeamPermissionForm.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Form\Type\TeamType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CustomerTeamPermissionForm extends AbstractType
|
||||
{
|
||||
use EntityFormTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('teams', TeamType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Customer::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_customer_teams_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.customerTeamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
45
src/Form/Extension/UserExtension.php
Normal file
45
src/Form/Extension/UserExtension.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\Extension;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\CurrentUser;
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class UserExtension extends AbstractTypeExtension
|
||||
{
|
||||
/**
|
||||
* @var CurrentUser
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(CurrentUser $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
return [FormType::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsResolver $resolver
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefined(['user']);
|
||||
// null needs to be allowed, as there is no user for anonymoud forms (like "forgot password" and "registration")
|
||||
$resolver->setAllowedTypes('user', [User::class, 'null']);
|
||||
$resolver->setDefault('user', $this->user->getUser());
|
||||
}
|
||||
}
|
||||
@@ -59,8 +59,11 @@ class ProjectEditForm extends AbstractType
|
||||
'required' => false,
|
||||
])
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
51
src/Form/ProjectTeamPermissionForm.php
Normal file
51
src/Form/ProjectTeamPermissionForm.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Form\Type\TeamType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectTeamPermissionForm extends AbstractType
|
||||
{
|
||||
use EntityFormTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('teams', TeamType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Project::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_project_teams_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.projectTeamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
51
src/Form/TeamCustomerForm.php
Normal file
51
src/Form/TeamCustomerForm.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\Type\CustomerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamCustomerForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('customers', CustomerType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
'query_builder_for_user' => false,
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_team_customer',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
61
src/Form/TeamEditForm.php
Normal file
61
src/Form/TeamEditForm.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 App\Form;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\Type\UserType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamEditForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'label.name',
|
||||
'attr' => [
|
||||
'autofocus' => 'autofocus'
|
||||
],
|
||||
])
|
||||
->add('teamlead', UserType::class, [
|
||||
'label' => 'label.teamlead',
|
||||
'multiple' => false,
|
||||
'expanded' => false,
|
||||
])
|
||||
->add('users', UserType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'by_reference' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_team_edit',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
52
src/Form/TeamProjectForm.php
Normal file
52
src/Form/TeamProjectForm.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;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TeamProjectForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('projects', ProjectType::class, [
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
'by_reference' => false,
|
||||
'attr' => ['size' => '20'],
|
||||
'query_builder_for_user' => false,
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Team::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_team_project',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.teamUpdate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -163,8 +163,11 @@ class TimesheetEditForm extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||
$query = new CustomerFormTypeQuery($customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
@@ -189,8 +192,11 @@ class TimesheetEditForm extends AbstractType
|
||||
array_merge($projectOptions, [
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
])
|
||||
);
|
||||
@@ -198,7 +204,7 @@ class TimesheetEditForm extends AbstractType
|
||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($project, $customer, $isNew) {
|
||||
function (FormEvent $event) use ($builder, $project, $customer, $isNew) {
|
||||
$data = $event->getData();
|
||||
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
|
||||
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
|
||||
@@ -207,7 +213,7 @@ class TimesheetEditForm extends AbstractType
|
||||
'placeholder' => '',
|
||||
'activity_enabled' => true,
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer, $isNew) {
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
|
||||
// is there a better wa to prevent starting a record with a hidden project ?
|
||||
if ($isNew && !is_object($project)) {
|
||||
/** @var Project $project */
|
||||
@@ -221,8 +227,10 @@ class TimesheetEditForm extends AbstractType
|
||||
}
|
||||
}
|
||||
}
|
||||
$query = new ProjectFormTypeQuery($project, $customer);
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
@@ -48,9 +50,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addUserChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('user', UserType::class, [
|
||||
@@ -59,20 +58,30 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addCustomerChoice(FormBuilderInterface $builder)
|
||||
protected function addUsersChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('users', UserType::class, [
|
||||
'label' => 'label.user',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addCustomerChoice(FormBuilderInterface $builder, bool $required = false)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
$builder->add('customer', HiddenType::class);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
function (FormEvent $event) use ($builder, $required) {
|
||||
$data = $event->getData();
|
||||
$event->getForm()->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'required' => $required,
|
||||
'project_enabled' => true,
|
||||
'query_builder' => function (CustomerRepository $repo) use ($data) {
|
||||
'query_builder' => function (CustomerRepository $repo) use ($builder, $data) {
|
||||
$query = new CustomerFormTypeQuery();
|
||||
$query->setUser($builder->getOption('user'));
|
||||
if (isset($data['customer']) && !empty($data['customer'])) {
|
||||
$query->setCustomer($data['customer']);
|
||||
}
|
||||
@@ -84,10 +93,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param string $label
|
||||
*/
|
||||
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'label.visible')
|
||||
{
|
||||
$builder->add('visibility', VisibilityType::class, [
|
||||
@@ -97,9 +102,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addPageSizeChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('pageSize', PageSizeType::class, [
|
||||
@@ -107,9 +109,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addUserRoleChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('role', UserRoleType::class, [
|
||||
@@ -117,9 +116,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addDateRangeChoice(FormBuilderInterface $builder, $allowEmpty = true)
|
||||
{
|
||||
$builder->add('daterange', DateRangeType::class, [
|
||||
@@ -128,9 +124,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addProjectChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
@@ -138,13 +131,14 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
function (FormEvent $event) use ($builder) {
|
||||
$data = $event->getData();
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'required' => false,
|
||||
'activity_enabled' => true,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data) {
|
||||
'query_builder' => function (ProjectRepository $repo) use ($builder, $data) {
|
||||
$query = new ProjectFormTypeQuery();
|
||||
$query->setUser($builder->getOption('user'));
|
||||
|
||||
if (isset($data['customer']) && !empty($data['customer'])) {
|
||||
$query->setCustomer($data['customer']);
|
||||
@@ -160,9 +154,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addActivityChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
// just a fake field for having this field at the right position in the frontend
|
||||
@@ -191,9 +182,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addHiddenPagination(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('page', HiddenType::class, [
|
||||
@@ -201,13 +189,38 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addTagInputField(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('tags', TagsInputType::class, [
|
||||
'required' => false
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addTimesheetStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
'label' => 'label.entryState',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addExportStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('exported', ChoiceType::class, [
|
||||
'label' => 'label.exported',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.exported' => TimesheetQuery::STATE_EXPORTED,
|
||||
'entryState.not_exported' => TimesheetQuery::STATE_NOT_EXPORTED
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user