Compare commits
54 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 | ||
|
|
12803b3957 | ||
|
|
7583498735 | ||
|
|
53eda31179 | ||
|
|
56e276939b | ||
|
|
39abccb35d | ||
|
|
dae8866053 | ||
|
|
a0e8aa3a43 | ||
|
|
e68b7da1f8 | ||
|
|
fcce7a531a | ||
|
|
f23dcbec17 | ||
|
|
8a1d28563d | ||
|
|
c33a87a07c | ||
|
|
b833e1ce27 | ||
|
|
6de1a55d73 | ||
|
|
cbf9086e10 | ||
|
|
eb4aa23577 | ||
|
|
bf9ae44775 | ||
|
|
f0822fa2b1 | ||
|
|
f966d34929 | ||
|
|
d51136f8f4 | ||
|
|
19843760e5 | ||
|
|
3457f93ed0 | ||
|
|
625ac2f1d7 | ||
|
|
061581c4ff | ||
|
|
a0116422c6 |
@@ -1,4 +1,5 @@
|
||||
unreleased=true
|
||||
future-release=1.0
|
||||
future-release=1.2
|
||||
exclude-labels=duplicate,question,invalid,wontfix,release
|
||||
enhancement_labels=>enhancement,Enhancement,feature request
|
||||
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt
|
||||
issues-wo-labels=false
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
build:
|
||||
environment:
|
||||
node: v8.11.2
|
||||
php:
|
||||
version: 7.2
|
||||
ini:
|
||||
date.timezone: "UTC"
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
override:
|
||||
- js-scrutinizer-run
|
||||
- php-scrutinizer-run
|
||||
tests:
|
||||
tests:
|
||||
override:
|
||||
-
|
||||
command: 'vendor/bin/phpunit --coverage-clover=clover.xml tests/'
|
||||
stop_on_failure: true
|
||||
coverage:
|
||||
file: 'clover.xml'
|
||||
format: 'clover'
|
||||
|
||||
filter:
|
||||
excluded_paths:
|
||||
- '.github/'
|
||||
- 'bin/'
|
||||
- 'config/'
|
||||
- 'public/'
|
||||
- 'templates/'
|
||||
- 'translations/'
|
||||
- 'var/'
|
||||
dependency_paths:
|
||||
- 'node_modules/'
|
||||
- 'vendor/'
|
||||
|
||||
build_failure_conditions:
|
||||
- 'project.metric("scrutinizer.quality", < 9.0)'
|
||||
- 'project.metric("scrutinizer.test_coverage", < 0.9)'
|
||||
- 'project.metric_change("scrutinizer.test_coverage", < -0.01)'
|
||||
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
|
||||
|
||||
790
CHANGELOG.md
790
CHANGELOG.md
@@ -1,811 +1,75 @@
|
||||
# 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)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.1)
|
||||
|
||||
## [1.0.1](https://github.com/kevinpapst/kimai2/tree/1.0.1) (2019-07-05)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.0...1.0.1)
|
||||
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.0.1)
|
||||
|
||||
## [1.0](https://github.com/kevinpapst/kimai2/tree/1.0) (2019-07-02)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.9...1.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Admin Timesheets Toolbar: order users by username [\#876](https://github.com/kevinpapst/kimai2/issues/876)
|
||||
- import project "budget" field [\#841](https://github.com/kevinpapst/kimai2/issues/841)
|
||||
- Add tag data to exports [\#825](https://github.com/kevinpapst/kimai2/issues/825)
|
||||
- Additional status indicators for a field \(tag field\) more like lables which are globally assigable and appear for each task a user is working on or has started [\#769](https://github.com/kevinpapst/kimai2/issues/769)
|
||||
- Include option of tags field to link various timesheet entries together and use them for filtering data [\#768](https://github.com/kevinpapst/kimai2/issues/768)
|
||||
- Disable manual To date and duration entry option in timer [\#764](https://github.com/kevinpapst/kimai2/issues/764)
|
||||
- Custom data for external references [\#760](https://github.com/kevinpapst/kimai2/issues/760)
|
||||
- Refactor active entries dropdown [\#754](https://github.com/kevinpapst/kimai2/issues/754)
|
||||
- Stop entries without page reload [\#753](https://github.com/kevinpapst/kimai2/issues/753)
|
||||
- More actions on data records resp. table rows [\#748](https://github.com/kevinpapst/kimai2/issues/748)
|
||||
- Action menu instead of icons [\#747](https://github.com/kevinpapst/kimai2/issues/747)
|
||||
- Multiple Work Orders under Project for a Customer [\#720](https://github.com/kevinpapst/kimai2/issues/720)
|
||||
- Add custom fields in timesheet record [\#697](https://github.com/kevinpapst/kimai2/issues/697)
|
||||
- Lock exported objects [\#686](https://github.com/kevinpapst/kimai2/issues/686)
|
||||
- Option to allow Markdown in Project/Activity-comments [\#677](https://github.com/kevinpapst/kimai2/issues/677)
|
||||
- Recalculate rates on every change [\#673](https://github.com/kevinpapst/kimai2/issues/673)
|
||||
- Show on the title if there is an active recording [\#660](https://github.com/kevinpapst/kimai2/issues/660)
|
||||
- Allow edit Date From for saved time records [\#627](https://github.com/kevinpapst/kimai2/issues/627)
|
||||
- Restarting a entry- missing description [\#626](https://github.com/kevinpapst/kimai2/issues/626)
|
||||
- Calendar: Return to last view after creating a new time entry [\#622](https://github.com/kevinpapst/kimai2/issues/622)
|
||||
- Unable to select «projects» while filtering in timesheets [\#601](https://github.com/kevinpapst/kimai2/issues/601)
|
||||
- View cut off on 4:3 monitor [\#575](https://github.com/kevinpapst/kimai2/issues/575)
|
||||
- Calendar view: more space for business hours [\#530](https://github.com/kevinpapst/kimai2/issues/530)
|
||||
- maximale Anzahl Projektstunden als Vorgabe [\#467](https://github.com/kevinpapst/kimai2/issues/467)
|
||||
- don't allow start end dates, just punch in or out [\#448](https://github.com/kevinpapst/kimai2/issues/448)
|
||||
- Refresh of Duration time [\#420](https://github.com/kevinpapst/kimai2/issues/420)
|
||||
- Statistic of time-used vs. time-budget [\#416](https://github.com/kevinpapst/kimai2/issues/416)
|
||||
- Colors for customers / projects [\#374](https://github.com/kevinpapst/kimai2/issues/374)
|
||||
- allow to disable user preferences by permission [\#895](https://github.com/kevinpapst/kimai2/pull/895) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- order users by username [\#880](https://github.com/kevinpapst/kimai2/pull/880) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- use default\_begin in duration only mode [\#873](https://github.com/kevinpapst/kimai2/pull/873) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Custom fields for timesheets, customers, projects and activities [\#871](https://github.com/kevinpapst/kimai2/pull/871) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- export tags in spreadsheets [\#860](https://github.com/kevinpapst/kimai2/pull/860) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- add tracking mode \(duration + fixed start time\) [\#859](https://github.com/kevinpapst/kimai2/pull/859) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- branding options [\#856](https://github.com/kevinpapst/kimai2/pull/856) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Widget refactoring [\#847](https://github.com/kevinpapst/kimai2/pull/847) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added hourly and money budgets to activity, project and customer [\#843](https://github.com/kevinpapst/kimai2/pull/843) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated dockerfile [\#840](https://github.com/kevinpapst/kimai2/pull/840) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added installation command [\#838](https://github.com/kevinpapst/kimai2/pull/838) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- add ldap support [\#815](https://github.com/kevinpapst/kimai2/pull/815) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added punch-in punch-out / time-clock mode [\#812](https://github.com/kevinpapst/kimai2/pull/812) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow markdown for customer, project and activity comments [\#807](https://github.com/kevinpapst/kimai2/pull/807) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- improved html export [\#802](https://github.com/kevinpapst/kimai2/pull/802) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- use dropdown for entity actions on detail pages [\#801](https://github.com/kevinpapst/kimai2/pull/801) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- lock exported timesheets [\#798](https://github.com/kevinpapst/kimai2/pull/798) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Calendar improvements [\#784](https://github.com/kevinpapst/kimai2/pull/784) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- restart via API allows to copy description [\#782](https://github.com/kevinpapst/kimai2/pull/782) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- delete timesheets via api [\#776](https://github.com/kevinpapst/kimai2/pull/776) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- javascript and api to stop and display active records [\#772](https://github.com/kevinpapst/kimai2/pull/772) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- recent activities via API [\#761](https://github.com/kevinpapst/kimai2/pull/761) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- ES6 javascript conversion [\#733](https://github.com/kevinpapst/kimai2/pull/733) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Use tags for every timesheet entry [\#604](https://github.com/kevinpapst/kimai2/pull/604) ([infastra](https://github.com/infastra))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Invoice calculator that differs between fixed and hourly rates [\#884](https://github.com/kevinpapst/kimai2/issues/884)
|
||||
- Timesheets table empty after update [\#878](https://github.com/kevinpapst/kimai2/issues/878)
|
||||
- Ability to hide the revenue stats to users [\#853](https://github.com/kevinpapst/kimai2/issues/853)
|
||||
- Error on update database migration: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes [\#788](https://github.com/kevinpapst/kimai2/issues/788)
|
||||
- adding a new timesheet entry is broken in current master [\#778](https://github.com/kevinpapst/kimai2/issues/778)
|
||||
- calendar week timesheet task with duration=true error 500 [\#704](https://github.com/kevinpapst/kimai2/issues/704)
|
||||
- Inter-month entries don't get exported in single month [\#652](https://github.com/kevinpapst/kimai2/issues/652)
|
||||
- fix selectbox is not resettable [\#901](https://github.com/kevinpapst/kimai2/pull/901) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed short invoice [\#891](https://github.com/kevinpapst/kimai2/pull/891) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix sqlite migration [\#881](https://github.com/kevinpapst/kimai2/pull/881) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added LDAP config to set filter for finding user attributes [\#877](https://github.com/kevinpapst/kimai2/pull/877) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix LDAP install for systems without ldap extension [\#846](https://github.com/kevinpapst/kimai2/pull/846) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed mpdf temp directory [\#845](https://github.com/kevinpapst/kimai2/pull/845) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- enhance tag query to show all tags of an item [\#810](https://github.com/kevinpapst/kimai2/pull/810) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix column and max key length [\#789](https://github.com/kevinpapst/kimai2/pull/789) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- bugfix for API selects [\#780](https://github.com/kevinpapst/kimai2/pull/780) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- translate date picker and charts [\#775](https://github.com/kevinpapst/kimai2/pull/775) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- refactored javascript to ES6 classes [\#759](https://github.com/kevinpapst/kimai2/pull/759) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Update Hungarian translation [\#897](https://github.com/kevinpapst/kimai2/issues/897)
|
||||
- Authentication to Active Directory [\#875](https://github.com/kevinpapst/kimai2/issues/875)
|
||||
- LDAP Documentation Enhancement for Active Directory [\#867](https://github.com/kevinpapst/kimai2/issues/867)
|
||||
- Cannot find bundle in plugin tests [\#863](https://github.com/kevinpapst/kimai2/issues/863)
|
||||
- Allowed memory in reset-dev [\#857](https://github.com/kevinpapst/kimai2/issues/857)
|
||||
- 503 Error Service Unavailable [\#834](https://github.com/kevinpapst/kimai2/issues/834)
|
||||
- Documentation for native wrapper like nativefier [\#830](https://github.com/kevinpapst/kimai2/issues/830)
|
||||
- Create documentation for layman users to deploy the master code on their servers [\#827](https://github.com/kevinpapst/kimai2/issues/827)
|
||||
- Allow export of data in timesheet view directly from the frontend [\#826](https://github.com/kevinpapst/kimai2/issues/826)
|
||||
- Adding a command as Plugin [\#797](https://github.com/kevinpapst/kimai2/issues/797)
|
||||
- connect kimai to ldap [\#795](https://github.com/kevinpapst/kimai2/issues/795)
|
||||
- Older Kimai2 version compatible with PHP 7.0? [\#790](https://github.com/kevinpapst/kimai2/issues/790)
|
||||
- POLL: Global vs. customer currency [\#781](https://github.com/kevinpapst/kimai2/issues/781)
|
||||
- Docker refactor [\#763](https://github.com/kevinpapst/kimai2/issues/763)
|
||||
- Finally got this installed! [\#743](https://github.com/kevinpapst/kimai2/issues/743)
|
||||
- Possible to translate login page [\#736](https://github.com/kevinpapst/kimai2/issues/736)
|
||||
- Grouping Users when Exporting [\#680](https://github.com/kevinpapst/kimai2/issues/680)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Update Hungarian translation [\#902](https://github.com/kevinpapst/kimai2/pull/902) ([infeeeee](https://github.com/infeeeee))
|
||||
- performance tuning on timesheets [\#874](https://github.com/kevinpapst/kimai2/pull/874) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Removed lines that skip plugins in test env [\#864](https://github.com/kevinpapst/kimai2/pull/864) ([tobybatch](https://github.com/tobybatch))
|
||||
- fix/LDAP multivalues Email Aliases [\#862](https://github.com/kevinpapst/kimai2/pull/862) ([jacoblanco](https://github.com/jacoblanco))
|
||||
- Spanish translate updated [\#852](https://github.com/kevinpapst/kimai2/pull/852) ([yayitazale](https://github.com/yayitazale))
|
||||
- Update of the French translation [\#850](https://github.com/kevinpapst/kimai2/pull/850) ([jeau](https://github.com/jeau))
|
||||
- improved fixture data for devs [\#848](https://github.com/kevinpapst/kimai2/pull/848) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Implement japanese translation 190601 [\#837](https://github.com/kevinpapst/kimai2/pull/837) ([nabbisen](https://github.com/nabbisen))
|
||||
- use codecov for coverage [\#833](https://github.com/kevinpapst/kimai2/pull/833) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code cleanup [\#811](https://github.com/kevinpapst/kimai2/pull/811) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- refactored permissions for simpler customizations [\#808](https://github.com/kevinpapst/kimai2/pull/808) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added phpstan for code analyzes [\#806](https://github.com/kevinpapst/kimai2/pull/806) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added SECURITY.md [\#805](https://github.com/kevinpapst/kimai2/pull/805) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added funding yaml [\#804](https://github.com/kevinpapst/kimai2/pull/804) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- timesheet controller refactoring [\#796](https://github.com/kevinpapst/kimai2/pull/796) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix phpunit annotations [\#794](https://github.com/kevinpapst/kimai2/pull/794) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Modify Japanese translation [\#779](https://github.com/kevinpapst/kimai2/pull/779) ([nabbisen](https://github.com/nabbisen))
|
||||
- Add Japanese translation [\#774](https://github.com/kevinpapst/kimai2/pull/774) ([nabbisen](https://github.com/nabbisen))
|
||||
- fix toolbar requests for empty choice [\#758](https://github.com/kevinpapst/kimai2/pull/758) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added more entity actions [\#757](https://github.com/kevinpapst/kimai2/pull/757) ([bjoerne2](https://github.com/bjoerne2))
|
||||
- Database interoperability [\#756](https://github.com/kevinpapst/kimai2/pull/756) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- dynamically update title and visible durations for running records [\#752](https://github.com/kevinpapst/kimai2/pull/752) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- colors for visual grouping of customer, projects and activities [\#751](https://github.com/kevinpapst/kimai2/pull/751) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- moved shared entity fields and functions to trait [\#750](https://github.com/kevinpapst/kimai2/pull/750) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Entity actions re-designed \(table buttons\) [\#746](https://github.com/kevinpapst/kimai2/pull/746) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- navbar block cleanup [\#742](https://github.com/kevinpapst/kimai2/pull/742) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- use bootstrap tooltips instead of own solution [\#741](https://github.com/kevinpapst/kimai2/pull/741) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- always query timesheets by begin date [\#740](https://github.com/kevinpapst/kimai2/pull/740) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- prevent jumping icons in navbar between collapsed and open state [\#739](https://github.com/kevinpapst/kimai2/pull/739) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- translate time-tracking in login screen and browser title [\#738](https://github.com/kevinpapst/kimai2/pull/738) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- theme update for swedish auth screens [\#737](https://github.com/kevinpapst/kimai2/pull/737) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Testing: Webpack upgrade [\#729](https://github.com/kevinpapst/kimai2/pull/729) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.0)
|
||||
|
||||
## [0.9](https://github.com/kevinpapst/kimai2/tree/0.9) (2019-04-29)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8.1...0.9)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Translation to Swedish [\#731](https://github.com/kevinpapst/kimai2/issues/731)
|
||||
- Remove active flag in user preferences form for non admin user [\#694](https://github.com/kevinpapst/kimai2/issues/694)
|
||||
- Add floor and ceil rounding [\#692](https://github.com/kevinpapst/kimai2/issues/692)
|
||||
- POST/PUT API endpoints for projects and activities [\#683](https://github.com/kevinpapst/kimai2/issues/683)
|
||||
- API is not able to handle CORS [\#646](https://github.com/kevinpapst/kimai2/issues/646)
|
||||
- Recompile the frontend assets for usage in a sub-directory [\#635](https://github.com/kevinpapst/kimai2/issues/635)
|
||||
- Remove database table prefix [\#633](https://github.com/kevinpapst/kimai2/issues/633)
|
||||
- Autofocus on first entry of forms [\#620](https://github.com/kevinpapst/kimai2/issues/620)
|
||||
- Activity admin - filter for globals [\#598](https://github.com/kevinpapst/kimai2/issues/598)
|
||||
- Non visible customer/projects etc are shown in dropdowns [\#597](https://github.com/kevinpapst/kimai2/issues/597)
|
||||
- remove duplicate confirm dialog of deletion [\#591](https://github.com/kevinpapst/kimai2/issues/591)
|
||||
- remove mobile navbar header [\#588](https://github.com/kevinpapst/kimai2/issues/588)
|
||||
- Support 12h time format [\#578](https://github.com/kevinpapst/kimai2/issues/578)
|
||||
- add timerange filter to timesheet export api [\#577](https://github.com/kevinpapst/kimai2/issues/577)
|
||||
- Extend rounding options with "to closest" mode [\#576](https://github.com/kevinpapst/kimai2/issues/576)
|
||||
- Custom CSS [\#516](https://github.com/kevinpapst/kimai2/issues/516)
|
||||
- reduce config complexity by converting php format to js format [\#734](https://github.com/kevinpapst/kimai2/pull/734) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated authorization screens [\#727](https://github.com/kevinpapst/kimai2/pull/727) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete timesheet API endpoint [\#726](https://github.com/kevinpapst/kimai2/pull/726) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Design updates [\#723](https://github.com/kevinpapst/kimai2/pull/723) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- API: changed date-format, camelCase instead of snake\_case, null values, update and create for customer and project [\#718](https://github.com/kevinpapst/kimai2/pull/718) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added api/activities POST and PATCH [\#717](https://github.com/kevinpapst/kimai2/pull/717) ([horlabs](https://github.com/horlabs))
|
||||
- allow to overwrite config via prepend [\#715](https://github.com/kevinpapst/kimai2/pull/715) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Load export type ids dynamically [\#703](https://github.com/kevinpapst/kimai2/pull/703) ([simone-gasparini](https://github.com/simone-gasparini))
|
||||
- fixing default env var for cors header [\#656](https://github.com/kevinpapst/kimai2/pull/656) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added cors bundle to allow external api access [\#655](https://github.com/kevinpapst/kimai2/pull/655) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added route to redirect locale to configured homepage [\#654](https://github.com/kevinpapst/kimai2/pull/654) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added database driven system configurations with admin screen [\#647](https://github.com/kevinpapst/kimai2/pull/647) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added new rounding mode: closest [\#611](https://github.com/kevinpapst/kimai2/pull/611) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- order datatable items by name [\#594](https://github.com/kevinpapst/kimai2/pull/594) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- UI improvements for datatables [\#593](https://github.com/kevinpapst/kimai2/pull/593) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Plugins support [\#592](https://github.com/kevinpapst/kimai2/pull/592) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Export date parsing does not care about timezone [\#707](https://github.com/kevinpapst/kimai2/issues/707)
|
||||
- Error on importing v1 timeSheets [\#645](https://github.com/kevinpapst/kimai2/issues/645)
|
||||
- 500 \(Internal Server Error\) create customer [\#711](https://github.com/kevinpapst/kimai2/issues/711)
|
||||
- API docs are wrong for GET entity \(ignores serialization group\) [\#701](https://github.com/kevinpapst/kimai2/issues/701)
|
||||
- API date and datetime format [\#700](https://github.com/kevinpapst/kimai2/issues/700)
|
||||
- totals do not match [\#691](https://github.com/kevinpapst/kimai2/issues/691)
|
||||
- Validate email when creating user via cmd-line [\#663](https://github.com/kevinpapst/kimai2/issues/663)
|
||||
- Datepicker "From" not working [\#651](https://github.com/kevinpapst/kimai2/issues/651)
|
||||
- Error export PDF [\#648](https://github.com/kevinpapst/kimai2/issues/648)
|
||||
- Time selection dropdown not working in Firefox [\#618](https://github.com/kevinpapst/kimai2/issues/618)
|
||||
- Revoke of start\_own\_timesheet permission doesn't prevent start active entry [\#606](https://github.com/kevinpapst/kimai2/issues/606)
|
||||
- resize mobile navbar buttons [\#582](https://github.com/kevinpapst/kimai2/issues/582)
|
||||
- fix tests that failed around midnight [\#735](https://github.com/kevinpapst/kimai2/pull/735) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- remove invalid cache header [\#722](https://github.com/kevinpapst/kimai2/pull/722) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix modal - autofocus problem, datepicker not shown on begin date [\#653](https://github.com/kevinpapst/kimai2/pull/653) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix pdf export cache directory [\#650](https://github.com/kevinpapst/kimai2/pull/650) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- 403 error after installation [\#725](https://github.com/kevinpapst/kimai2/issues/725)
|
||||
- AppExtension Error after bin/console cache:clear --env=prod on Line 43 [\#721](https://github.com/kevinpapst/kimai2/issues/721)
|
||||
- Kimai-v1 Import Failure [\#719](https://github.com/kevinpapst/kimai2/issues/719)
|
||||
- Review export lifecycle for plugin injection [\#702](https://github.com/kevinpapst/kimai2/issues/702)
|
||||
- Missing database-table kimai2\_configuration while trying to run import/migration script [\#698](https://github.com/kevinpapst/kimai2/issues/698)
|
||||
- Import from v1 fails if two users have the same email or blank email [\#688](https://github.com/kevinpapst/kimai2/issues/688)
|
||||
- mysql limit of max amount of activities ? [\#681](https://github.com/kevinpapst/kimai2/issues/681)
|
||||
- Filter by project broken [\#670](https://github.com/kevinpapst/kimai2/issues/670)
|
||||
- Group of user [\#669](https://github.com/kevinpapst/kimai2/issues/669)
|
||||
- Demo not loading [\#665](https://github.com/kevinpapst/kimai2/issues/665)
|
||||
- Admin Panel [\#662](https://github.com/kevinpapst/kimai2/issues/662)
|
||||
- Some activites don't show up in last activities [\#609](https://github.com/kevinpapst/kimai2/issues/609)
|
||||
- Wrong link in the CSS [\#605](https://github.com/kevinpapst/kimai2/issues/605)
|
||||
- Failed to import users: failed to validate user [\#600](https://github.com/kevinpapst/kimai2/issues/600)
|
||||
- Edit timesheet , delete user edit own timesheet [\#498](https://github.com/kevinpapst/kimai2/issues/498)
|
||||
- mixed date formats in the api for the timesheet [\#658](https://github.com/kevinpapst/kimai2/issues/658)
|
||||
- Add demo bundle [\#442](https://github.com/kevinpapst/kimai2/issues/442)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- added svenska [\#732](https://github.com/kevinpapst/kimai2/pull/732) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- apply users timezone to toolbar queries [\#716](https://github.com/kevinpapst/kimai2/pull/716) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- remove unused role ROLE\_CUSTOMER [\#712](https://github.com/kevinpapst/kimai2/pull/712) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Update to Symfony 4.2 [\#710](https://github.com/kevinpapst/kimai2/pull/710) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix validation for create user command [\#709](https://github.com/kevinpapst/kimai2/pull/709) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- remove the total counter from all admin pages [\#708](https://github.com/kevinpapst/kimai2/pull/708) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- removed active flag in user preferences form for non admin user. \(\#694\) [\#696](https://github.com/kevinpapst/kimai2/pull/696) ([simone-gasparini](https://github.com/simone-gasparini))
|
||||
- added floor and ceil rounding with tests \(\#692\) [\#695](https://github.com/kevinpapst/kimai2/pull/695) ([simone-gasparini](https://github.com/simone-gasparini))
|
||||
- fixed activity-project assignment for kimai v1 importer [\#679](https://github.com/kevinpapst/kimai2/pull/679) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added content-type to allowed headers for api [\#672](https://github.com/kevinpapst/kimai2/pull/672) ([hmr-it-jr](https://github.com/hmr-it-jr))
|
||||
- added plugin screen [\#671](https://github.com/kevinpapst/kimai2/pull/671) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated hungarian translation [\#661](https://github.com/kevinpapst/kimai2/pull/661) ([infeeeee](https://github.com/infeeeee))
|
||||
- remove mailer url from about debug info [\#657](https://github.com/kevinpapst/kimai2/pull/657) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- delete directory with invalid case [\#643](https://github.com/kevinpapst/kimai2/pull/643) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added command to create release packages \(for FTP user\) [\#642](https://github.com/kevinpapst/kimai2/pull/642) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- removed dynamic database prefix [\#641](https://github.com/kevinpapst/kimai2/pull/641) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- set autofocus attribute on form fields for better keyboard support [\#640](https://github.com/kevinpapst/kimai2/pull/640) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- API - added begin end and export filter for timesheets [\#639](https://github.com/kevinpapst/kimai2/pull/639) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- replaced delete confirm dialog with modal [\#638](https://github.com/kevinpapst/kimai2/pull/638) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- convert-timezone: add a default value 'y' to the asked question [\#637](https://github.com/kevinpapst/kimai2/pull/637) ([blueowl04](https://github.com/blueowl04))
|
||||
- support subdirectories and subdomains for assets [\#636](https://github.com/kevinpapst/kimai2/pull/636) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- improved plugin support [\#634](https://github.com/kevinpapst/kimai2/pull/634) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix random order in recent activities [\#631](https://github.com/kevinpapst/kimai2/pull/631) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix for firefox datepicker focus problem in modals [\#630](https://github.com/kevinpapst/kimai2/pull/630) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- remove invisible entities from toolbars [\#628](https://github.com/kevinpapst/kimai2/pull/628) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- removed inline documentation [\#624](https://github.com/kevinpapst/kimai2/pull/624) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added support for Ante meridiem and Post meridiem format [\#615](https://github.com/kevinpapst/kimai2/pull/615) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added filter for global activities [\#614](https://github.com/kevinpapst/kimai2/pull/614) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improve create and start permission handling [\#613](https://github.com/kevinpapst/kimai2/pull/613) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Edit and create timesheet records in modal [\#603](https://github.com/kevinpapst/kimai2/pull/603) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code cleanup [\#602](https://github.com/kevinpapst/kimai2/pull/602) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- simplify theme settings in user profile [\#586](https://github.com/kevinpapst/kimai2/pull/586) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.9)
|
||||
|
||||
## [0.8.1](https://github.com/kevinpapst/kimai2/tree/0.8.1) (2019-02-22)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8...0.8.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- deletion of customers leads to internal server error 500 [\#589](https://github.com/kevinpapst/kimai2/issues/589)
|
||||
- Entry editor label wrong \(from/to\) [\#587](https://github.com/kevinpapst/kimai2/issues/587)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Support foreign keys with SQLite [\#590](https://github.com/kevinpapst/kimai2/pull/590) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- post release fixes [\#584](https://github.com/kevinpapst/kimai2/pull/584) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Mobile navbar size [\#583](https://github.com/kevinpapst/kimai2/pull/583) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1)
|
||||
|
||||
## [0.8](https://github.com/kevinpapst/kimai2/tree/0.8) (2019-02-19)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.7...0.8)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Export other users' timesheets using the API [\#562](https://github.com/kevinpapst/kimai2/issues/562)
|
||||
- Some importer bugs/improvements [\#553](https://github.com/kevinpapst/kimai2/issues/553)
|
||||
- Export timesheet only exports entries on the current page [\#534](https://github.com/kevinpapst/kimai2/issues/534)
|
||||
- On the last activities dropdown show tha last ten DIFFERENT activities [\#533](https://github.com/kevinpapst/kimai2/issues/533)
|
||||
- Better user expierence on "My Times" [\#526](https://github.com/kevinpapst/kimai2/issues/526)
|
||||
- Highlight current day in datepickers and not only the selected day [\#522](https://github.com/kevinpapst/kimai2/issues/522)
|
||||
- Configuration of first page after login [\#501](https://github.com/kevinpapst/kimai2/issues/501)
|
||||
- Visual Grouping of entry from same day or same week with some stats of that group [\#495](https://github.com/kevinpapst/kimai2/issues/495)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Error 404 on 'My times' page 2+ if the time range is changed to something smaller [\#558](https://github.com/kevinpapst/kimai2/issues/558)
|
||||
- Timezone is not working correctly [\#554](https://github.com/kevinpapst/kimai2/issues/554)
|
||||
- New time picker not working on languages without updated translation [\#546](https://github.com/kevinpapst/kimai2/issues/546)
|
||||
- Export timesheet only exports entries on the current page [\#534](https://github.com/kevinpapst/kimai2/issues/534)
|
||||
- Kimai2 will not allow me to enter times in the future [\#531](https://github.com/kevinpapst/kimai2/issues/531)
|
||||
- No projects in filter section [\#525](https://github.com/kevinpapst/kimai2/issues/525)
|
||||
- This value should be greater than or equal to zero [\#511](https://github.com/kevinpapst/kimai2/issues/511)
|
||||
- Wrong time after migration kimai v1 data [\#507](https://github.com/kevinpapst/kimai2/issues/507)
|
||||
- SQL-Error for getRecentActivities\(\) caused by GROUP BY [\#488](https://github.com/kevinpapst/kimai2/issues/488)
|
||||
- fix sql for only\_full\_group\_by [\#581](https://github.com/kevinpapst/kimai2/pull/581) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Feedback for improving installation setup [\#541](https://github.com/kevinpapst/kimai2/issues/541)
|
||||
- template entry.end\_time shows empty field [\#535](https://github.com/kevinpapst/kimai2/issues/535)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- handle deleted user during import from v1 [\#569](https://github.com/kevinpapst/kimai2/pull/569) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix pagination in combination with daterange picker [\#568](https://github.com/kevinpapst/kimai2/pull/568) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow to query other users timesheets via api [\#563](https://github.com/kevinpapst/kimai2/pull/563) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated HTML invoice templates [\#560](https://github.com/kevinpapst/kimai2/pull/560) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated composer packages [\#559](https://github.com/kevinpapst/kimai2/pull/559) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix timezone problems in timesheet forms [\#555](https://github.com/kevinpapst/kimai2/pull/555) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Daily stats in timesheet [\#552](https://github.com/kevinpapst/kimai2/pull/552) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added more php requirements to documentation [\#551](https://github.com/kevinpapst/kimai2/pull/551) ([infeeeee](https://github.com/infeeeee))
|
||||
- improve recent activities [\#550](https://github.com/kevinpapst/kimai2/pull/550) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- scss fixes: year selector width, dropdown menu width, navbar refactoring [\#549](https://github.com/kevinpapst/kimai2/pull/549) ([infeeeee](https://github.com/infeeeee))
|
||||
- improved installation docs [\#548](https://github.com/kevinpapst/kimai2/pull/548) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix daterange-picker for fr, hu and ar [\#547](https://github.com/kevinpapst/kimai2/pull/547) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- do not limit users timesheet export page size [\#545](https://github.com/kevinpapst/kimai2/pull/545) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Use Symfony formatter for currency symbol placement [\#542](https://github.com/kevinpapst/kimai2/pull/542) ([sanjitlpatel](https://github.com/sanjitlpatel))
|
||||
- Localized date-inputs and daterange-picker [\#540](https://github.com/kevinpapst/kimai2/pull/540) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added export module [\#538](https://github.com/kevinpapst/kimai2/pull/538) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated documentation [\#536](https://github.com/kevinpapst/kimai2/pull/536) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- support remote data in beta-test selectpicker [\#529](https://github.com/kevinpapst/kimai2/pull/529) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improve allowed running records [\#528](https://github.com/kevinpapst/kimai2/pull/528) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improve daterangepicker [\#527](https://github.com/kevinpapst/kimai2/pull/527) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable view after login [\#523](https://github.com/kevinpapst/kimai2/pull/523) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- convert timesheets to UTC with support for user timezone [\#372](https://github.com/kevinpapst/kimai2/pull/372) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.8)
|
||||
|
||||
## [0.7](https://github.com/kevinpapst/kimai2/tree/0.7) (2019-01-28)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6.1...0.7)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Editing from calendar view will return to my times table instead of calendar [\#515](https://github.com/kevinpapst/kimai2/issues/515)
|
||||
- Timesheet Export for Admins [\#503](https://github.com/kevinpapst/kimai2/issues/503)
|
||||
- Customer List Not Alphabetic [\#499](https://github.com/kevinpapst/kimai2/issues/499)
|
||||
- sorting of ${entry.X} values [\#487](https://github.com/kevinpapst/kimai2/issues/487)
|
||||
- ${entry.description} needed in Word-Docx [\#485](https://github.com/kevinpapst/kimai2/issues/485)
|
||||
- Automatic sort for activities, customers, etc. [\#477](https://github.com/kevinpapst/kimai2/issues/477)
|
||||
- User title in the timesheet invoice [\#461](https://github.com/kevinpapst/kimai2/issues/461)
|
||||
- global variables for reports/invoices [\#438](https://github.com/kevinpapst/kimai2/issues/438)
|
||||
- Configuration option: Only one active record for each user [\#427](https://github.com/kevinpapst/kimai2/issues/427)
|
||||
- User creating activity and projects [\#393](https://github.com/kevinpapst/kimai2/issues/393)
|
||||
- Configuration option to disable fixed rate and hourly rate from "edit timesheet" [\#330](https://github.com/kevinpapst/kimai2/issues/330)
|
||||
- Set other users hourly rate [\#303](https://github.com/kevinpapst/kimai2/issues/303)
|
||||
- Feature request - Make "Rate" hideable [\#217](https://github.com/kevinpapst/kimai2/issues/217)
|
||||
- fixed null project for advanced invoice calculator [\#462](https://github.com/kevinpapst/kimai2/pull/462) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Timesheet Export for Admins [\#503](https://github.com/kevinpapst/kimai2/issues/503)
|
||||
- admin activity: visibility "none" \(no filter\) causes sql-error [\#491](https://github.com/kevinpapst/kimai2/issues/491)
|
||||
- login-screen optimizations [\#483](https://github.com/kevinpapst/kimai2/issues/483)
|
||||
- Configuration of roles to add/edit customers, projects, activities... [\#479](https://github.com/kevinpapst/kimai2/issues/479)
|
||||
- Line breaks for address and payment information fields [\#464](https://github.com/kevinpapst/kimai2/issues/464)
|
||||
- Possible to use a decimal in hourly rate field? [\#458](https://github.com/kevinpapst/kimai2/issues/458)
|
||||
- Invoice Number Generator possibly not compatible [\#454](https://github.com/kevinpapst/kimai2/issues/454)
|
||||
- users can change their role to system-admin [\#440](https://github.com/kevinpapst/kimai2/issues/440)
|
||||
- fix wrong include filename in user registration [\#520](https://github.com/kevinpapst/kimai2/pull/520) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix segmentation fault - rollback composer dependencies [\#463](https://github.com/kevinpapst/kimai2/pull/463) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed null project for advanced invoice calculator [\#462](https://github.com/kevinpapst/kimai2/pull/462) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix the restart timesheet button [\#436](https://github.com/kevinpapst/kimai2/pull/436) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Redirecting when using Kimai on a subdirectory + reverse proxy [\#492](https://github.com/kevinpapst/kimai2/issues/492)
|
||||
- Installation \(Cannot declare.... in use\) [\#455](https://github.com/kevinpapst/kimai2/issues/455)
|
||||
- New Number Generator not recognized [\#453](https://github.com/kevinpapst/kimai2/issues/453)
|
||||
- de/help/invoices returns 404 error [\#452](https://github.com/kevinpapst/kimai2/issues/452)
|
||||
- Can you Add Brazilian Portuguese Translation? I can help it.. [\#444](https://github.com/kevinpapst/kimai2/issues/444)
|
||||
- replace all selects with smart-selects and live-search [\#441](https://github.com/kevinpapst/kimai2/issues/441)
|
||||
- use parsedown-extra for rendering markdown [\#388](https://github.com/kevinpapst/kimai2/issues/388)
|
||||
- Think about a cooler name [\#133](https://github.com/kevinpapst/kimai2/issues/133)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- pagination without reload while keeping filters applied [\#521](https://github.com/kevinpapst/kimai2/pull/521) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- go back to calendar after editing and creation of time-records [\#519](https://github.com/kevinpapst/kimai2/pull/519) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fetch toolbar results without page reload [\#518](https://github.com/kevinpapst/kimai2/pull/518) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Form and theme improvements [\#513](https://github.com/kevinpapst/kimai2/pull/513) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- validation for future and negative times [\#512](https://github.com/kevinpapst/kimai2/pull/512) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- alphabetical order for selectboxes [\#510](https://github.com/kevinpapst/kimai2/pull/510) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Export team timesheets [\#508](https://github.com/kevinpapst/kimai2/pull/508) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix broken sql in activity admin [\#506](https://github.com/kevinpapst/kimai2/pull/506) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- display invoice entries in ascending order [\#505](https://github.com/kevinpapst/kimai2/pull/505) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improve DOCX template row [\#504](https://github.com/kevinpapst/kimai2/pull/504) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- do not display admin menu if it has no children [\#500](https://github.com/kevinpapst/kimai2/pull/500) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- login-screen optimizations [\#493](https://github.com/kevinpapst/kimai2/pull/493) ([lduer](https://github.com/lduer))
|
||||
- preg\_replace for md-file-extensions when rendering correct link… [\#482](https://github.com/kevinpapst/kimai2/pull/482) ([lduer](https://github.com/lduer))
|
||||
- Better composer install in the docker [\#481](https://github.com/kevinpapst/kimai2/pull/481) ([tobybatch](https://github.com/tobybatch))
|
||||
- Update dockerfile for https in composer installer [\#478](https://github.com/kevinpapst/kimai2/pull/478) ([scolson](https://github.com/scolson))
|
||||
- support line breaks in docx [\#466](https://github.com/kevinpapst/kimai2/pull/466) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated README and docu [\#465](https://github.com/kevinpapst/kimai2/pull/465) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow decimals in users hourly rate [\#460](https://github.com/kevinpapst/kimai2/pull/460) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated all composer packages [\#459](https://github.com/kevinpapst/kimai2/pull/459) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- moved some packages to dev requirements [\#456](https://github.com/kevinpapst/kimai2/pull/456) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added portuguese translations [\#446](https://github.com/kevinpapst/kimai2/pull/446) ([marquesmatheus](https://github.com/marquesmatheus))
|
||||
- Simplify timesheet edit form if only one customer is existing [\#443](https://github.com/kevinpapst/kimai2/pull/443) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added project variables for invoice templates [\#439](https://github.com/kevinpapst/kimai2/pull/439) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable permission system [\#424](https://github.com/kevinpapst/kimai2/pull/424) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Only one running timesheet: automatically stop others [\#386](https://github.com/kevinpapst/kimai2/pull/386) ([lduer](https://github.com/lduer))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.7)
|
||||
|
||||
## [0.6.1](https://github.com/kevinpapst/kimai2/tree/0.6.1) (2018-11-19)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6...0.6.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Error importing v1 Database [\#428](https://github.com/kevinpapst/kimai2/issues/428)
|
||||
- Redo button missing? [\#426](https://github.com/kevinpapst/kimai2/issues/426)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Error importing v1 Database \(could not convert to string\) [\#431](https://github.com/kevinpapst/kimai2/issues/431)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- fix migration for MySQL [\#430](https://github.com/kevinpapst/kimai2/pull/430) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix importer for MySQL [\#429](https://github.com/kevinpapst/kimai2/pull/429) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.6.1)
|
||||
|
||||
## [0.6](https://github.com/kevinpapst/kimai2/tree/0.6) (2018-11-18)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.5...0.6)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Language setting does not work immediately [\#418](https://github.com/kevinpapst/kimai2/issues/418)
|
||||
- Merge projects and/or activities [\#406](https://github.com/kevinpapst/kimai2/issues/406)
|
||||
- Global activities support for Kimai 1 importer [\#400](https://github.com/kevinpapst/kimai2/issues/400)
|
||||
- Invoice Grouped by Activity [\#379](https://github.com/kevinpapst/kimai2/issues/379)
|
||||
- Set the order of columns \(in my times page\) [\#371](https://github.com/kevinpapst/kimai2/issues/371)
|
||||
- Columns on small/narrow displays \(mobile\) on „my times“ page [\#370](https://github.com/kevinpapst/kimai2/issues/370)
|
||||
- Make settings more accessible [\#365](https://github.com/kevinpapst/kimai2/issues/365)
|
||||
- Data validation for register page username field. [\#360](https://github.com/kevinpapst/kimai2/issues/360)
|
||||
- Option for setting default calendar view [\#359](https://github.com/kevinpapst/kimai2/issues/359)
|
||||
- Activities just Duration [\#334](https://github.com/kevinpapst/kimai2/issues/334)
|
||||
- API: Add filtering/sorting/limits [\#333](https://github.com/kevinpapst/kimai2/issues/333)
|
||||
- API: Timesheets [\#315](https://github.com/kevinpapst/kimai2/issues/315)
|
||||
- Feature request: Quick search box for activities when creating new timesheet [\#307](https://github.com/kevinpapst/kimai2/issues/307)
|
||||
- Separate Customer / Project / Activity input when logging time [\#250](https://github.com/kevinpapst/kimai2/issues/250)
|
||||
- Interactive help after fresh installation [\#94](https://github.com/kevinpapst/kimai2/issues/94)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Invoicing export - 500: Internal Server Error [\#390](https://github.com/kevinpapst/kimai2/issues/390)
|
||||
- invalid CSRF-Token [\#344](https://github.com/kevinpapst/kimai2/issues/344)
|
||||
- fix column visibility if unconfigured \(no cookie existing\) [\#423](https://github.com/kevinpapst/kimai2/pull/423) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix duration and rate for html invoices [\#414](https://github.com/kevinpapst/kimai2/pull/414) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed broken toolbar form with empty date [\#391](https://github.com/kevinpapst/kimai2/pull/391) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- After installation no graphic/background/icons shown [\#417](https://github.com/kevinpapst/kimai2/issues/417)
|
||||
- Form fields should be always visible [\#408](https://github.com/kevinpapst/kimai2/issues/408)
|
||||
- Active filter should stay open [\#405](https://github.com/kevinpapst/kimai2/issues/405)
|
||||
- Make project field optional [\#401](https://github.com/kevinpapst/kimai2/issues/401)
|
||||
- Unrecognized Options "renderer, number\_generator" under "kimai.invoice" [\#381](https://github.com/kevinpapst/kimai2/issues/381)
|
||||
- 500: Internal Server Error after login \(new install\) [\#342](https://github.com/kevinpapst/kimai2/issues/342)
|
||||
- Invoices export error [\#341](https://github.com/kevinpapst/kimai2/issues/341)
|
||||
- add the same activity for multiple projects [\#325](https://github.com/kevinpapst/kimai2/issues/325)
|
||||
- API: Endpoint for API docs needs session cookie [\#319](https://github.com/kevinpapst/kimai2/issues/319)
|
||||
- docker image for kimai2 [\#284](https://github.com/kevinpapst/kimai2/issues/284)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- redirect to user language after profile update [\#421](https://github.com/kevinpapst/kimai2/pull/421) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added more timesheet related invoice fields [\#411](https://github.com/kevinpapst/kimai2/pull/411) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- toolbar improvements [\#410](https://github.com/kevinpapst/kimai2/pull/410) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- move entries from one entity to another upon deletion [\#409](https://github.com/kevinpapst/kimai2/pull/409) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- show toolbar filter if they were submitted [\#407](https://github.com/kevinpapst/kimai2/pull/407) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- full configurable data columns in all screen sizes [\#404](https://github.com/kevinpapst/kimai2/pull/404) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added global activity support in importer [\#402](https://github.com/kevinpapst/kimai2/pull/402) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added parsedown extension for header ids \(\#388\) [\#399](https://github.com/kevinpapst/kimai2/pull/399) ([lduer](https://github.com/lduer))
|
||||
- Fix for \#397 [\#398](https://github.com/kevinpapst/kimai2/pull/398) ([tobybatch](https://github.com/tobybatch))
|
||||
- Added links to docker hub and my docker repo [\#396](https://github.com/kevinpapst/kimai2/pull/396) ([tobybatch](https://github.com/tobybatch))
|
||||
- fixed issues- & milestone links [\#395](https://github.com/kevinpapst/kimai2/pull/395) ([lduer](https://github.com/lduer))
|
||||
- added links to further docker images by the community [\#394](https://github.com/kevinpapst/kimai2/pull/394) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- beta test: added quick search box for customers, projects and activities [\#392](https://github.com/kevinpapst/kimai2/pull/392) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated the documentation, reflecting the changes in \#306 [\#387](https://github.com/kevinpapst/kimai2/pull/387) ([lduer](https://github.com/lduer))
|
||||
- Added Hungarian translation and fixed the path of kimai.yaml in translation documentation [\#385](https://github.com/kevinpapst/kimai2/pull/385) ([infeeeee](https://github.com/infeeeee))
|
||||
- improved docs and cross-linked docker [\#383](https://github.com/kevinpapst/kimai2/pull/383) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Doc update of install/update instructions re: username [\#382](https://github.com/kevinpapst/kimai2/pull/382) ([srdco](https://github.com/srdco))
|
||||
- invoice calculator to group entries by activity [\#380](https://github.com/kevinpapst/kimai2/pull/380) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- basic about screen with server and debug infos [\#378](https://github.com/kevinpapst/kimai2/pull/378) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Dev/demo dockerfile [\#377](https://github.com/kevinpapst/kimai2/pull/377) ([tobybatch](https://github.com/tobybatch))
|
||||
- Sidebar - user-profile links and language settings [\#369](https://github.com/kevinpapst/kimai2/pull/369) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable initial calendar view [\#363](https://github.com/kevinpapst/kimai2/pull/363) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- decrease minimum username to 3 character [\#362](https://github.com/kevinpapst/kimai2/pull/362) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Default country \(\#2\) [\#358](https://github.com/kevinpapst/kimai2/pull/358) ([tobybatch](https://github.com/tobybatch))
|
||||
- composer: removed deprecated symfony/lt, updated others [\#354](https://github.com/kevinpapst/kimai2/pull/354) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated admin-lte theme bundle to 2.1.1 [\#350](https://github.com/kevinpapst/kimai2/pull/350) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated timesheet and email documentation [\#349](https://github.com/kevinpapst/kimai2/pull/349) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated required dependencies [\#346](https://github.com/kevinpapst/kimai2/pull/346) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fix password reset [\#345](https://github.com/kevinpapst/kimai2/pull/345) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added arabic translations and locale support in datepicker [\#337](https://github.com/kevinpapst/kimai2/pull/337) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- API endpoint for Timesheet entries [\#332](https://github.com/kevinpapst/kimai2/pull/332) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Support global activities [\#259](https://github.com/kevinpapst/kimai2/pull/259) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.6)
|
||||
|
||||
## [0.5](https://github.com/kevinpapst/kimai2/tree/0.5) (2018-09-27)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.4...0.5)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- API: Kimai metadata endpoint [\#320](https://github.com/kevinpapst/kimai2/issues/320)
|
||||
- Support Markdown in timesheet description [\#295](https://github.com/kevinpapst/kimai2/issues/295)
|
||||
- Add support for MS Office templates [\#283](https://github.com/kevinpapst/kimai2/issues/283)
|
||||
- Adding rates to customers, projects and tasks [\#271](https://github.com/kevinpapst/kimai2/issues/271)
|
||||
- Delete invoice templates [\#267](https://github.com/kevinpapst/kimai2/issues/267)
|
||||
- Sum of hours in timesheet invoice [\#262](https://github.com/kevinpapst/kimai2/issues/262)
|
||||
- Feature request - API [\#251](https://github.com/kevinpapst/kimai2/issues/251)
|
||||
- Feature request - PDF Download for monthly report [\#244](https://github.com/kevinpapst/kimai2/issues/244)
|
||||
- Add support for OpenOffice templates [\#223](https://github.com/kevinpapst/kimai2/issues/223)
|
||||
- Feature Request - Excel Output of times [\#221](https://github.com/kevinpapst/kimai2/issues/221)
|
||||
- Add hourly rate options [\#111](https://github.com/kevinpapst/kimai2/issues/111)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Excel Output not containing data [\#327](https://github.com/kevinpapst/kimai2/issues/327)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Creating database schema throws exceptions [\#322](https://github.com/kevinpapst/kimai2/issues/322)
|
||||
- Add permission voter for invoices and templates [\#293](https://github.com/kevinpapst/kimai2/issues/293)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- added copy invoice template action [\#331](https://github.com/kevinpapst/kimai2/pull/331) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix spreadsheet renderer for invoices with one entry [\#328](https://github.com/kevinpapst/kimai2/pull/328) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- invoice VAT as float and database key length [\#323](https://github.com/kevinpapst/kimai2/pull/323) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added version command + api endpoint [\#321](https://github.com/kevinpapst/kimai2/pull/321) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Timesheet export [\#317](https://github.com/kevinpapst/kimai2/pull/317) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added invoice voter [\#316](https://github.com/kevinpapst/kimai2/pull/316) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Refactored invoice system [\#306](https://github.com/kevinpapst/kimai2/pull/306) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- add stable controller tests [\#305](https://github.com/kevinpapst/kimai2/pull/305) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fixed rate and Hourly rates [\#304](https://github.com/kevinpapst/kimai2/pull/304) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- add hourly\_rate and fixed\_rate to timesheet entries [\#302](https://github.com/kevinpapst/kimai2/pull/302) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow to switch language on any page [\#299](https://github.com/kevinpapst/kimai2/pull/299) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fix duration\_short format for french translation [\#297](https://github.com/kevinpapst/kimai2/pull/297) ([jeau](https://github.com/jeau))
|
||||
- allow markdown in timesheet descriptions [\#296](https://github.com/kevinpapst/kimai2/pull/296) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added option to delete invoice template [\#294](https://github.com/kevinpapst/kimai2/pull/294) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved invoice preview [\#254](https://github.com/kevinpapst/kimai2/pull/254) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.5)
|
||||
|
||||
## [0.4](https://github.com/kevinpapst/kimai2/tree/0.4) (2018-09-01)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.3...0.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Delete user [\#225](https://github.com/kevinpapst/kimai2/issues/225)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Don't allow to stop an already stopped entry [\#282](https://github.com/kevinpapst/kimai2/issues/282)
|
||||
- installed it, and then what??? [\#255](https://github.com/kevinpapst/kimai2/issues/255)
|
||||
- fixed calendar for empty timesheet descriptions [\#265](https://github.com/kevinpapst/kimai2/pull/265) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed .htaccess for apache users [\#260](https://github.com/kevinpapst/kimai2/pull/260) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Spanish Translation Contrib [\#248](https://github.com/kevinpapst/kimai2/issues/248)
|
||||
- Administration of projects throws error if locale is "de" [\#247](https://github.com/kevinpapst/kimai2/issues/247)
|
||||
- Web Installer Package for near-automated installs from browser [\#235](https://github.com/kevinpapst/kimai2/issues/235)
|
||||
- Improve dashboard display [\#280](https://github.com/kevinpapst/kimai2/issues/280)
|
||||
- Feature request - Monthly overview / report [\#207](https://github.com/kevinpapst/kimai2/issues/207)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- fixed money display in dashboard widgets [\#291](https://github.com/kevinpapst/kimai2/pull/291) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Symfony update 4.1.4 [\#290](https://github.com/kevinpapst/kimai2/pull/290) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- do not allow to stop already stopped timesheets [\#289](https://github.com/kevinpapst/kimai2/pull/289) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable formats for duration [\#287](https://github.com/kevinpapst/kimai2/pull/287) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated russian translations [\#286](https://github.com/kevinpapst/kimai2/pull/286) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Update french translation [\#279](https://github.com/kevinpapst/kimai2/pull/279) ([jeau](https://github.com/jeau))
|
||||
- Fix standard date format in french [\#278](https://github.com/kevinpapst/kimai2/pull/278) ([jeau](https://github.com/jeau))
|
||||
- new translations: spanish, french and italian [\#276](https://github.com/kevinpapst/kimai2/pull/276) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- French translation for Kimai [\#275](https://github.com/kevinpapst/kimai2/pull/275) ([jeau](https://github.com/jeau))
|
||||
- updated to AdminLTE 2.4.8 [\#274](https://github.com/kevinpapst/kimai2/pull/274) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixing migration\_v1.md url [\#270](https://github.com/kevinpapst/kimai2/pull/270) ([ltsavar](https://github.com/ltsavar))
|
||||
- dashboard widgets are configurable via config [\#269](https://github.com/kevinpapst/kimai2/pull/269) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Spanish translation for Kimai [\#266](https://github.com/kevinpapst/kimai2/pull/266) ([cobaltos](https://github.com/cobaltos))
|
||||
- moved theme settings to dynamic compiler pass [\#264](https://github.com/kevinpapst/kimai2/pull/264) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added config setting to disable password-reset and user-registration [\#261](https://github.com/kevinpapst/kimai2/pull/261) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added basic API endpoints [\#258](https://github.com/kevinpapst/kimai2/pull/258) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Hotfix for Symfony Flex incompatibility with latest composer changes [\#257](https://github.com/kevinpapst/kimai2/pull/257) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added italian translation [\#253](https://github.com/kevinpapst/kimai2/pull/253) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Delete user [\#249](https://github.com/kevinpapst/kimai2/pull/249) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- dashboard widgets from event [\#246](https://github.com/kevinpapst/kimai2/pull/246) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added timesheet-calendar view [\#236](https://github.com/kevinpapst/kimai2/pull/236) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved data fixtures [\#234](https://github.com/kevinpapst/kimai2/pull/234) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.4)
|
||||
|
||||
## [0.3](https://github.com/kevinpapst/kimai2/tree/0.3) (2018-07-22)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.2...0.3)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Install Kimai as WebApp [\#203](https://github.com/kevinpapst/kimai2/issues/203)
|
||||
- Replace AvanzuAdminTheme with AdminLTE bundle [\#201](https://github.com/kevinpapst/kimai2/issues/201)
|
||||
- Add dynamic column filter [\#174](https://github.com/kevinpapst/kimai2/issues/174)
|
||||
- Add register user function [\#164](https://github.com/kevinpapst/kimai2/issues/164)
|
||||
- Add forgot password function - login screen [\#163](https://github.com/kevinpapst/kimai2/issues/163)
|
||||
- Integrate FOSUserBundle \(for registration, password-reset, etc.\) [\#144](https://github.com/kevinpapst/kimai2/issues/144)
|
||||
- added dynamic column filter \#174 [\#184](https://github.com/kevinpapst/kimai2/pull/184) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Missing assets in fresh installation [\#213](https://github.com/kevinpapst/kimai2/issues/213)
|
||||
- Cannot remove payment terms from invoice template [\#188](https://github.com/kevinpapst/kimai2/issues/188)
|
||||
- Set PHP locale for date format and month names [\#110](https://github.com/kevinpapst/kimai2/issues/110)
|
||||
- Show revenue only for Admin \(wrong currency shown\) [\#19](https://github.com/kevinpapst/kimai2/issues/19)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Installation Issue of version1.3.1 [\#208](https://github.com/kevinpapst/kimai2/issues/208)
|
||||
- Error on bin/console doctrine:schema:create [\#191](https://github.com/kevinpapst/kimai2/issues/191)
|
||||
- Verifying email configuration [\#226](https://github.com/kevinpapst/kimai2/issues/226)
|
||||
- Add favicon [\#205](https://github.com/kevinpapst/kimai2/issues/205)
|
||||
- Document DB requirements in install docu [\#196](https://github.com/kevinpapst/kimai2/issues/196)
|
||||
- easier creation of tasks [\#195](https://github.com/kevinpapst/kimai2/issues/195)
|
||||
- Integrate lock bot [\#183](https://github.com/kevinpapst/kimai2/issues/183)
|
||||
- Upgrade to FontAwesome 5 [\#179](https://github.com/kevinpapst/kimai2/issues/179)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Easier creation of Tasks/Projects/User [\#230](https://github.com/kevinpapst/kimai2/pull/230) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added email configuration docs [\#228](https://github.com/kevinpapst/kimai2/pull/228) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Show revenue only for ROLE\_ADMIN [\#227](https://github.com/kevinpapst/kimai2/pull/227) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- hotfix responsiveness [\#220](https://github.com/kevinpapst/kimai2/pull/220) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Integrated FOSUserBundle [\#216](https://github.com/kevinpapst/kimai2/pull/216) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added auto-script to install assets [\#214](https://github.com/kevinpapst/kimai2/pull/214) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added yunohost installation link [\#212](https://github.com/kevinpapst/kimai2/pull/212) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added favicons and web app metadata [\#211](https://github.com/kevinpapst/kimai2/pull/211) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added FAQ and database requirement docu \#196 [\#210](https://github.com/kevinpapst/kimai2/pull/210) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated to AdminLTEBundle 1.0 [\#206](https://github.com/kevinpapst/kimai2/pull/206) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- replaced AvanzuAdminTheme with AdminLTE bundle [\#202](https://github.com/kevinpapst/kimai2/pull/202) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow empty invoice template terms \#188 [\#189](https://github.com/kevinpapst/kimai2/pull/189) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- improved install docs and composer [\#187](https://github.com/kevinpapst/kimai2/pull/187) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added lock-bot config \#183 [\#186](https://github.com/kevinpapst/kimai2/pull/186) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Composer package name [\#185](https://github.com/kevinpapst/kimai2/pull/185) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated to font-awesome 5 \#179 [\#181](https://github.com/kevinpapst/kimai2/pull/181) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- language specific money and date display [\#180](https://github.com/kevinpapst/kimai2/pull/180) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.3)
|
||||
|
||||
## [0.2](https://github.com/kevinpapst/kimai2/tree/0.2) (2018-06-23)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.1...0.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add english translation [\#8](https://github.com/kevinpapst/kimai2/issues/8)
|
||||
- Add configurable system configuration [\#6](https://github.com/kevinpapst/kimai2/issues/6)
|
||||
- Hide notifications after some seconds [\#169](https://github.com/kevinpapst/kimai2/issues/169)
|
||||
- Setup Scrutinizer to run code sniffer [\#158](https://github.com/kevinpapst/kimai2/issues/158)
|
||||
- Local config to overwrite settings [\#153](https://github.com/kevinpapst/kimai2/issues/153)
|
||||
- Improve mobile view [\#151](https://github.com/kevinpapst/kimai2/issues/151)
|
||||
- Add help text to form elements [\#138](https://github.com/kevinpapst/kimai2/issues/138)
|
||||
- Add duration-only mode [\#131](https://github.com/kevinpapst/kimai2/issues/131)
|
||||
- Add help controller [\#129](https://github.com/kevinpapst/kimai2/issues/129)
|
||||
- Secure create-user command [\#123](https://github.com/kevinpapst/kimai2/issues/123)
|
||||
- Add command for building a production release [\#115](https://github.com/kevinpapst/kimai2/issues/115)
|
||||
- Add webpack and webpack-encore for frontend assets [\#113](https://github.com/kevinpapst/kimai2/issues/113)
|
||||
- Add time rounding option [\#112](https://github.com/kevinpapst/kimai2/issues/112)
|
||||
- Add field order number in Projects [\#107](https://github.com/kevinpapst/kimai2/issues/107)
|
||||
- Create docu for invoice templates [\#106](https://github.com/kevinpapst/kimai2/issues/106)
|
||||
- Add invoice template: timesheet [\#104](https://github.com/kevinpapst/kimai2/issues/104)
|
||||
- Add importer from Kimai v1 [\#102](https://github.com/kevinpapst/kimai2/issues/102)
|
||||
- Setup error pages [\#100](https://github.com/kevinpapst/kimai2/issues/100)
|
||||
- Add basic HTML invoice rendering [\#97](https://github.com/kevinpapst/kimai2/issues/97)
|
||||
- Filter user in timesheet admin [\#93](https://github.com/kevinpapst/kimai2/issues/93)
|
||||
- Support to switch theme settings [\#89](https://github.com/kevinpapst/kimai2/issues/89)
|
||||
- Roles should become a real table [\#86](https://github.com/kevinpapst/kimai2/issues/86)
|
||||
- Fix Doctrine extensions config loader [\#84](https://github.com/kevinpapst/kimai2/issues/84)
|
||||
- Upgrade to Symfony 4 and Flex [\#74](https://github.com/kevinpapst/kimai2/issues/74)
|
||||
- Add remember me login [\#53](https://github.com/kevinpapst/kimai2/issues/53)
|
||||
- Add preferences sidebar [\#10](https://github.com/kevinpapst/kimai2/issues/10)
|
||||
- Add user settings [\#9](https://github.com/kevinpapst/kimai2/issues/9)
|
||||
- Mobile alignments and toolbar \#151 [\#157](https://github.com/kevinpapst/kimai2/pull/157) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Local config \#153 [\#156](https://github.com/kevinpapst/kimai2/pull/156) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Filter in hidden toolbar - improved mobile \#151 [\#155](https://github.com/kevinpapst/kimai2/pull/155) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved mobile view \#151 [\#150](https://github.com/kevinpapst/kimai2/pull/150) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Change to users language after login [\#7](https://github.com/kevinpapst/kimai2/issues/7)
|
||||
- Prevent end time before start time [\#152](https://github.com/kevinpapst/kimai2/issues/152)
|
||||
- Importing old data fails [\#145](https://github.com/kevinpapst/kimai2/issues/145)
|
||||
- Translate LanguageType choices [\#83](https://github.com/kevinpapst/kimai2/issues/83)
|
||||
- Mobile alignments and toolbar \\#151 [\#157](https://github.com/kevinpapst/kimai2/pull/157) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved mobile view \\#151 [\#150](https://github.com/kevinpapst/kimai2/pull/150) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Add CHANGELOG.md [\#177](https://github.com/kevinpapst/kimai2/issues/177)
|
||||
- Add preview blog message and info text on old website [\#162](https://github.com/kevinpapst/kimai2/issues/162)
|
||||
- Upgrade to Symfony 4.1 [\#149](https://github.com/kevinpapst/kimai2/issues/149)
|
||||
- Replace has\_role\(\) with is\_granted\(\) [\#148](https://github.com/kevinpapst/kimai2/issues/148)
|
||||
- Duration mode: creating new timesheet records prefills duration with some seconds [\#137](https://github.com/kevinpapst/kimai2/issues/137)
|
||||
- \[RFC\] Encore: configureFilenames with hash after filename instead of in the filename [\#125](https://github.com/kevinpapst/kimai2/issues/125)
|
||||
- \[RFC\] Kimai license header [\#124](https://github.com/kevinpapst/kimai2/issues/124)
|
||||
- Setup travis to run phpunit tests [\#120](https://github.com/kevinpapst/kimai2/issues/120)
|
||||
- Add russian translation [\#95](https://github.com/kevinpapst/kimai2/issues/95)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- \[WIP\] Translations [\#173](https://github.com/kevinpapst/kimai2/pull/173) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- various documentation updates [\#176](https://github.com/kevinpapst/kimai2/pull/176) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code styles: header\_comment and ordered\_imports [\#175](https://github.com/kevinpapst/kimai2/pull/175) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Code sniffer rules \#158 [\#172](https://github.com/kevinpapst/kimai2/pull/172) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Set default locale for unit-tests [\#171](https://github.com/kevinpapst/kimai2/pull/171) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Hide success notification after 5 seconds [\#170](https://github.com/kevinpapst/kimai2/pull/170) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added validation for end date before start date [\#167](https://github.com/kevinpapst/kimai2/pull/167) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Security expression \#148 [\#166](https://github.com/kevinpapst/kimai2/pull/166) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Composer install [\#165](https://github.com/kevinpapst/kimai2/pull/165) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added scrutinizer config \#158 [\#161](https://github.com/kevinpapst/kimai2/pull/161) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Update to Symfony 4.1 \#149 [\#160](https://github.com/kevinpapst/kimai2/pull/160) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated README [\#159](https://github.com/kevinpapst/kimai2/pull/159) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added russian translation \#95 [\#147](https://github.com/kevinpapst/kimai2/pull/147) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Bugfix importer \(\#145\) [\#146](https://github.com/kevinpapst/kimai2/pull/146) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- help text and link for form labels \#138 [\#143](https://github.com/kevinpapst/kimai2/pull/143) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- bugfix for duration\_only mode \#137 [\#142](https://github.com/kevinpapst/kimai2/pull/142) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Make the back link appear like a normal link instead of a button [\#139](https://github.com/kevinpapst/kimai2/pull/139) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- Use short array syntax [\#135](https://github.com/kevinpapst/kimai2/pull/135) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- Duration only mode \#131 [\#134](https://github.com/kevinpapst/kimai2/pull/134) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added controller to render markdown documentation \#129 [\#132](https://github.com/kevinpapst/kimai2/pull/132) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Rounding rules [\#128](https://github.com/kevinpapst/kimai2/pull/128) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Secure create-user command [\#127](https://github.com/kevinpapst/kimai2/pull/127) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- ConfigureFilenames with hash after filename [\#126](https://github.com/kevinpapst/kimai2/pull/126) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- added webpack-encore for managing frontend assets \#113 [\#122](https://github.com/kevinpapst/kimai2/pull/122) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Remove personal mentions in license header [\#119](https://github.com/kevinpapst/kimai2/pull/119) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- Add github issue and pull request template [\#118](https://github.com/kevinpapst/kimai2/pull/118) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- Improvements [\#114](https://github.com/kevinpapst/kimai2/pull/114) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added first docu on extending kimai with bundles \#106 [\#109](https://github.com/kevinpapst/kimai2/pull/109) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added order number to projects \#107 [\#108](https://github.com/kevinpapst/kimai2/pull/108) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added short invoice calculator and timesheet invoice template \#104 [\#105](https://github.com/kevinpapst/kimai2/pull/105) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added command to import data from kimai v1 \#102 [\#103](https://github.com/kevinpapst/kimai2/pull/103) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Error pages \#100 [\#101](https://github.com/kevinpapst/kimai2/pull/101) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added basic invoice rendering \#97 [\#99](https://github.com/kevinpapst/kimai2/pull/99) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added remember me feature \#53 [\#98](https://github.com/kevinpapst/kimai2/pull/98) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code-cleanup: removed unused classes, added role constants, phpcs fixes [\#96](https://github.com/kevinpapst/kimai2/pull/96) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- load doctrine extensions during kernel bootstrap \#84 [\#92](https://github.com/kevinpapst/kimai2/pull/92) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- support theme options via user preferences \#89 [\#91](https://github.com/kevinpapst/kimai2/pull/91) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added dynamic user preferences \#7 \#9 [\#90](https://github.com/kevinpapst/kimai2/pull/90) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added control sidebar \#10 [\#87](https://github.com/kevinpapst/kimai2/pull/87) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added english translation \#8 [\#82](https://github.com/kevinpapst/kimai2/pull/82) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- upgraded to symfony 4 \#74 [\#81](https://github.com/kevinpapst/kimai2/pull/81) ([kevinpapst](https://github.com/kevinpapst))
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.2)
|
||||
|
||||
## [0.1](https://github.com/kevinpapst/kimai2/tree/0.1) (2018-01-10)
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Hide inactive delete user button [\#78](https://github.com/kevinpapst/kimai2/issues/78)
|
||||
- Delete activity [\#71](https://github.com/kevinpapst/kimai2/issues/71)
|
||||
- Delete other users timesheet entries [\#70](https://github.com/kevinpapst/kimai2/issues/70)
|
||||
- Delete Project [\#69](https://github.com/kevinpapst/kimai2/issues/69)
|
||||
- Delete Customer [\#68](https://github.com/kevinpapst/kimai2/issues/68)
|
||||
- Delete timesheet entries [\#67](https://github.com/kevinpapst/kimai2/issues/67)
|
||||
- Add installation docu [\#64](https://github.com/kevinpapst/kimai2/issues/64)
|
||||
- Disallow login for inactive user [\#57](https://github.com/kevinpapst/kimai2/issues/57)
|
||||
- Add toolbar to filter user screen [\#56](https://github.com/kevinpapst/kimai2/issues/56)
|
||||
- Upgrade to latest Avanzu version [\#55](https://github.com/kevinpapst/kimai2/issues/55)
|
||||
- AdminLTE login screen [\#52](https://github.com/kevinpapst/kimai2/issues/52)
|
||||
- Add edit Timesheet entry for Teamleads [\#45](https://github.com/kevinpapst/kimai2/issues/45)
|
||||
- Add Timesheet Voter [\#44](https://github.com/kevinpapst/kimai2/issues/44)
|
||||
- Developer improvements [\#42](https://github.com/kevinpapst/kimai2/issues/42)
|
||||
- Base class for repository and query objects [\#40](https://github.com/kevinpapst/kimai2/issues/40)
|
||||
- Add console command to create user [\#36](https://github.com/kevinpapst/kimai2/issues/36)
|
||||
- Add basic security Voter [\#34](https://github.com/kevinpapst/kimai2/issues/34)
|
||||
- Do not show hidden Activities [\#33](https://github.com/kevinpapst/kimai2/issues/33)
|
||||
- Do not show hidden Projects [\#32](https://github.com/kevinpapst/kimai2/issues/32)
|
||||
- Add "create timesheet record" form [\#31](https://github.com/kevinpapst/kimai2/issues/31)
|
||||
- Add filter to Customer admin [\#28](https://github.com/kevinpapst/kimai2/issues/28)
|
||||
- Do not show hidden Customer [\#27](https://github.com/kevinpapst/kimai2/issues/27)
|
||||
- Rename Timesheet query class [\#25](https://github.com/kevinpapst/kimai2/issues/25)
|
||||
- Add "create activity" functionality [\#24](https://github.com/kevinpapst/kimai2/issues/24)
|
||||
- Add "create project" functionality [\#23](https://github.com/kevinpapst/kimai2/issues/23)
|
||||
- Add "create customer" functionality [\#22](https://github.com/kevinpapst/kimai2/issues/22)
|
||||
- Translate dashboard widgets [\#20](https://github.com/kevinpapst/kimai2/issues/20)
|
||||
- Display user avatar in navbar [\#16](https://github.com/kevinpapst/kimai2/issues/16)
|
||||
- Add role SUPER\_ADMIN [\#15](https://github.com/kevinpapst/kimai2/issues/15)
|
||||
- Teamlead can see all user times [\#13](https://github.com/kevinpapst/kimai2/issues/13)
|
||||
- Add filter to Timesheet \(user\) [\#11](https://github.com/kevinpapst/kimai2/issues/11)
|
||||
- Add "create user" functionality [\#5](https://github.com/kevinpapst/kimai2/issues/5)
|
||||
- Edit users role [\#4](https://github.com/kevinpapst/kimai2/issues/4)
|
||||
- Add filter to Timesheet admin [\#3](https://github.com/kevinpapst/kimai2/issues/3)
|
||||
- Add filter to Activity admin [\#2](https://github.com/kevinpapst/kimai2/issues/2)
|
||||
- Add filter to Project admin [\#1](https://github.com/kevinpapst/kimai2/issues/1)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- hide inactive delete user button \#78 [\#79](https://github.com/kevinpapst/kimai2/pull/79) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete customer action \#68 [\#77](https://github.com/kevinpapst/kimai2/pull/77) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete action for projects \#69 [\#76](https://github.com/kevinpapst/kimai2/pull/76) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Delete actions \#71 [\#75](https://github.com/kevinpapst/kimai2/pull/75) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete timesheet record for admins \#70 [\#73](https://github.com/kevinpapst/kimai2/pull/73) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete timesheet records action for users \#67 [\#72](https://github.com/kevinpapst/kimai2/pull/72) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added toolbar to user screen \#56 [\#66](https://github.com/kevinpapst/kimai2/pull/66) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- document installation options \#64 [\#65](https://github.com/kevinpapst/kimai2/pull/65) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- update avanzu admin bundle \#55 [\#63](https://github.com/kevinpapst/kimai2/pull/63) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- prevent inactive user from login \#57 [\#61](https://github.com/kevinpapst/kimai2/pull/61) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- handle hidden customer \#27 [\#60](https://github.com/kevinpapst/kimai2/pull/60) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- handle hidden projects \#32 [\#59](https://github.com/kevinpapst/kimai2/pull/59) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Handle hidden activities \#33 [\#58](https://github.com/kevinpapst/kimai2/pull/58) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- using the AdminLTE login screen \#52 [\#54](https://github.com/kevinpapst/kimai2/pull/54) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added toolbar for activity screen \#2 [\#51](https://github.com/kevinpapst/kimai2/pull/51) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added toolbar to project screen \#1 [\#50](https://github.com/kevinpapst/kimai2/pull/50) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added toolbar to customer screen \#28 [\#49](https://github.com/kevinpapst/kimai2/pull/49) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added form to edit timesheet entries for all user \#45 [\#48](https://github.com/kevinpapst/kimai2/pull/48) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Create timesheet for User \#31 [\#47](https://github.com/kevinpapst/kimai2/pull/47) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added timesheet voter \#44 [\#46](https://github.com/kevinpapst/kimai2/pull/46) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Developer cleanup \#42 [\#43](https://github.com/kevinpapst/kimai2/pull/43) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Repository cleanup [\#41](https://github.com/kevinpapst/kimai2/pull/41) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added voter for customer, project and activity [\#39](https://github.com/kevinpapst/kimai2/pull/39) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added create user console command [\#38](https://github.com/kevinpapst/kimai2/pull/38) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added "create user" functionality [\#37](https://github.com/kevinpapst/kimai2/pull/37) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added "create activity" form [\#35](https://github.com/kevinpapst/kimai2/pull/35) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added create project functionality [\#30](https://github.com/kevinpapst/kimai2/pull/30) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Rename TimesheetQuery class [\#29](https://github.com/kevinpapst/kimai2/pull/29) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Add customer functionality [\#26](https://github.com/kevinpapst/kimai2/pull/26) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Dashboard widgets [\#21](https://github.com/kevinpapst/kimai2/pull/21) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added form to edit user roles [\#18](https://github.com/kevinpapst/kimai2/pull/18) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added role ROLE\_SUPER\_ADMIN [\#17](https://github.com/kevinpapst/kimai2/pull/17) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow TEAMLEAD to see user times [\#14](https://github.com/kevinpapst/kimai2/pull/14) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Timesheet toolbar [\#12](https://github.com/kevinpapst/kimai2/pull/12) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
|
||||
|
||||
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
|
||||
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.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.0 | :white_check_mark: |
|
||||
| < 1.0 | :x: |
|
||||
| 1.2 | :white_check_mark: |
|
||||
| < 1.2 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
26
UPGRADING.md
26
UPGRADING.md
@@ -5,10 +5,30 @@ _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)
|
||||
|
||||
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0 / 1.0.1.
|
||||
|
||||
## [1.0.1](https://github.com/kevinpapst/kimai2/releases/tag/1.0.1)
|
||||
|
||||
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0.
|
||||
|
||||
## [1.0](https://github.com/kevinpapst/kimai2/releases/tag/1.0)
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@ require('fullcalendar/dist/locale/hu');
|
||||
require('fullcalendar/dist/locale/pt-br');
|
||||
require('fullcalendar/dist/locale/sv');
|
||||
require('fullcalendar/dist/locale/ja');
|
||||
require('fullcalendar/dist/locale/sk');
|
||||
|
||||
require('fullcalendar/dist/fullcalendar.min.css');
|
||||
|
||||
@@ -89,9 +89,6 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
|
||||
jQuery(html).find('#form_modal .modal-content')
|
||||
);
|
||||
|
||||
// TODO these should be handled with Events, probably using Custom Events
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
|
||||
|
||||
// activate new loaded widgets
|
||||
self.getContainer().getPlugin('date-time-picker').activateDateTimePicker(formIdentifier);
|
||||
self.getContainer().getPlugin('autocomplete').activateAutocomplete(formIdentifier + " .js-autocomplete");
|
||||
|
||||
@@ -47,7 +47,6 @@ export default class KimaiDatatable extends KimaiPlugin {
|
||||
}
|
||||
|
||||
reloadDatatable() {
|
||||
// FIXME remove query
|
||||
const durations = this.getContainer().getPlugin('timesheet-duration');
|
||||
const form = jQuery('.toolbar form');
|
||||
let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
|
||||
|
||||
@@ -67,10 +67,44 @@ export default class KimaiSelectDataAPI extends KimaiPlugin {
|
||||
select.append('<option value="">' + emptyOption.text() + '</option>');
|
||||
}
|
||||
|
||||
jQuery.each(data, function(i, obj) {
|
||||
select.append('<option value="' + obj.id + '">' + obj.name + '</option>');
|
||||
const options = {};
|
||||
for (const apiData of data) {
|
||||
let title = apiData.parentTitle;
|
||||
if (title === null) {
|
||||
title = '__empty__';
|
||||
}
|
||||
if (!options.hasOwnProperty(title)) {
|
||||
options[title] = [];
|
||||
}
|
||||
options[title].push(apiData);
|
||||
}
|
||||
|
||||
const ordered = {};
|
||||
Object.keys(options).sort().forEach(function(key) {
|
||||
ordered[key] = options[key];
|
||||
});
|
||||
|
||||
let htmlOptions = '';
|
||||
let emptyOptions = '';
|
||||
|
||||
for (const [key, value] of Object.entries(ordered)) {
|
||||
if (key === '__empty__') {
|
||||
for (const entity of value) {
|
||||
emptyOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
htmlOptions += '<optgroup label="' + key + '">';
|
||||
for (const entity of value) {
|
||||
htmlOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
|
||||
}
|
||||
htmlOptions += '</optgroup>';
|
||||
}
|
||||
|
||||
select.append(htmlOptions);
|
||||
select.append(emptyOptions);
|
||||
|
||||
// if we don't trigger the change, the other selects won't be resetted
|
||||
select.trigger('change');
|
||||
|
||||
|
||||
@@ -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,10 +69,16 @@
|
||||
"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": {
|
||||
"php": "7.2"
|
||||
},
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
},
|
||||
@@ -135,7 +138,7 @@
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true,
|
||||
"require": "~4.2.0"
|
||||
"require": "4.3.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2377
composer.lock
generated
2377
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,24 +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'
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# FORMS
|
||||
# Default settings used to populate forms
|
||||
# --------------------------------------------------------------------------------
|
||||
# defaults:
|
||||
# customer:
|
||||
# timezone: Europe/Berlin
|
||||
# country: DE
|
||||
# currency: EUR
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -293,12 +292,3 @@ kimai:
|
||||
data_dir: '%kernel.project_dir%/var/data'
|
||||
plugin_dir: '%kernel.project_dir%/var/plugins'
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# LDAP
|
||||
# Connect to your companies directory server, see https://www.kimai.org/documentation/ldap.html
|
||||
# --------------------------------------------------------------------------------
|
||||
# ldap:
|
||||
# active: true
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,8 +10,8 @@ monolog:
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
console:
|
||||
type: console
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -35,6 +35,11 @@ App\Entity\Activity:
|
||||
metaFields:
|
||||
exclude: true
|
||||
virtual_properties:
|
||||
parentTitle:
|
||||
serialized_name: parentTitle
|
||||
include: true
|
||||
exp: "object.getProject() === null ? null : object.getProject().getName()"
|
||||
groups: [Default]
|
||||
getProject:
|
||||
serialized_name: project
|
||||
exp: "object.getProject() === null ? null : object.getProject().getId()"
|
||||
|
||||
@@ -33,6 +33,11 @@ App\Entity\Project:
|
||||
metaFields:
|
||||
exclude: true
|
||||
virtual_properties:
|
||||
parentTitle:
|
||||
serialized_name: parentTitle
|
||||
include: true
|
||||
exp: "object.getCustomer() === null ? null : object.getCustomer().getName()"
|
||||
groups: [Default]
|
||||
getCustomer:
|
||||
serialized_name: customer
|
||||
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"
|
||||
|
||||
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
|
||||
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.0ac6f178.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.0ac6f178.js": "sha384-eORpGh3VKK5IISACV4rmp8CpkZ+s47G8aKaZnXVpswgZccU/YcJPkSa5+jhcI88T",
|
||||
"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.0ac6f178.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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +69,7 @@ class ActivityController extends BaseApiController
|
||||
* @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter activities. If none is provided, all activities will be returned.")
|
||||
* @Rest\QueryParam(name="visible", requirements="1|2|3", strict=true, nullable=true, description="Visibility status to filter activities. Allowed values: 1=visible, 2=hidden, 3=all (default: 1)")
|
||||
* @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Use if you want to fetch only global activities. Allowed values: true (default: false)")
|
||||
* @Rest\QueryParam(name="globalsFirst", requirements="false", strict=true, nullable=true, description="Use if you don't want global activities to be listed first. Allowed values: false (default: true)")
|
||||
* @Rest\QueryParam(name="globalsFirst", requirements="true|false", strict=true, nullable=true, description="Deprecated parameter, value is not used any more")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|name|project", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, name, project (default: name)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: ASC)")
|
||||
*
|
||||
@@ -71,10 +78,6 @@ class ActivityController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setOrderGlobalsFirst(true)
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
@@ -88,8 +91,8 @@ class ActivityController extends BaseApiController
|
||||
$query->setGlobalsOnly(true);
|
||||
}
|
||||
|
||||
if ('false' === $paramFetcher->get('globalsFirst')) {
|
||||
$query->setOrderGlobalsFirst(false);
|
||||
if (null !== $paramFetcher->get('globalsFirst')) {
|
||||
@trigger_error('API parameter globalsFirst is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
if (!empty($project = $paramFetcher->get('project'))) {
|
||||
@@ -100,7 +103,7 @@ class ActivityController extends BaseApiController
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$data = $this->repository->getActivitiesForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Activity']);
|
||||
|
||||
@@ -173,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']);
|
||||
@@ -222,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)
|
||||
{
|
||||
@@ -235,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);
|
||||
@@ -247,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,10 +75,7 @@ class CustomerController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query
|
||||
->setResultType(CustomerQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
@@ -85,7 +89,7 @@ class CustomerController extends BaseApiController
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$data = $this->repository->getCustomersForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Customer']);
|
||||
|
||||
@@ -151,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']);
|
||||
@@ -200,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)
|
||||
{
|
||||
@@ -213,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);
|
||||
@@ -225,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,10 +77,7 @@ class ProjectController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query
|
||||
->setResultType(ProjectQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
@@ -91,7 +95,7 @@ class ProjectController extends BaseApiController
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$data = $this->repository->getProjectsForQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Project']);
|
||||
|
||||
@@ -157,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']);
|
||||
@@ -206,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)
|
||||
{
|
||||
@@ -219,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);
|
||||
@@ -231,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
|
||||
@@ -108,7 +116,7 @@ class TimesheetController extends BaseApiController
|
||||
* @Rest\QueryParam(name="activity", requirements="\d+", strict=true, nullable=true, description="Activity ID to filter timesheets")
|
||||
* @Rest\QueryParam(name="page", requirements="\d+", strict=true, nullable=true, description="The page to display, renders a 404 if not found (default: 1)")
|
||||
* @Rest\QueryParam(name="size", requirements="\d+", strict=true, nullable=true, description="The amount of entries for each page (default: 50)")
|
||||
* @Rest\QueryParam(name="tags", requirements="[a-zA-Z0-9 -,]+", strict=true, nullable=true, description="The name of tags which are in the datasets")
|
||||
* @Rest\QueryParam(name="tags", requirements="[a-zA-Z0-9 \-,]+", strict=true, nullable=true, description="The name of tags which are in the datasets")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|begin|end|rate", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: DESC)")
|
||||
* @Rest\QueryParam(name="begin", requirements=@Constraints\DateTime(format="Y-m-d\TH:i:s"), strict=true, nullable=true, description="Only records after this date will be included (format: HTML5)")
|
||||
@@ -127,7 +135,6 @@ class TimesheetController extends BaseApiController
|
||||
{
|
||||
$query = new TimesheetQuery();
|
||||
$query->setUser($this->getUser());
|
||||
$query->setResultType(TimesheetQuery::RESULT_TYPE_PAGER);
|
||||
|
||||
if ($this->isGranted('view_other_timesheet') && null !== ($user = $paramFetcher->get('user'))) {
|
||||
if ('all' === $user) {
|
||||
@@ -283,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, [
|
||||
@@ -306,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']);
|
||||
@@ -363,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, [
|
||||
@@ -383,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']);
|
||||
@@ -410,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
|
||||
*/
|
||||
@@ -524,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
|
||||
@@ -569,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
|
||||
@@ -583,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();
|
||||
@@ -594,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())
|
||||
@@ -603,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());
|
||||
@@ -625,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);
|
||||
@@ -651,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)
|
||||
{
|
||||
@@ -673,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 = [
|
||||
@@ -124,7 +129,6 @@ class CreateReleaseCommand extends Command
|
||||
'.codecov.yml',
|
||||
'.editorconfig',
|
||||
'.php_cs.dist',
|
||||
'.scrutinizer.yml',
|
||||
'.travis.yml',
|
||||
'*.lock',
|
||||
'package.json',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -76,16 +76,9 @@ class InstallCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Welcome to the interactive Kimai installer!');
|
||||
$io->title('Kimai installer - v' . Constants::VERSION);
|
||||
|
||||
if (!$input->isInteractive()) {
|
||||
$io->error('Installation only works in interactive mode');
|
||||
|
||||
return self::ERROR_INTERACTIVE;
|
||||
}
|
||||
|
||||
$rows = $this->checkPermissions();
|
||||
$result = $this->confirmAbortToReviewPermissions($io, $input, $output, $rows);
|
||||
$result = $this->reviewPermissions($io, $input, $output);
|
||||
if (true !== $result) {
|
||||
return $result;
|
||||
}
|
||||
@@ -95,7 +88,6 @@ class InstallCommand extends Command
|
||||
// $io->note(sprintf('You have chosen the "%s" environment', $environment));
|
||||
$environment = getenv('APP_ENV');
|
||||
|
||||
// create database if necessary
|
||||
try {
|
||||
$this->createDatabase($io, $input, $output);
|
||||
} catch (\Exception $ex) {
|
||||
@@ -132,8 +124,11 @@ class InstallCommand extends Command
|
||||
|
||||
protected function rebuildCaches(string $environment, SymfonyStyle $io, InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if (!$this->askConfirmation($input, $output, 'Do you want me to rebuild the caches (yes) or skip this step (no)?', true)) {
|
||||
return;
|
||||
if ($input->isInteractive()) {
|
||||
$question = 'Do you want me to rebuild the caches (yes) or skip this step (no)?';
|
||||
if (!$this->askConfirmation($input, $output, $question, true)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$io->text('Rebuilding your cache now, please be patient ...');
|
||||
@@ -196,8 +191,14 @@ class InstallCommand extends Command
|
||||
return $rows;
|
||||
}
|
||||
|
||||
protected function confirmAbortToReviewPermissions(SymfonyStyle $io, InputInterface $input, OutputInterface $output, array $permissions)
|
||||
protected function reviewPermissions(SymfonyStyle $io, InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if (!$input->isInteractive()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$permissions = $this->checkPermissions();
|
||||
|
||||
if (empty($permissions)) {
|
||||
return true;
|
||||
}
|
||||
@@ -217,21 +218,34 @@ class InstallCommand extends Command
|
||||
return self::ERROR_PERMISSIONS;
|
||||
}
|
||||
$io->writeln('');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function importMigrations(SymfonyStyle $io, OutputInterface $output)
|
||||
{
|
||||
if ($this->connection->getSchemaManager()->tablesExist(['migration_versions'])) {
|
||||
$amount = $this->connection->executeQuery('SELECT count(*) as counter FROM migration_versions')->fetchColumn(0);
|
||||
if ($amount > 0) {
|
||||
$io->note(sprintf('Found %s migrations in your database, skipping import', $amount));
|
||||
if (!$this->connection->getSchemaManager()->tablesExist(['migration_versions'])) {
|
||||
$command = $this->getApplication()->find('doctrine:migrations:version');
|
||||
$cmdInput = new ArrayInput(['--add' => true, '--all' => true]);
|
||||
$cmdInput->setInteractive(false);
|
||||
$command->run($cmdInput, $output);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$command = $this->getApplication()->find('doctrine:migrations:version');
|
||||
$cmdInput = new ArrayInput(['--add' => true, '--all' => true]);
|
||||
// this case should not happen, but you know ... everything is possible
|
||||
$amount = $this->connection->executeQuery('SELECT count(*) as counter FROM migration_versions')->fetchColumn(0);
|
||||
if ($amount === 0) {
|
||||
$command = $this->getApplication()->find('doctrine:migrations:version');
|
||||
$cmdInput = new ArrayInput(['--add' => true, '--all' => true]);
|
||||
$cmdInput->setInteractive(false);
|
||||
$command->run($cmdInput, $output);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$command = $this->getApplication()->find('doctrine:migrations:migrate');
|
||||
$cmdInput = new ArrayInput(['--allow-no-migration' => true]);
|
||||
$cmdInput->setInteractive(false);
|
||||
$command->run($cmdInput, $output);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class FormConfiguration implements SystemBundleConfiguration
|
||||
return 'defaults';
|
||||
}
|
||||
|
||||
public function getCustomerDefaultTimezone(): string
|
||||
public function getCustomerDefaultTimezone(): ?string
|
||||
{
|
||||
return $this->find('customer.timezone');
|
||||
}
|
||||
@@ -32,4 +32,24 @@ class FormConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
return $this->find('customer.country');
|
||||
}
|
||||
|
||||
public function getUserDefaultTimezone(): ?string
|
||||
{
|
||||
return $this->find('user.timezone');
|
||||
}
|
||||
|
||||
public function getUserDefaultTheme(): ?string
|
||||
{
|
||||
return $this->find('user.theme');
|
||||
}
|
||||
|
||||
public function getUserDefaultLanguage(): string
|
||||
{
|
||||
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.0';
|
||||
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;
|
||||
@@ -16,6 +17,7 @@ use App\Form\ActivityEditForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityFormTypeQuery;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
@@ -39,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;
|
||||
}
|
||||
|
||||
@@ -67,26 +74,20 @@ class ActivityController extends AbstractController
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query
|
||||
->setOrderBy('name')
|
||||
->setExclusiveVisibility(true)
|
||||
->setPage($page)
|
||||
;
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ActivityQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('activity/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'showFilter' => $query->isDirty(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
@@ -160,16 +161,11 @@ class ActivityController extends AbstractController
|
||||
->add('activity', ActivityType::class, [
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity) {
|
||||
$query = new ActivityQuery();
|
||||
$query
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->setProject($activity->getProject())
|
||||
->setOrderGlobalsFirst(true)
|
||||
->addIgnoredEntity($activity)
|
||||
->setGlobalsOnly(null === $activity->getProject())
|
||||
;
|
||||
$query = new ActivityFormTypeQuery();
|
||||
$query->setProject($activity->getProject());
|
||||
$query->setActivityToIgnore($activity);
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
@@ -208,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);
|
||||
@@ -261,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,12 +13,13 @@ 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;
|
||||
use App\Repository\Query\CustomerFormTypeQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -71,33 +72,23 @@ 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
|
||||
->setOrderBy('name')
|
||||
->setPage($page)
|
||||
;
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var CustomerQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('customer/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'showFilter' => $query->isDirty(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
@@ -105,26 +96,55 @@ 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)
|
||||
{
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $this->configuration->getCustomerDefaultTimezone()) {
|
||||
$timezone = $this->configuration->getCustomerDefaultTimezone();
|
||||
}
|
||||
|
||||
$customer = new Customer();
|
||||
$customer->setCountry($this->configuration->getCustomerDefaultCountry());
|
||||
$customer->setCurrency($this->configuration->getCustomerDefaultCurrency());
|
||||
$customer->setTimezone($this->configuration->getCustomerDefaultTimezone());
|
||||
$customer->setTimezone($timezone);
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -137,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)
|
||||
{
|
||||
@@ -150,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,12 +181,11 @@ class CustomerController extends AbstractController
|
||||
->add('customer', CustomerType::class, [
|
||||
'label' => 'label.customer',
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
$query = new CustomerQuery();
|
||||
$query
|
||||
->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->addIgnoredEntity($customer);
|
||||
$query = new CustomerFormTypeQuery();
|
||||
$query->setCustomerToIgnore($customer);
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
@@ -210,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);
|
||||
|
||||
@@ -233,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', [
|
||||
@@ -247,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
|
||||
{
|
||||
@@ -55,71 +56,49 @@ class ExportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExportQuery
|
||||
* @throws \Exception
|
||||
* @Route(path="/", name="export", methods={"GET"})
|
||||
*/
|
||||
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", "POST"})
|
||||
* @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();
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ExportQuery $query */
|
||||
$query = $form->getData();
|
||||
$showPreview = false;
|
||||
$maxItemsPreview = 500;
|
||||
$entries = [];
|
||||
|
||||
$form = $this->getToolbarForm($query, 'GET');
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isValid()) {
|
||||
/** @var SubmitButton $previewButton */
|
||||
$previewButton = $form->get('preview');
|
||||
if ($previewButton->isClicked()) {
|
||||
$showPreview = true;
|
||||
$query->setPageSize($maxItemsPreview);
|
||||
$entries = $this->getEntries($query);
|
||||
}
|
||||
}
|
||||
|
||||
$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={"GET", "POST"})
|
||||
* @Security("is_granted('create_export')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
* @Route(path="/data", name="export_data", methods={"POST"})
|
||||
*/
|
||||
public function export(Request $request)
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ExportQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
$form = $this->getToolbarForm($query, 'POST');
|
||||
$form->handleRequest($request);
|
||||
|
||||
$type = $query->getType();
|
||||
if (null === $type) {
|
||||
@@ -133,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,15 +149,11 @@ class ExportController extends AbstractController
|
||||
return $this->timesheetRepository->getTimesheetsForQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportQuery $query
|
||||
* @return FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(ExportQuery $query): FormInterface
|
||||
protected function getToolbarForm(ExportQuery $query, string $method): FormInterface
|
||||
{
|
||||
return $this->createForm(ExportToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('export', []),
|
||||
'method' => 'POST',
|
||||
'method' => $method,
|
||||
'attr' => [
|
||||
'id' => 'export-form'
|
||||
]
|
||||
|
||||
@@ -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,22 +13,25 @@ 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;
|
||||
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
|
||||
{
|
||||
@@ -56,24 +59,6 @@ class InvoiceController extends AbstractController
|
||||
$this->dateTimeFactory = $dateTimeFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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", "POST"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
@@ -85,19 +70,37 @@ 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();
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
$form = $this->getToolbarForm($query, 'GET');
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
$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,33 +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={"GET", "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);
|
||||
$form->handleRequest($request);
|
||||
$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());
|
||||
|
||||
if (!$form->isSubmitted() || !$form->isValid()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
protected function renderInvoice(InvoiceQuery $query, TimesheetRepository $repository)
|
||||
{
|
||||
$entries = $this->getEntries($query, $repository);
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
|
||||
@@ -142,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()) {
|
||||
@@ -179,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
|
||||
@@ -212,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);
|
||||
}
|
||||
@@ -246,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');
|
||||
@@ -279,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);
|
||||
@@ -297,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);
|
||||
|
||||
@@ -328,26 +306,18 @@ class InvoiceController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(InvoiceQuery $query)
|
||||
protected function getToolbarForm(InvoiceQuery $query, string $method): FormInterface
|
||||
{
|
||||
return $this->createForm(InvoiceToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('invoice', []),
|
||||
'method' => 'POST',
|
||||
'method' => $method,
|
||||
'attr' => [
|
||||
'id' => 'invoice-print-form'
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,13 +9,16 @@
|
||||
|
||||
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;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
@@ -39,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;
|
||||
}
|
||||
|
||||
@@ -59,46 +67,62 @@ 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
|
||||
->setOrderBy('name')
|
||||
->setExclusiveVisibility(true)
|
||||
->setPage($page)
|
||||
;
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ProjectQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
$entries = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render('project/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'showFilter' => $query->isDirty(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
{
|
||||
@@ -114,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)
|
||||
{
|
||||
@@ -129,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)
|
||||
{
|
||||
@@ -142,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)
|
||||
{
|
||||
@@ -161,13 +174,12 @@ class ProjectController extends AbstractController
|
||||
->add('project', ProjectType::class, [
|
||||
'label' => 'label.project',
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project) {
|
||||
$query = new ProjectQuery();
|
||||
$query
|
||||
->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->setCustomer($project->getCustomer())
|
||||
->addIgnoredEntity($project);
|
||||
$query = new ProjectFormTypeQuery();
|
||||
$query->setCustomer($project->getCustomer());
|
||||
$query->setProjectToIgnore($project);
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
return $repo->getQueryBuilderForFormType($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
@@ -203,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);
|
||||
@@ -233,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', [
|
||||
@@ -247,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();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ use App\Form\Model\Configuration;
|
||||
use App\Form\Model\SystemConfiguration as SystemConfigurationModel;
|
||||
use App\Form\SystemConfigurationForm;
|
||||
use App\Form\Type\EnhancedSelectboxType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\SkinType;
|
||||
use App\Form\Type\TrackingModeType;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
@@ -167,11 +169,14 @@ 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) {
|
||||
$config->setValue($this->configurations->find($config->getName()));
|
||||
$configValue = $this->configurations->find($config->getName());
|
||||
if (null !== $configValue) {
|
||||
$config->setValue($configValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +221,8 @@ class SystemConfigurationController extends AbstractController
|
||||
(new Configuration())
|
||||
->setName('defaults.customer.timezone')
|
||||
->setLabel('timezone')
|
||||
->setType(TimezoneType::class),
|
||||
->setType(TimezoneType::class)
|
||||
->setValue(date_default_timezone_get()),
|
||||
(new Configuration())
|
||||
->setName('defaults.customer.country')
|
||||
->setLabel('country')
|
||||
@@ -226,6 +232,27 @@ class SystemConfigurationController extends AbstractController
|
||||
->setLabel('currency')
|
||||
->setType(CurrencyType::class),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_FORM_USER)
|
||||
->setConfiguration([
|
||||
(new Configuration())
|
||||
->setName('defaults.user.timezone')
|
||||
->setLabel('timezone')
|
||||
->setType(TimezoneType::class)
|
||||
->setValue(date_default_timezone_get()),
|
||||
(new Configuration())
|
||||
->setName('defaults.user.language')
|
||||
->setLabel('language')
|
||||
->setType(LanguageType::class),
|
||||
(new Configuration())
|
||||
->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)
|
||||
->setConfiguration([
|
||||
|
||||
@@ -38,18 +38,15 @@ class TagController extends AbstractController
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var TagQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$tags = $repository->getTagCount($query);
|
||||
|
||||
return $this->render('tags/index.html.twig', [
|
||||
'tags' => $tags,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'showFilter' => $query->isDirty(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -87,10 +87,10 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var TimesheetQuery $query */
|
||||
$query = $form->getData();
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isValid()) {
|
||||
if (null !== $query->getBegin()) {
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
}
|
||||
@@ -99,10 +99,6 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->includeUserInForms()) {
|
||||
$query->setUser($this->getUser());
|
||||
}
|
||||
|
||||
$tags = $query->getTags(true);
|
||||
if (!empty($tags)) {
|
||||
/** @var TagRepository $tagRepo */
|
||||
@@ -114,13 +110,17 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
$dirtyQuery = $query->isDirty();
|
||||
|
||||
$this->prepareQuery($query);
|
||||
|
||||
$pager = $this->getRepository()->getPagerfantaForQuery($query);
|
||||
|
||||
return $this->render($renderTemplate, [
|
||||
'entries' => $pager,
|
||||
'page' => $query->getPage(),
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'showFilter' => $dirtyQuery,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'showSummary' => $this->includeSummary(),
|
||||
'showStartEndTime' => $this->canSeeStartEndTime()
|
||||
@@ -136,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);
|
||||
@@ -165,7 +165,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
* @param ActivityRepository $activityRepository
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function create(Request $request, string $renderTemplate, ProjectRepository $projectRepository, ActivityRepository $activityRepository)
|
||||
protected function create(Request $request, string $renderTemplate, ProjectRepository $projectRepository, ActivityRepository $activityRepository, TagRepository $tagRepository)
|
||||
{
|
||||
$entry = new Timesheet();
|
||||
$entry->setUser($this->getUser());
|
||||
@@ -181,8 +181,20 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$entry->setActivity($activity);
|
||||
}
|
||||
|
||||
if ($request->query->get('tags')) {
|
||||
$tagNames = explode(',', $request->query->get('tags'));
|
||||
foreach ($tagNames as $tagName) {
|
||||
$tag = $tagRepository->findOneByName($tagName);
|
||||
if (!$tag) {
|
||||
$tag = new Tag();
|
||||
$tag->setName($tagName);
|
||||
}
|
||||
$entry->addTag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
$event = new TimesheetMetaDefinitionEvent($entry);
|
||||
$this->dispatcher->dispatch(TimesheetMetaDefinitionEvent::class, $event);
|
||||
$this->dispatcher->dispatch($event, TimesheetMetaDefinitionEvent::class);
|
||||
|
||||
$mode = $this->getTrackingMode();
|
||||
$mode->create($entry, $request);
|
||||
@@ -223,11 +235,8 @@ abstract class TimesheetAbstractController extends AbstractController
|
||||
$query = new TimesheetQuery();
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var TimesheetQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
// by default the current month is exported, but it can be overwritten
|
||||
// this should not be removed, otherwise we would export EVERY available record in the admin section
|
||||
@@ -242,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);
|
||||
|
||||
@@ -254,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, [
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Controller;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TagRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -70,9 +71,9 @@ class TimesheetController extends TimesheetAbstractController
|
||||
* @param ActivityRepository $activityRepository
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request, ProjectRepository $projectRepository, ActivityRepository $activityRepository)
|
||||
public function createAction(Request $request, ProjectRepository $projectRepository, ActivityRepository $activityRepository, TagRepository $tagRepository)
|
||||
{
|
||||
return $this->create($request, 'timesheet/edit.html.twig', $projectRepository, $activityRepository);
|
||||
return $this->create($request, 'timesheet/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,8 @@ 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;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -70,9 +72,14 @@ class TimesheetTeamController extends TimesheetAbstractController
|
||||
* @param ActivityRepository $activityRepository
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request, ProjectRepository $projectRepository, ActivityRepository $activityRepository)
|
||||
public function createAction(Request $request, ProjectRepository $projectRepository, ActivityRepository $activityRepository, TagRepository $tagRepository)
|
||||
{
|
||||
return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository);
|
||||
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
|
||||
|
||||
@@ -69,11 +69,8 @@ class UserController extends AbstractController
|
||||
$query->setOrderBy('username');
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var UserQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
@@ -81,7 +78,7 @@ class UserController extends AbstractController
|
||||
return $this->render('user/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'showFilter' => $query->isDirty(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -70,9 +70,17 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
$engine = $this->findEngine();
|
||||
|
||||
$configDir = realpath(
|
||||
$container->getParameter('kernel.project_dir') . '/vendor/beberlei/DoctrineExtensions/config/'
|
||||
$container->getParameter('kernel.project_dir') . '/vendor/beberlei/doctrineextensions/config/'
|
||||
);
|
||||
|
||||
if (!file_exists($configDir)) {
|
||||
@trigger_error('Using deprecated doctrine extensions config directory', E_USER_DEPRECATED);
|
||||
|
||||
$configDir = realpath(
|
||||
$container->getParameter('kernel.project_dir') . '/vendor/beberlei/DoctrineExtensions/config/'
|
||||
);
|
||||
}
|
||||
|
||||
$configFile = $configDir . '/' . $engine . '.yml';
|
||||
|
||||
if (!file_exists($configFile)) {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use App\Timesheet\Rounding\RoundingInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
@@ -249,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()
|
||||
@@ -265,7 +268,7 @@ class Configuration implements ConfigurationInterface
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('begin')->defaultValue('00:00')->end()
|
||||
->scalarNode('end')->defaultValue('24:00')->end()
|
||||
->scalarNode('end')->defaultValue('23:59')->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('google')
|
||||
@@ -435,9 +438,18 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayNode('customer')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('timezone')->defaultValue('Europe/Berlin')->end()
|
||||
->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')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->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;
|
||||
|
||||
@@ -15,8 +15,18 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_activities")
|
||||
* @ORM\Table(name="kimai2_activities",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"visible","project_id"}),
|
||||
* @ORM\Index(columns={"visible","project_id","name"}),
|
||||
* @ORM\Index(columns={"visible","name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
|
||||
*
|
||||
* columns={"visible","name"} => IDX_8811FE1C7AB0E8595E237E06 => activity administration without filter
|
||||
* columns={"visible","project_id"} => IDX_8811FE1C7AB0E859166D1F9C => activity administration with customer or project filter
|
||||
* columns={"visible","project_id","name"} => IDX_8811FE1C7AB0E859166D1F9C5E237E06 => activity drop-down for global activities in toolbar or globalsOnly filter in activity administration
|
||||
*/
|
||||
class Activity implements EntityWithMetaFields
|
||||
{
|
||||
@@ -32,7 +42,7 @@ class Activity implements EntityWithMetaFields
|
||||
/**
|
||||
* @var Project|null
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="activities")
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
*/
|
||||
private $project;
|
||||
@@ -40,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;
|
||||
|
||||
@@ -61,13 +73,6 @@ class Activity implements EntityWithMetaFields
|
||||
*/
|
||||
private $visible = true;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Timesheet", mappedBy="activity")
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
@@ -82,7 +87,6 @@ class Activity implements EntityWithMetaFields
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->timesheets = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
}
|
||||
|
||||
@@ -91,14 +95,6 @@ class Activity implements EntityWithMetaFields
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Timesheet>
|
||||
*/
|
||||
public function getTimesheets(): Collection
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
|
||||
@@ -30,7 +30,7 @@ class ActivityMeta implements MetaTableTypeInterface
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
|
||||
@@ -27,7 +27,7 @@ trait BudgetTrait
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="time_budget", type="integer", precision=10, scale=2, nullable=false)
|
||||
* @ORM\Column(name="time_budget", type="integer", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $timeBudget = 0;
|
||||
|
||||
@@ -15,8 +15,14 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_customers")
|
||||
* @ORM\Table(name="kimai2_customers",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"visible"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
||||
*
|
||||
* columns={"visible"} => IDX_5A9760447AB0E859 => used in customer dropdown
|
||||
*/
|
||||
class Customer implements EntityWithMetaFields
|
||||
{
|
||||
@@ -34,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;
|
||||
|
||||
@@ -44,23 +52,17 @@ 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;
|
||||
|
||||
/**
|
||||
* @var Project[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Project", mappedBy="customer")
|
||||
*/
|
||||
private $projects;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
@@ -73,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;
|
||||
|
||||
@@ -80,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;
|
||||
|
||||
@@ -95,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;
|
||||
|
||||
@@ -103,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;
|
||||
|
||||
@@ -110,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;
|
||||
|
||||
@@ -117,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;
|
||||
|
||||
@@ -124,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;
|
||||
|
||||
@@ -138,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;
|
||||
|
||||
@@ -161,10 +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->projects = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -352,14 +384,6 @@ class Customer implements EntityWithMetaFields
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Project>
|
||||
*/
|
||||
public function getProjects(): Collection
|
||||
{
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only here for symfony forms
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
@@ -409,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
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,7 @@ class CustomerMeta implements MetaTableTypeInterface
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ trait MetaTableTypeTrait
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false)
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $visible = false;
|
||||
|
||||
@@ -15,8 +15,16 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_projects")
|
||||
* @ORM\Table(name="kimai2_projects",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"customer_id","visible","name"}),
|
||||
* @ORM\Index(columns={"customer_id","visible","id"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
|
||||
*
|
||||
* columns={"customer_id","visible","name"} => IDX_407F12069395C3F37AB0E8595E237E06 => project administration without filter
|
||||
* columns={"customer_id","visible","id"} => IDX_407F12069395C3F37AB0E859BF396750 => used in joins between project and customer, eg. dropdowns and activity administration page
|
||||
*/
|
||||
class Project implements EntityWithMetaFields
|
||||
{
|
||||
@@ -32,7 +40,7 @@ class Project implements EntityWithMetaFields
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="projects")
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
@@ -41,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;
|
||||
|
||||
@@ -58,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;
|
||||
|
||||
@@ -70,25 +80,11 @@ class Project implements EntityWithMetaFields
|
||||
*/
|
||||
private $visible = true;
|
||||
|
||||
/**
|
||||
* @var Activity[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Activity", mappedBy="project")
|
||||
*/
|
||||
private $activities;
|
||||
|
||||
// keep the trait include exactly here, for placing the column at the correct position
|
||||
use RatesTrait;
|
||||
use ColorTrait;
|
||||
use BudgetTrait;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Timesheet", mappedBy="project")
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
/**
|
||||
* @var ProjectMeta[]|Collection
|
||||
*
|
||||
@@ -96,11 +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->activities = new ArrayCollection();
|
||||
$this->timesheets = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -163,22 +174,6 @@ class Project implements EntityWithMetaFields
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Timesheet>
|
||||
*/
|
||||
public function getTimesheets(): Collection
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<Activity>
|
||||
*/
|
||||
public function getActivities(): Collection
|
||||
{
|
||||
return $this->activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
@@ -247,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
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,7 @@ class ProjectMeta implements MetaTableTypeInterface
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
|
||||
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;
|
||||
@@ -18,12 +20,23 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\Table(name="kimai2_timesheet",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"user"}),
|
||||
* @ORM\Index(columns={"activity_id"})
|
||||
* @ORM\Index(columns={"activity_id"}),
|
||||
* @ORM\Index(columns={"user","start_time"}),
|
||||
* @ORM\Index(columns={"start_time"}),
|
||||
* @ORM\Index(columns={"start_time","end_time"}),
|
||||
* @ORM\Index(columns={"start_time","end_time","user"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository")
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @App\Validator\Constraints\Timesheet
|
||||
*
|
||||
* columns={"user"} => IDX_4F60C6B18D93D649 => count results for user timesheets
|
||||
* columns={"activity_id"} => IDX_4F60C6B181C06096 => ???
|
||||
* columns={"user","start_time"} => IDX_4F60C6B18D93D649502DF587 => recent activities, user timesheet with date filzer
|
||||
* columns={"start_time"} => IDX_4F60C6B1502DF587 => team timesheets with timerange filter only
|
||||
* columns={"start_time","end_time"} => IDX_4F60C6B1502DF58741561401 => ???
|
||||
* columns={"start_time","end_time","user"} => IDX_4F60C6B1502DF587415614018D93D649 => ???
|
||||
*/
|
||||
class Timesheet implements EntityWithMetaFields
|
||||
{
|
||||
@@ -37,7 +50,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var DateTime
|
||||
*
|
||||
* @ORM\Column(name="start_time", type="datetime", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
@@ -45,7 +58,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
private $begin;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @var DateTime
|
||||
*
|
||||
* @ORM\Column(name="end_time", type="datetime", nullable=true)
|
||||
*/
|
||||
@@ -83,7 +96,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
/**
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity", inversedBy="timesheets")
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
@@ -92,7 +105,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="timesheets")
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
@@ -101,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;
|
||||
|
||||
@@ -131,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")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
@@ -177,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();
|
||||
|
||||
@@ -195,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();
|
||||
@@ -206,7 +219,7 @@ class Timesheet implements EntityWithMetaFields
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnd(): ?\DateTime
|
||||
public function getEnd(): ?DateTime
|
||||
{
|
||||
$this->localizeDates();
|
||||
|
||||
@@ -214,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;
|
||||
|
||||
@@ -411,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
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,7 @@ class TimesheetMeta implements MetaTableTypeInterface
|
||||
* @var Timesheet
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Timesheet", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $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
|
||||
{
|
||||
@@ -23,7 +23,7 @@ final class ThemeEvent extends Event
|
||||
public const CONTENT_AFTER = 'app.theme.content_after';
|
||||
|
||||
/**
|
||||
* @var User
|
||||
* @var User|null
|
||||
*/
|
||||
protected $user;
|
||||
/**
|
||||
@@ -35,33 +35,28 @@ final class ThemeEvent extends Event
|
||||
*/
|
||||
protected $payload = null;
|
||||
|
||||
public function __construct(User $user, $payload = null)
|
||||
public function __construct(?User $user = null, $payload = null)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
* @deprecated since 1.0
|
||||
*/
|
||||
public function getUser()
|
||||
public function getUser(): ?User
|
||||
{
|
||||
@trigger_error('Using ThemeEvent::getUser() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @return ThemeEvent
|
||||
*/
|
||||
public function addContent(string $content)
|
||||
public function addContent(string $content): ThemeEvent
|
||||
{
|
||||
$this->content .= $content;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user