Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67dab35b57 | ||
|
|
55d8bc9ba5 | ||
|
|
572f01ed74 | ||
|
|
ee32d6f926 | ||
|
|
89abaa9dda | ||
|
|
a9ece209ae | ||
|
|
47ac50b4d0 | ||
|
|
5539f68a54 | ||
|
|
5b1ec1eb8f | ||
|
|
a609cbf84a | ||
|
|
1ad440f488 | ||
|
|
64330e6cc4 | ||
|
|
03421c099a | ||
|
|
b5c4084452 | ||
|
|
5d5b971f57 | ||
|
|
825d31af20 | ||
|
|
7880f980a8 | ||
|
|
137fd69f47 | ||
|
|
ed8efcf139 | ||
|
|
7fa3b67cc4 | ||
|
|
93c500fbf2 | ||
|
|
c58c12ea3d | ||
|
|
35305ad107 | ||
|
|
589d750e59 | ||
|
|
43b488f21f | ||
|
|
54d06462fc | ||
|
|
2d6109d587 | ||
|
|
8fddf627bf | ||
|
|
0334f6ce86 | ||
|
|
025558e58e | ||
|
|
f74547008d | ||
|
|
67dc1c7640 | ||
|
|
40322c6de4 |
2
.github/lock.yml
vendored
2
.github/lock.yml
vendored
@@ -14,6 +14,8 @@ lockComment: >
|
||||
This thread has been automatically locked since there has not been
|
||||
any recent activity after it was closed. Please open a new issue for
|
||||
related bugs.
|
||||
If you use Kimai on a daily basis, please [consider donating](https://www.kimai.org/donate/) to
|
||||
support further development of Kimai.
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: false
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
unreleased=true
|
||||
future-release=0.6
|
||||
exclude-labels=duplicate,question,invalid,wontfix,release
|
||||
future-release=0.7
|
||||
exclude-labels=duplicate,question,invalid,wontfix,release
|
||||
enhancement_labels=>enhancement,Enhancement,feature request
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
|
||||
# ========== KIMAI ==========
|
||||
# IDE
|
||||
.idea/
|
||||
@@ -10,6 +9,7 @@
|
||||
templates/invoice/renderer/.~lock*
|
||||
|
||||
/var/data/*.sqlite
|
||||
/var/data/*.sqlite-journal
|
||||
!/var/data/kimai_test.sqlite
|
||||
/var/coverage/
|
||||
/var/cache/*
|
||||
@@ -43,12 +43,13 @@ templates/invoice/renderer/.~lock*
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> symfony/webpack-encore-pack ###
|
||||
/node_modules/
|
||||
###< symfony/webpack-encore-pack ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
||||
###> symfony/webpack-encore-pack ###
|
||||
/node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
###< symfony/webpack-encore-pack ###
|
||||
|
||||
213
CHANGELOG.md
213
CHANGELOG.md
@@ -1,8 +1,118 @@
|
||||
# Change Log
|
||||
|
||||
## [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:**
|
||||
|
||||
- This value should be greater than or equal to zero [\#511](https://github.com/kevinpapst/kimai2/issues/511)
|
||||
- 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))
|
||||
|
||||
## [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))
|
||||
|
||||
## [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)
|
||||
@@ -13,31 +123,16 @@
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Language setting does not work immediately [\#418](https://github.com/kevinpapst/kimai2/issues/418)
|
||||
- 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)
|
||||
- Merge projects and/or activities [\#406](https://github.com/kevinpapst/kimai2/issues/406)
|
||||
- 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)
|
||||
- Global activities support for Kimai 1 importer [\#400](https://github.com/kevinpapst/kimai2/issues/400)
|
||||
- Unrecognized Options "renderer, number\_generator" under "kimai.invoice" [\#381](https://github.com/kevinpapst/kimai2/issues/381)
|
||||
- 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)
|
||||
- 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)
|
||||
- Activities just Duration [\#334](https://github.com/kevinpapst/kimai2/issues/334)
|
||||
- API: Add filtering/sorting/limits [\#333](https://github.com/kevinpapst/kimai2/issues/333)
|
||||
- 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)
|
||||
- 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)
|
||||
- docker image for kimai2 [\#284](https://github.com/kevinpapst/kimai2/issues/284)
|
||||
- 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)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
@@ -77,16 +172,10 @@
|
||||
## [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)
|
||||
|
||||
**Fixed bugs:**
|
||||
**Implemented enhancements:**
|
||||
|
||||
- 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)
|
||||
- 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 permission voter for invoices and templates [\#293](https://github.com/kevinpapst/kimai2/issues/293)
|
||||
- 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)
|
||||
@@ -97,6 +186,15 @@
|
||||
- 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))
|
||||
@@ -118,6 +216,10 @@
|
||||
## [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)
|
||||
@@ -131,7 +233,6 @@
|
||||
- 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)
|
||||
- Delete user [\#225](https://github.com/kevinpapst/kimai2/issues/225)
|
||||
- Feature request - Monthly overview / report [\#207](https://github.com/kevinpapst/kimai2/issues/207)
|
||||
|
||||
**Merged pull requests:**
|
||||
@@ -162,6 +263,16 @@
|
||||
## [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)
|
||||
@@ -175,16 +286,10 @@
|
||||
- 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)
|
||||
- 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)
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
@@ -203,42 +308,24 @@
|
||||
- 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))
|
||||
- added dynamic column filter \#174 [\#184](https://github.com/kevinpapst/kimai2/pull/184) ([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))
|
||||
|
||||
## [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)
|
||||
|
||||
**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:**
|
||||
**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)
|
||||
- Add CHANGELOG.md [\#177](https://github.com/kevinpapst/kimai2/issues/177)
|
||||
- Hide notifications after some seconds [\#169](https://github.com/kevinpapst/kimai2/issues/169)
|
||||
- Add preview blog message and info text on old website [\#162](https://github.com/kevinpapst/kimai2/issues/162)
|
||||
- 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)
|
||||
- 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)
|
||||
- Add help text to form elements [\#138](https://github.com/kevinpapst/kimai2/issues/138)
|
||||
- Duration mode: creating new timesheet records prefills duration with some seconds [\#137](https://github.com/kevinpapst/kimai2/issues/137)
|
||||
- Add duration-only mode [\#131](https://github.com/kevinpapst/kimai2/issues/131)
|
||||
- Add help controller [\#129](https://github.com/kevinpapst/kimai2/issues/129)
|
||||
- \[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)
|
||||
- Secure create-user command [\#123](https://github.com/kevinpapst/kimai2/issues/123)
|
||||
- Setup travis to run phpunit tests [\#120](https://github.com/kevinpapst/kimai2/issues/120)
|
||||
- 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)
|
||||
@@ -248,7 +335,6 @@
|
||||
- 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)
|
||||
- Add russian translation [\#95](https://github.com/kevinpapst/kimai2/issues/95)
|
||||
- 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)
|
||||
@@ -257,6 +343,31 @@
|
||||
- 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:**
|
||||
|
||||
@@ -272,8 +383,6 @@
|
||||
- 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))
|
||||
- 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))
|
||||
- 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))
|
||||
@@ -305,7 +414,7 @@
|
||||
- upgraded to symfony 4 \#74 [\#81](https://github.com/kevinpapst/kimai2/pull/81) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.1](https://github.com/kevinpapst/kimai2/tree/0.1) (2018-01-10)
|
||||
**Closed issues:**
|
||||
**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)
|
||||
|
||||
34
Dockerfile
34
Dockerfile
@@ -5,10 +5,7 @@
|
||||
|
||||
FROM php:7.2.9-apache-stretch
|
||||
|
||||
WORKDIR /opt/kimai
|
||||
|
||||
RUN php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer && \
|
||||
apt update && \
|
||||
RUN apt update && \
|
||||
apt install -y \
|
||||
git \
|
||||
libicu-dev \
|
||||
@@ -17,24 +14,35 @@ RUN php -r "readfile('http://getcomposer.org/installer');" | php -- --install-di
|
||||
libldb-dev \
|
||||
libpng-dev \
|
||||
unzip \
|
||||
wget \
|
||||
zip \
|
||||
&& \
|
||||
EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)" && \
|
||||
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
|
||||
ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" && \
|
||||
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]; then >&2 echo 'ERROR: Invalid installer signature'; rm composer-setup.php; exit 1; fi && \
|
||||
php composer-setup.php --quiet && \
|
||||
rm composer-setup.php && \
|
||||
mv /var/www/html/composer.phar /usr/bin/composer && \
|
||||
docker-php-ext-install \
|
||||
gd \
|
||||
intl \
|
||||
ldap \
|
||||
pdo_mysql \
|
||||
zip && \
|
||||
apt-get -y autoremove && \
|
||||
apt-get clean && \
|
||||
apt remove -y wget && \
|
||||
apt -y autoremove && \
|
||||
apt clean && \
|
||||
git clone https://github.com/kevinpapst/kimai2.git /opt/kimai && \
|
||||
sed "s/prod/dev/g" .env.dist > .env && \
|
||||
composer install --dev --optimize-autoloader && \
|
||||
bin/console doctrine:database:create && \
|
||||
bin/console doctrine:schema:create && \
|
||||
bin/console doctrine:migrations:version --add --all && \
|
||||
bin/console cache:warmup && \
|
||||
chown -R www-data:www-data var
|
||||
sed "s/prod/dev/g" /opt/kimai/.env.dist > /opt/kimai/.env && \
|
||||
composer install --working-dir=/opt/kimai --dev --optimize-autoloader && \
|
||||
/opt/kimai/bin/console doctrine:database:create && \
|
||||
/opt/kimai/bin/console doctrine:schema:create && \
|
||||
/opt/kimai/bin/console doctrine:migrations:version --add --all && \
|
||||
/opt/kimai/bin/console cache:warmup && \
|
||||
chown -R www-data:www-data /opt/kimai/var
|
||||
|
||||
WORKDIR /opt/kimai
|
||||
|
||||
EXPOSE 8001
|
||||
USER www-data
|
||||
|
||||
171
README.md
171
README.md
@@ -1,6 +1,6 @@
|
||||
# Kimai v2 - Time Tracking
|
||||
# Kimai 2 - Time-tracking made easy
|
||||
|
||||
Kimai v2 - the open source time-tracking application with a mobile-first approach, read more at the [official website](http://v2.kimai.org).
|
||||
Kimai - the open source time-tracker application with a mobile-first approach (read more at the [official website](http://v2.kimai.org)).
|
||||
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
@@ -11,156 +11,58 @@ Kimai v2 - the open source time-tracking application with a mobile-first approac
|
||||
|
||||
## Introduction
|
||||
|
||||
This is the reloaded version of the open source timetracker Kimai.
|
||||
The new version has not much in common with its predecessor [Kimai v1](http://www.kimai.org) besides the basic ideas of time-tracking and the current development team.
|
||||
This is the _reloaded_ version of the open source timetracker Kimai.
|
||||
It is built from scratch and doesn't share any source code with its [predecessor](http://www.kimai.org).
|
||||
But it adapts the same ideas and a clean & simple UI for your time-tracking experience.
|
||||
|
||||
Right now its in an early development phase, its usable but some advanced features from Kimai v1 are missing by now (like export and ODT invoices).
|
||||
But we already support to [import your timesheets](var/docs/migration_v1.md) from Kimai v1.
|
||||
By now it is in an pre-stable development phase, usable and with most advanced features from Kimai 1.
|
||||
You can even [import your data](var/docs/migration_v1.md) and start testing and using it today.
|
||||
|
||||
It is developed with modern frameworks like [Symfony v4](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/),
|
||||
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json).
|
||||
|
||||
Kimai is a multi-language application and already translated to: English, German, Italian, French, Spanish, Russian, Arabic and Hungarian.
|
||||
If you want to support us in translating Kimai, please [read this documentation](var/docs/translations.md).
|
||||
Kimai is a [multi-language application](var/docs/translations.md) and already translated to english, german, italian, french, spanish, russian, arabic, hungarian and portuguese.
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 7.1.3 or higher
|
||||
- The PHP extensions:
|
||||
- [PDO](https://php.net/manual/en/book.pdo.php) (with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled)
|
||||
- [intl](https://php.net/manual/en/book.intl.php)
|
||||
- [zip](https://php.net/manual/en/book.zip.php)
|
||||
- The [usual Symfony application requirements](http://symfony.com/doc/current/reference/requirements.html)
|
||||
- PHP 7.1.3 or higher (test your system compatibility with the [requirements-checker](http://symfony.com/doc/current/reference/requirements.html))
|
||||
- The PHP extensions [intl](https://php.net/manual/en/book.intl.php), [zip](https://php.net/manual/en/book.zip.php) and [PDO](https://php.net/manual/en/book.pdo.php) with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled
|
||||
- If you use MariaDB, make sure its at least v10.2.7 (see [FAQ](var/docs/faq.md))
|
||||
- Kimai needs to be installed in the root directory of a domain or you need to [recompile the frontend assets](var/docs/developers.md)
|
||||
- A modern browser, Kimai v2 might be broken on old browsers like IE 10
|
||||
|
||||
## Documentation & Roadmap
|
||||
## Documentation
|
||||
|
||||
Looking for more information about using Kimai? Check out our more detailed [documentation](var/docs/).
|
||||
Looking for more information about Kimai 2? Check out our detailed [documentation](var/docs/).
|
||||
|
||||
You can see our development roadmap for the future in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
||||
### Installation
|
||||
|
||||
Our roadmap is open for changes and input from the community, please [send us](https://github.com/kevinpapst/kimai2/issues) your ideas and questions.
|
||||
There are multiple ways to install Kimai, all of them described in the [installation docu](var/docs/installation.md):
|
||||
|
||||
## Installation
|
||||
- [Recommended installation with GIT and Composer](var/docs/installation.md#recommended-setup)
|
||||
- [Development setup](var/docs/installation.md#development-installation)
|
||||
- [Docker](var/docs/docker.md)
|
||||
- [1-click installations](var/docs/installation.md#hosting-and-1-click-installations)
|
||||
- [FTP](var/docs/installation.md#ftp-installation)
|
||||
|
||||
> **NOTE**
|
||||
>
|
||||
> There are [further infos about installation](var/docs/installation.md) if you have to use FTP, want to develop with Kimai
|
||||
or are setting up Kimai on your personal computer. There is also a [docker image](var/docs/docker.md) available.
|
||||
### Updating Kimai
|
||||
|
||||
If you want to install Kimai 2 in your production environment, then SSH into your server and change to your webserver root.
|
||||
You need to install Git and [Composer](https://getcomposer.org/doc/00-intro.md) if you haven't already.
|
||||
Read the following documentations before you start your upgrade:
|
||||
|
||||
First clone this repo:
|
||||
- The [update documentation](var/docs/updates.md)
|
||||
- The version specific [UPGRADING guide](UPGRADING.md)
|
||||
- The [release information](https://github.com/kevinpapst/kimai2/releases)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kevinpapst/kimai2.git
|
||||
cd kimai2/
|
||||
```
|
||||
## Roadmap and releases
|
||||
|
||||
Make sure the [file permissions are correct](https://symfony.com/doc/current/setup/file_permissions.html) and create your `.env` file:
|
||||
```bash
|
||||
chown -R :www-data .
|
||||
chmod -R g+r .
|
||||
chmod -R g+rw var/
|
||||
cp .env.dist .env
|
||||
```
|
||||
You can see our development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome!
|
||||
|
||||
It's up to you which database server you want to use, Kimai v2 supports MySQL/MariaDB and SQLite, but SQLite is [not recommended](var/docs/faq.md) for production usage.
|
||||
Configure the database connection string in your the `.env` file:
|
||||
```
|
||||
# adjust all settings in .env to your needs
|
||||
APP_ENV=prod
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database
|
||||
```
|
||||
> Kimai 2 uses a rolling release concept for delivering updates.
|
||||
> You can upgrade Kimai at any time, you don't need to wait for the next official release.
|
||||
|
||||
Now install all dependencies for Kimai 2:
|
||||
|
||||
```bash
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
|
||||
Optionally create the database:
|
||||
```bash
|
||||
bin/console doctrine:database:create
|
||||
```
|
||||
|
||||
Create all schema tables:
|
||||
```bash
|
||||
bin/console doctrine:schema:create
|
||||
```
|
||||
|
||||
Make sure that upcoming updates can be correctly applied by setting the initial database version:
|
||||
```bash
|
||||
bin/console doctrine:migrations:version --add --all
|
||||
```
|
||||
|
||||
Warm up the cache (as webserver user):
|
||||
```bash
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
```
|
||||
|
||||
Create your first user with the following command. You will be asked to enter a password afterwards:
|
||||
```bash
|
||||
bin/console kimai:create-user username admin@example.com ROLE_SUPER_ADMIN
|
||||
```
|
||||
_Tip: You can skip the "create user" step, if you are going to [import data from Kimai v1](var/docs/migration_v1.md)._
|
||||
|
||||
For available roles, please refer to the [user documentation](var/docs/users.md).
|
||||
|
||||
> **NOTE**
|
||||
>
|
||||
> If you want to use a fully-featured web server (like Nginx or Apache) to run
|
||||
> Kimai, configure it to point at the `public/` directory of the project.
|
||||
> For more details, see:
|
||||
> http://symfony.com/doc/current/cookbook/configuration/web_server_configuration.html
|
||||
|
||||
Installation complete: enjoy time-tracking :-)
|
||||
|
||||
## Updating Kimai
|
||||
|
||||
> **NOTE**
|
||||
>
|
||||
> Don't forget tweaks that may be necessary to these instructions if you are using FTP, developing or updating on your
|
||||
personal computer instead of a server. See [further infos about installation](var/docs/installation.md).
|
||||
|
||||
**STOP**
|
||||
|
||||
1. It's important that you don't execute the Installation steps before or after your upgrade
|
||||
2. Make sure that you have a working database backup before you start the update
|
||||
3. Read the [UPGRADING](UPGRADING.md) guide and [release information](https://github.com/kevinpapst/kimai2/releases) to check if there a further steps required
|
||||
|
||||
Get the latest code and install dependencies:
|
||||
```bash
|
||||
cd kimai2/
|
||||
git pull origin master
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
|
||||
Refresh your cache:
|
||||
```bash
|
||||
sudo -u www-data bin/console cache:clear --env=prod
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
```
|
||||
|
||||
And upgrade your database:
|
||||
```bash
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
Done! You can use the latest version of Kimai 2.
|
||||
|
||||
## Rolling releases & Git
|
||||
|
||||
Please note: Kimai 2 uses a rolling release concept for delivering updates.
|
||||
Release versions will be created on a regular base and you can use these tags if you are familiar with Git, but we
|
||||
will not provide support for any specific version (whether its bugs or installation/update docu).
|
||||
|
||||
Every code change, whether it's a new features or bug fixes, will be targeted against the master branch and
|
||||
intensively tested before merging. We have to go this way, as we develop Kimai in our free time and want to put our
|
||||
effort into the software instead of installation scripts and complicated upgrade processes.
|
||||
Release versions will be created on a regular base and you can use these tags if you are familiar with Git,
|
||||
but we will not provide support for any specific version.
|
||||
Every code change, whether it's a new feature or a bug fix, will be done on the master branch and
|
||||
intensively tested before merging. We have to do it this way, as we develop Kimai in our free time and want to put our
|
||||
effort into the software instead of backporting changes for old versions.
|
||||
|
||||
## Extensions for Kimai 2
|
||||
|
||||
@@ -171,7 +73,10 @@ All available Kimai 2 bundles can be found at the [Kimai recipes](https://github
|
||||
|
||||
## Developer
|
||||
|
||||
If you want to develop with and for Kimai 2 please read the following documentation:
|
||||
Kimai 2 is developed with modern frameworks like [Symfony v4](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/),
|
||||
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json).
|
||||
|
||||
If you want to start developing for Kimai 2, please read the following documentation:
|
||||
|
||||
- an example on how to extend Kimai 2 can be found in this [GitHub repository](https://github.com/kevinpapst/kimai2-invoice)
|
||||
- the [developer documentation](var/docs/developers.md) is available both on GitHub and your local installation
|
||||
|
||||
@@ -18,6 +18,15 @@ bin/console doctrine:migrations:migrate
|
||||
There might be version specific tasks that need to be executed before or after these steps, please see below
|
||||
if your updated version is mentioned below.
|
||||
|
||||
## [0.7](https://github.com/kevinpapst/kimai2/releases/tag/0.7) (2019-01-28)
|
||||
|
||||
The configuration `kimai.theme.active_warning` was deprecated and should be replaced in your local.yaml,
|
||||
[read config docs for more information](https://github.com/kevinpapst/kimai2/blob/master/var/docs/configurations.md#limit-active-entries).
|
||||
|
||||
## [0.6.1](https://github.com/kevinpapst/kimai2/releases/tag/0.6.1) (2018-11-19)
|
||||
|
||||
A bugfix release to address database compatibility issues with older MySQL/MariaDB versions.
|
||||
|
||||
## [0.6](https://github.com/kevinpapst/kimai2/releases/tag/0.6) (2018-11-18)
|
||||
|
||||
The API has some minor BC breaks: some fields were renamed and entities have a larger attribute set than collections.
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
|
||||
|
||||
.error-page {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* ================================ CONTENT ================================ */
|
||||
|
||||
.content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================ NAVBAR ================================ */
|
||||
|
||||
.navbar-nav > li > a.ddt-large {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
li.messages-menu ul.menu li .pull-left i {
|
||||
color: #dd4b39;
|
||||
}
|
||||
|
||||
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.navbar-nav .start_record {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
.ticktac:hover i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
li.open .ticktac i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
.ticktac i.stopped{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ticktac:hover i.stopped{
|
||||
color: #4ff131;
|
||||
}
|
||||
.user-panel>.info {
|
||||
left: 40px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* ================================ TOOLBAR ================================ */
|
||||
|
||||
/* Page based action buttons in the upper right corner of the content area */
|
||||
.content-header > .breadcrumb {
|
||||
position: absolute;
|
||||
float: right;
|
||||
background: transparent;
|
||||
}
|
||||
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
|
||||
.control-sidebar {
|
||||
select {
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
div.image {
|
||||
img.img-circle {
|
||||
max-width: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The filter form is available on most pages above the datatable */
|
||||
@media (min-width: 768px) {
|
||||
.toolbar {
|
||||
form.navbar-form {
|
||||
font-size: $font-size-base;
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ================================ SIDEBAR ================================ */
|
||||
|
||||
/* Used for the Sidebar UserPanel (which is deactivated right now) */
|
||||
.user-panel>.info {
|
||||
left: 80px;
|
||||
}
|
||||
|
||||
/* ================================ FOOTER ================================ */
|
||||
|
||||
footer.main-footer {
|
||||
padding: 5px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* ================================ PRINT ================================ */
|
||||
|
||||
@media print{
|
||||
.sf-toolbar, .control-sidebar {display: none !important;}
|
||||
}
|
||||
|
||||
/* ================================ INVOICES ================================ */
|
||||
|
||||
.invoice {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.invoice {
|
||||
margin: 10px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-small{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div.invoice-address {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
table.invoice-records {}
|
||||
table.invoice-meta th {
|
||||
padding-right: 40px;
|
||||
}
|
||||
table.invoice-sum th {
|
||||
width: 70%;
|
||||
}
|
||||
table.invoice-sum th, table.invoice-sum td {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ $(function() {
|
||||
moment.locale($.kimai.settings['locale']);
|
||||
|
||||
// ask before a delete call is executed
|
||||
$('a.btn-trash').click(function (event) {
|
||||
$('body').on('click', 'a.btn-trash', function (event) {
|
||||
return confirm($.kimai.settings['confirmDelete']);
|
||||
});
|
||||
|
||||
@@ -97,11 +97,21 @@ $(function() {
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
$('#' + targetSelect).find('option').remove().end().find('optgroup').remove().end();
|
||||
var selectName = '#' + targetSelect;
|
||||
var $select = $(selectName);
|
||||
var $emptyOption = $(selectName + ' option[value=""]');
|
||||
|
||||
$select.find('option').remove().end().find('optgroup').remove().end();
|
||||
|
||||
if ($emptyOption.length != 0) {
|
||||
$select.append('<option value="">' + $emptyOption.text() + '</option>');
|
||||
}
|
||||
|
||||
$.each(data, function(i, obj) {
|
||||
$('#' + targetSelect).append('<option value="' + obj.id + '">' + obj.name + '</option>');
|
||||
$select.append('<option value="' + obj.id + '">' + obj.name + '</option>');
|
||||
});
|
||||
$('#' + targetSelect).trigger('change')
|
||||
|
||||
$select.trigger('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -115,16 +125,6 @@ $(function() {
|
||||
}, function(){
|
||||
$(this).find('a.anchor').hide();
|
||||
});
|
||||
|
||||
/*
|
||||
$('input').iCheck({
|
||||
checkboxClass: 'icheckbox_square-blue',
|
||||
radioClass: 'iradio_square-blue',
|
||||
increaseArea: '20%' // optional
|
||||
});
|
||||
*/
|
||||
|
||||
// $.AdminLTE.pushMenu.expandOnHover();
|
||||
},
|
||||
pauseRecord: function(selector) {
|
||||
$(selector + ' .pull-left i').hover(function () {
|
||||
|
||||
@@ -6,28 +6,51 @@
|
||||
*/
|
||||
$(document).ready(function () {
|
||||
|
||||
//$('.navbar-form select').selectpicker({});
|
||||
/* Submit the pagination including the toolbar filters */
|
||||
$('body').on('click', 'div.navigation ul.pagination li a', function(event) {
|
||||
var $pager = $(".toolbar form input[name='page']");
|
||||
if ($pager.length === 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var $urlParts = $(this).attr('href').split('/');
|
||||
var page = $urlParts[$urlParts.length-1];
|
||||
$pager.val(page);
|
||||
$pager.trigger('change');
|
||||
return false;
|
||||
});
|
||||
|
||||
// DateRange - TODO improve me, so users can type without reloading in between
|
||||
$('.toolbar form input').change(function (event) {
|
||||
$('.toolbar form').submit();
|
||||
toolbarLoadDataAfterChange();
|
||||
});
|
||||
|
||||
$('.toolbar form select').change(function (event) {
|
||||
switch (event.target.id) {
|
||||
case 'customer':
|
||||
$('.toolbar form select#project').val('');
|
||||
if ($('.toolbar form select#activity').find(':selected').attr('data-project')) {
|
||||
$('.toolbar form select#activity').val('');
|
||||
}
|
||||
break;
|
||||
case 'project':
|
||||
if ($('.toolbar form select#activity').find(':selected').attr('data-project')) {
|
||||
$('.toolbar form select#activity').val('');
|
||||
}
|
||||
case 'page':
|
||||
break;
|
||||
default:
|
||||
$('.toolbar form input#page').val(1);
|
||||
}
|
||||
$('.toolbar form').submit();
|
||||
toolbarLoadDataAfterChange();
|
||||
});
|
||||
|
||||
function toolbarLoadDataAfterChange()
|
||||
{
|
||||
var $form = $('.toolbar form');
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
type: $form.attr('method'),
|
||||
data: $form.serialize(),
|
||||
success: function(html) {
|
||||
$('section.content').replaceWith(
|
||||
$(html).find('section.content')
|
||||
);
|
||||
},
|
||||
error: function(xhr, err) {
|
||||
$form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -222,3 +222,15 @@ div.dataTables_scrollHead table.table-bordered {
|
||||
.table-striped.dataTable tbody tr.active:nth-child(odd) th {
|
||||
background-color: #017ebc;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure that the action buttons do not line-break if another column takes all available space
|
||||
See https://github.com/twbs/bootstrap/issues/9939#issuecomment-37682952
|
||||
*/
|
||||
table.dataTable {
|
||||
td {
|
||||
.btn-group {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,12 @@
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.control-label {
|
||||
width: 100%;
|
||||
}
|
||||
.input-group {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"beberlei/DoctrineExtensions": "^1.0",
|
||||
"dama/doctrine-test-bundle": "^5.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.0",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^2.3",
|
||||
"friendsofsymfony/user-bundle": "~2.0",
|
||||
"fzaninotto/faker": "^1.8",
|
||||
"jms/serializer-bundle": "^2.4",
|
||||
"kevinpapst/adminlte-bundle": "~2.1",
|
||||
"nelmio/api-doc-bundle": "^3.2",
|
||||
@@ -21,11 +24,13 @@
|
||||
"sensio/framework-extra-bundle": "^5.2",
|
||||
"symfony/asset": "^4.0",
|
||||
"symfony/console": "^4.0",
|
||||
"symfony/debug-pack": "^1.0",
|
||||
"symfony/dotenv": "^4.0",
|
||||
"symfony/expression-language": "^4.0",
|
||||
"symfony/flex": "^1.0",
|
||||
"symfony/form": "^4.0",
|
||||
"symfony/framework-bundle": "^4.0",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/orm-pack": "^1.0",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
@@ -36,25 +41,20 @@
|
||||
"symfony/translation": "^4.0",
|
||||
"symfony/twig-bundle": "^4.0",
|
||||
"symfony/validator": "^4.0",
|
||||
"symfony/webpack-encore-pack": "^1.0",
|
||||
"symfony/web-server-bundle": "^4.0",
|
||||
"symfony/yaml": "^4.0",
|
||||
"twig/extensions": "^1.5",
|
||||
"white-october/pagerfanta-bundle": "^1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^5.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^2.10",
|
||||
"fzaninotto/faker": "^1.8",
|
||||
"phpunit/phpunit": "^7.0",
|
||||
"squizlabs/php_codesniffer": "^3.2",
|
||||
"symfony/browser-kit": "^4.0",
|
||||
"symfony/css-selector": "^4.0",
|
||||
"symfony/debug-pack": "^1.0",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/phpunit-bridge": "^4.0",
|
||||
"symfony/thanks": "^1.0",
|
||||
"symfony/web-server-bundle": "^4.0"
|
||||
"symfony/webpack-encore-pack": "^1.0"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": {
|
||||
|
||||
1202
composer.lock
generated
1202
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,12 @@
|
||||
kimai:
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Settings for the user management and login forms
|
||||
user:
|
||||
registration: true
|
||||
password_reset: true
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# All configs related to timesheet and record management
|
||||
timesheet:
|
||||
|
||||
@@ -33,6 +35,19 @@ kimai:
|
||||
# days: ['saturday','sunday']
|
||||
# factor: 1.5
|
||||
|
||||
# If you want to limit the max. active entries per user, you can do it here.
|
||||
#
|
||||
# The soft_limit is used as theme setting (formerly "kimai.theme.active_warning"):
|
||||
# display a warning color if the user has at least X active recordings
|
||||
#
|
||||
# The hard_limit is used to detect how many active records are allowed per user:
|
||||
# By default a user can only have one active time-record. The first one is stopped when the new one starts.
|
||||
# When hard_limit is > 1: as soon as the limit is reached and warning is shown and the user has to stop one active entry.
|
||||
active_entries:
|
||||
soft_limit: 1
|
||||
hard_limit: 3
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Invoice management
|
||||
#invoice:
|
||||
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
||||
@@ -40,6 +55,45 @@ kimai:
|
||||
# - 'var/invoices/'
|
||||
# - 'templates/invoice/renderer/'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Default settings used to populate forms
|
||||
#defaults:
|
||||
# customer:
|
||||
# timezone: Europe/Berlin
|
||||
# country: DE
|
||||
# currency: EUR
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Free configurable permission system, see var/docs/permissions.md
|
||||
permissions:
|
||||
# mapping complex rulesets of single permissions to named "sets" ("set name" = [array of "permissions"])
|
||||
sets:
|
||||
ACTIVITIES: [view_activity,create_activity,edit_activity,delete_activity]
|
||||
PROJECTS: [view_project,create_project,edit_project,delete_project]
|
||||
CUSTOMERS: [view_customer,create_customer,edit_customer,delete_customer]
|
||||
INVOICE: [view_invoice,create_invoice]
|
||||
INVOICE_TEMPLATE: [view_invoice_template,create_invoice_template,edit_invoice_template,delete_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]
|
||||
USER: [view_user,create_user,delete_user]
|
||||
RATE: [view_rate_own_timesheet,edit_rate_own_timesheet]
|
||||
RATE_OTHER: [view_rate_other_timesheet,edit_rate_other_timesheet]
|
||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||
maps:
|
||||
ROLE_USER: [TIMESHEET,PROFILE]
|
||||
ROLE_TEAMLEAD: [INVOICE,TIMESHEET,TIMESHEET_OTHER,PROFILE]
|
||||
ROLE_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,RATE,RATE_OTHER]
|
||||
ROLE_SUPER_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,PROFILE_OTHER,USER,RATE,RATE_OTHER]
|
||||
# adding single permissions to user roles, extending the definition from "sets" ("role name" = [array of "permissions"])
|
||||
roles:
|
||||
ROLE_USER: []
|
||||
ROLE_TEAMLEAD: [view_invoice_template,create_invoice_template,edit_invoice_template,view_rate_own_timesheet,view_rate_other_timesheet,hourly-rate_own_profile]
|
||||
ROLE_ADMIN: [hourly-rate_own_profile]
|
||||
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile]
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Language specific settings, like the date formats
|
||||
languages:
|
||||
de:
|
||||
@@ -50,6 +104,10 @@ kimai:
|
||||
date_short: 'Y-m-d'
|
||||
duration: '%%h:%%m hours'
|
||||
duration_short: '%%h:%%m h'
|
||||
pt_BR:
|
||||
date_short: 'd-m-Y'
|
||||
duration: '%%h:%%m horas'
|
||||
duration_short: '%%h:%%m h'
|
||||
it:
|
||||
date_short: 'd.m.Y'
|
||||
duration: '%%h:%%m h'
|
||||
@@ -75,6 +133,7 @@ kimai:
|
||||
duration: '%%h:%%m óra'
|
||||
duration_short: '%%h:%%m h'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# timesheet calendar configuration
|
||||
calendar:
|
||||
week_numbers: true
|
||||
@@ -93,18 +152,18 @@ kimai:
|
||||
# id: 'de.german#holiday@group.v.calendar.google.com'
|
||||
# color: '#ccc'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# theme related settings, will be available as twig globals at "kimai_context.*"
|
||||
# please see documentation at var/docs/theme.md
|
||||
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: ~
|
||||
# display a warning color if the user has at least X active recordings
|
||||
active_warning: 3
|
||||
# fallback color for all widgets that don't have a dedicated color
|
||||
# possible options: blue, black, purple, yellow, red, green
|
||||
box_color: 'green'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Dashboard widget sections, please see documentation at var/docs/dashboard.md
|
||||
dashboard:
|
||||
user_duration:
|
||||
@@ -133,6 +192,7 @@ kimai:
|
||||
permission: ROLE_ADMIN
|
||||
widgets: [amountToday, amountWeek, amountMonth, amountYear]
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# All available widgets, please see documentation at var/docs/dashboard.md
|
||||
widgets:
|
||||
userDurationToday: { title: stats.durationToday, query: duration, user: true, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
|
||||
|
||||
@@ -47,5 +47,5 @@ security:
|
||||
- { path: '^/(%app_locales%)/login', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }
|
||||
- { path: '^/(%app_locales%)/', roles: ROLE_USER }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED }
|
||||
|
||||
@@ -2,3 +2,5 @@ twig:
|
||||
#paths: ['%kernel.project_dir%/templates']
|
||||
debug: '%kernel.debug%'
|
||||
strict_variables: '%kernel.debug%'
|
||||
form_themes:
|
||||
- 'form/kimai-theme.html.twig'
|
||||
|
||||
@@ -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
|
||||
app_locales: en|de|it|fr|es|ru|ar|hu|pt_BR
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
# The best practice is to be explicit about your dependencies anyway.
|
||||
bind:
|
||||
$projectDirectory: "%kernel.project_dir%"
|
||||
$durationOnly: "%kimai.timesheet.duration_only%"
|
||||
$hardLimit: "%kimai.timesheet.active_entries.hard_limit%"
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
@@ -93,6 +93,14 @@ services:
|
||||
tags:
|
||||
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
||||
|
||||
App\Form\TimesheetEditForm:
|
||||
arguments:
|
||||
$durationOnly: "%kimai.timesheet.duration_only%"
|
||||
|
||||
App\Form\Extension\SelectWithApiDataExtension:
|
||||
tags:
|
||||
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
||||
|
||||
# ================================================================================
|
||||
# THEME
|
||||
# ================================================================================
|
||||
@@ -118,6 +126,13 @@ services:
|
||||
App\Timesheet\Calculator\RateCalculator:
|
||||
arguments: ["%kimai.timesheet.rates%"]
|
||||
|
||||
# ================================================================================
|
||||
# SECURITY & VOTER
|
||||
# ================================================================================
|
||||
|
||||
App\Security\RolePermissionManager:
|
||||
arguments: ["%kimai.permissions%"]
|
||||
|
||||
# ================================================================================
|
||||
# REPOSITORIES
|
||||
# ================================================================================
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build/app.js": "/build/app.js?d823754f6c3293656052",
|
||||
"build/app.css": "/build/app.css?979d88ed847886068fcf3eb0bc3de545",
|
||||
"build/app.js": "/build/app.js?5b6a5a9160ea779a403e",
|
||||
"build/app.css": "/build/app.css?fc142c29c80f5dcdbc4c745af16509f6",
|
||||
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||
"build/images/blue.png": "/build/images/blue.png?96f8a905",
|
||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
|
||||
|
||||
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
@@ -59,6 +58,9 @@ class ActivityController extends BaseApiController
|
||||
* @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter activities. If none is provided, only global activities will be returned.")
|
||||
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter activities (1=visible, 2=hidden, 3=both)")
|
||||
* @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Pass 'true' as string to fetch only global activities")
|
||||
* @Rest\QueryParam(name="globalsFirst", requirements="false", strict=true, nullable=true, description="Pass 'false' as string if you don't want the global activities to be listed first")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order (allowed values: 'ASC', 'DESC')")
|
||||
* @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')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
@@ -66,12 +68,26 @@ class ActivityController extends BaseApiController
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setOrderGlobalsFirst(true)
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS);
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
if (null !== ($orderBy = $paramFetcher->get('orderBy'))) {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
if (null !== ($globals = $paramFetcher->get('globals'))) {
|
||||
$query->setGlobalsOnly(true);
|
||||
}
|
||||
|
||||
if ('false' === ($globalsFirst = $paramFetcher->get('globalsFirst'))) {
|
||||
$query->setOrderGlobalsFirst(false);
|
||||
}
|
||||
|
||||
if (null !== ($project = $paramFetcher->get('project'))) {
|
||||
$query->setProject($project);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
@@ -57,13 +56,26 @@ class CustomerController extends BaseApiController
|
||||
* @SWG\Schema(ref="#/definitions/CustomerCollection"),
|
||||
* )
|
||||
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter activities (1=visible, 2=hidden, 3=both)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order (allowed values: 'ASC', 'DESC')")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|name", strict=true, nullable=true, description="The field by which results will be ordered (allowed values: 'id', 'name')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->setResultType(CustomerQuery::RESULT_TYPE_OBJECTS);
|
||||
$query
|
||||
->setResultType(CustomerQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
if (null !== ($orderBy = $paramFetcher->get('orderBy'))) {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
|
||||
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
@@ -58,6 +57,8 @@ class ProjectController extends BaseApiController
|
||||
* )
|
||||
* @Rest\QueryParam(name="customer", requirements="\d+", strict=true, nullable=true, description="Customer ID to filter projects")
|
||||
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter projects (1=visible, 2=hidden, 3=both)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order (allowed values: 'ASC', 'DESC')")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|name", strict=true, nullable=true, description="The field by which results will be ordered (allowed values: 'id', 'name')")
|
||||
*
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
@@ -65,7 +66,18 @@ class ProjectController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setResultType(ProjectQuery::RESULT_TYPE_OBJECTS);
|
||||
$query
|
||||
->setResultType(ProjectQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
if (null !== ($orderBy = $paramFetcher->get('orderBy'))) {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
if (null !== ($customer = $paramFetcher->get('customer'))) {
|
||||
$query->setCustomer($customer);
|
||||
|
||||
@@ -28,8 +28,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Timesheet")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class TimesheetController extends BaseApiController
|
||||
{
|
||||
@@ -43,14 +41,21 @@ class TimesheetController extends BaseApiController
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $hardLimit;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param TimesheetRepository $repository
|
||||
* @param int $hardLimit
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository)
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, int $hardLimit)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->hardLimit = $hardLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +72,8 @@ class TimesheetController extends BaseApiController
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order (allowed values: 'ASC', 'DESC')")
|
||||
* @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')")
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
@@ -120,6 +127,8 @@ class TimesheetController extends BaseApiController
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity")
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
@@ -146,6 +155,8 @@ class TimesheetController extends BaseApiController
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
@@ -157,8 +168,7 @@ class TimesheetController extends BaseApiController
|
||||
|
||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||
'csrf_protection' => false,
|
||||
'method' => 'POST',
|
||||
'duration_only' => false,
|
||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||
]);
|
||||
|
||||
$form->setData($timesheet);
|
||||
@@ -173,6 +183,21 @@ class TimesheetController extends BaseApiController
|
||||
return new Response('You are not allowed to start this timesheet record', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if ($form->has('duration')) {
|
||||
$duration = $form->get('duration')->getData();
|
||||
if ($duration > 0) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $form->getData();
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$record->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
$this->repository->stopActiveEntries(
|
||||
$timesheet->getUser(),
|
||||
$this->hardLimit
|
||||
);
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($timesheet);
|
||||
$entityManager->flush();
|
||||
|
||||
@@ -22,9 +22,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("User")
|
||||
*
|
||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
*/
|
||||
class UserController extends BaseApiController
|
||||
{
|
||||
@@ -55,6 +52,8 @@ class UserController extends BaseApiController
|
||||
* @SWG\Schema(ref="#/definitions/UserCollection"),
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction()
|
||||
@@ -73,6 +72,8 @@ class UserController extends BaseApiController
|
||||
* @SWG\Schema(ref="#/definitions/UserEntity"),
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
|
||||
@@ -69,8 +69,8 @@ class TimesheetEntity
|
||||
|
||||
if (null === $entry->getEnd()) {
|
||||
// TODO move these colors to the controller
|
||||
$this->borderColor = '#f39c12';
|
||||
$this->backgroundColor = '#f39c12';
|
||||
$this->borderColor = '367fa9'; //'#f39c12';
|
||||
$this->backgroundColor = '#3c8dbc'; //'#f39c12';
|
||||
} else {
|
||||
$this->end = $entry->getEnd();
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ class KimaiImporterCommand extends Command
|
||||
$allImports += $counter;
|
||||
$io->success('Imported users: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import users: ' . $ex->getMessage());
|
||||
$io->error('Failed to import users: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -257,7 +257,7 @@ class KimaiImporterCommand extends Command
|
||||
$allImports += $counter;
|
||||
$io->success('Imported customers: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import customers: ' . $ex->getMessage());
|
||||
$io->error('Failed to import customers: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -267,7 +267,7 @@ class KimaiImporterCommand extends Command
|
||||
$allImports += $counter;
|
||||
$io->success('Imported projects: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import projects: ' . $ex->getMessage());
|
||||
$io->error('Failed to import projects: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -277,7 +277,7 @@ class KimaiImporterCommand extends Command
|
||||
$allImports += $counter;
|
||||
$io->success('Imported activities: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import activities: ' . $ex->getMessage());
|
||||
$io->error('Failed to import activities: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -287,7 +287,7 @@ class KimaiImporterCommand extends Command
|
||||
$allImports += $counter;
|
||||
$io->success('Imported timesheet records: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import timesheet records: ' . $ex->getMessage());
|
||||
$io->error('Failed to import timesheet records: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -318,19 +318,22 @@ class KimaiImporterCommand extends Command
|
||||
*/
|
||||
protected function checkDatabaseVersion(SymfonyStyle $io, $requiredVersion, $requiredRevision)
|
||||
{
|
||||
$optionColumn = $this->connection->quoteIdentifier('option');
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
|
||||
$version = $this->connection->createQueryBuilder()
|
||||
->select('value')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration'))
|
||||
->where('option = :option')
|
||||
->setParameter(':option', 'version')
|
||||
->where($qb->expr()->eq($optionColumn, ':option'))
|
||||
->setParameter('option', 'version')
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
$revision = $this->connection->createQueryBuilder()
|
||||
->select('value')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration'))
|
||||
->where('option = :option')
|
||||
->setParameter(':option', 'revision')
|
||||
->where($qb->expr()->eq($optionColumn, ':option'))
|
||||
->setParameter('option', 'revision')
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
@@ -382,8 +385,10 @@ class KimaiImporterCommand extends Command
|
||||
protected function bytesHumanReadable($size)
|
||||
{
|
||||
$unit = ['b', 'kB', 'MB', 'GB'];
|
||||
$i = floor(log($size, 1024));
|
||||
$a = (int) $i;
|
||||
|
||||
return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
|
||||
return @round($size / pow(1024, $i), 2) . ' ' . $unit[$a];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,12 +424,8 @@ class KimaiImporterCommand extends Command
|
||||
if ($errors->count() > 0) {
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolation $error */
|
||||
foreach ($errors as $error) {
|
||||
$value = $error->getInvalidValue();
|
||||
$io->error(
|
||||
$error->getPropertyPath()
|
||||
. ' (' . (is_array($value) ? implode(',', $value) : $value) . ')'
|
||||
. "\n "
|
||||
. $error->getMessage()
|
||||
(string) $error
|
||||
);
|
||||
}
|
||||
|
||||
@@ -938,7 +939,7 @@ class KimaiImporterCommand extends Command
|
||||
;
|
||||
|
||||
if (!$this->validateImport($io, $timesheet)) {
|
||||
throw new \Exception('Failed to validate timesheet record: ' . $timesheet->getId());
|
||||
throw new \Exception('Failed to validate timesheet record: ' . $oldRecord['timeEntryID']);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -949,8 +950,7 @@ class KimaiImporterCommand extends Command
|
||||
}
|
||||
++$counter;
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to create timesheet record: ' . $timesheet->getId());
|
||||
$io->error('Reason: ' . $ex->getMessage());
|
||||
$io->error('Failed to create timesheet record: ' . $ex->getMessage());
|
||||
}
|
||||
|
||||
$io->write('.');
|
||||
|
||||
@@ -17,7 +17,7 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = 0.6;
|
||||
public const VERSION = '0.7';
|
||||
/**
|
||||
* The release name, will only change for new major version
|
||||
*/
|
||||
|
||||
@@ -42,15 +42,7 @@ abstract class AbstractController extends Controller
|
||||
*/
|
||||
protected function flashSuccess($translationKey, $parameter = [])
|
||||
{
|
||||
if (!empty($parameter)) {
|
||||
$translationKey = $this->getTranslator()->trans(
|
||||
$translationKey,
|
||||
$parameter,
|
||||
self::DOMAIN_FLASH
|
||||
);
|
||||
}
|
||||
|
||||
$this->addFlash(self::FLASH_SUCCESS, $translationKey);
|
||||
$this->addFlashTranslated(self::FLASH_SUCCESS, $translationKey, $parameter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,15 +53,7 @@ abstract class AbstractController extends Controller
|
||||
*/
|
||||
protected function flashWarning($translationKey, $parameter = [])
|
||||
{
|
||||
if (!empty($parameter)) {
|
||||
$translationKey = $this->getTranslator()->trans(
|
||||
$translationKey,
|
||||
$parameter,
|
||||
self::DOMAIN_FLASH
|
||||
);
|
||||
}
|
||||
|
||||
$this->addFlash(self::FLASH_WARNING, $translationKey);
|
||||
$this->addFlashTranslated(self::FLASH_WARNING, $translationKey, $parameter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,15 +63,30 @@ abstract class AbstractController extends Controller
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashError($translationKey, $parameter = [])
|
||||
{
|
||||
$this->addFlashTranslated(self::FLASH_ERROR, $translationKey, $parameter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fully translated (both $message and all keys in $parameter) flash message to the stack.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $message
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function addFlashTranslated(string $type, string $message, array $parameter = [])
|
||||
{
|
||||
if (!empty($parameter)) {
|
||||
$translationKey = $this->getTranslator()->trans(
|
||||
$translationKey,
|
||||
foreach ($parameter as $key => $value) {
|
||||
$parameter[$key] = $this->getTranslator()->trans($value, [], self::DOMAIN_FLASH);
|
||||
}
|
||||
$message = $this->getTranslator()->trans(
|
||||
$message,
|
||||
$parameter,
|
||||
self::DOMAIN_FLASH
|
||||
);
|
||||
}
|
||||
|
||||
$this->addFlash(self::FLASH_ERROR, $translationKey);
|
||||
$this->addFlash($type, $message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class AboutController extends AbstractController
|
||||
'open_basedir',
|
||||
'post_max_size',
|
||||
'sys_temp_dir',
|
||||
'date.timezone',
|
||||
];
|
||||
|
||||
$settings = [];
|
||||
|
||||
@@ -27,8 +27,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/activity")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*/
|
||||
class ActivityController extends AbstractController
|
||||
{
|
||||
@@ -44,6 +43,11 @@ class ActivityController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_activity", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -71,6 +75,7 @@ class ActivityController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_activity_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_activity')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
@@ -94,8 +99,6 @@ class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_activity_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', activity)")
|
||||
*
|
||||
|
||||
@@ -26,8 +26,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/customer")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_customer')")
|
||||
*/
|
||||
class CustomerController extends AbstractController
|
||||
{
|
||||
@@ -55,6 +54,11 @@ class CustomerController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_customer", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_customer')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -81,6 +85,10 @@ class CustomerController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_customer')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -95,6 +103,10 @@ 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 \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function editAction(Customer $customer, Request $request)
|
||||
{
|
||||
@@ -102,35 +114,6 @@ class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderCustomerForm(Customer $customer, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_edit.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', customer)")
|
||||
*
|
||||
@@ -179,6 +162,33 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderCustomerForm(Customer $customer, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($customer);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_edit.html.twig', [
|
||||
'customer' => $customer,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
|
||||
@@ -28,8 +28,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage projects in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/project")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_project')")
|
||||
*/
|
||||
class ProjectController extends AbstractController
|
||||
{
|
||||
@@ -45,6 +44,11 @@ class ProjectController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_project", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
* @Security("is_granted('view_project')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -72,6 +76,10 @@ class ProjectController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_project_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_project')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -81,6 +89,10 @@ 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 \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function editAction(Project $project, Request $request)
|
||||
{
|
||||
@@ -88,8 +100,6 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', project)")
|
||||
*
|
||||
|
||||
@@ -24,27 +24,16 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used for manage timesheet entries in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/team/timesheet")
|
||||
* @Security("is_granted('ROLE_TEAMLEAD')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
*/
|
||||
class TimesheetController extends AbstractController
|
||||
{
|
||||
use TimesheetControllerTrait;
|
||||
|
||||
/**
|
||||
* TimesheetController constructor.
|
||||
* @param bool $durationOnly
|
||||
*/
|
||||
public function __construct(bool $durationOnly)
|
||||
{
|
||||
$this->setDurationMode($durationOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* This route shows all users timesheet entries.
|
||||
*
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_timesheet", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
*
|
||||
* @param $page
|
||||
* @param Request $request
|
||||
@@ -73,7 +62,7 @@ class TimesheetController extends AbstractController
|
||||
|
||||
return $this->render('admin/timesheet.html.twig', [
|
||||
'entries' => $entries,
|
||||
'page' => $page,
|
||||
'page' => $query->getPage(),
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
@@ -81,8 +70,38 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to stop a running entry.
|
||||
* @Route(path="/export", name="admin_timesheet_export", methods={"GET"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function exportAction(Request $request)
|
||||
{
|
||||
$query = new TimesheetQuery();
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var TimesheetQuery $query */
|
||||
$query = $form->getData();
|
||||
if (null !== $query->getBegin()) {
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
}
|
||||
if (null !== $query->getEnd()) {
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
}
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/timesheet_export.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/stop", name="admin_timesheet_stop", methods={"GET"})
|
||||
* @Security("is_granted('stop', entry)")
|
||||
*
|
||||
@@ -95,8 +114,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to edit an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/edit", name="admin_timesheet_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', entry)")
|
||||
*
|
||||
@@ -110,9 +127,8 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to create a new entry by form.
|
||||
*
|
||||
* @Route(path="/create", name="admin_timesheet_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_other_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
@@ -123,8 +139,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="admin_timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
@@ -149,33 +163,33 @@ class TimesheetController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param string $redirectRoute
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getCreateForm(Timesheet $entry)
|
||||
protected function getCreateForm(Timesheet $entry, string $redirectRoute)
|
||||
{
|
||||
return $this->createForm(TimesheetEditForm::class, $entry, [
|
||||
'action' => $this->generateUrl('admin_timesheet_create'),
|
||||
'method' => 'POST',
|
||||
'duration_only' => $this->isDurationOnlyMode(),
|
||||
'include_user' => true
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'include_user' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param int $page
|
||||
* @param string $redirectRoute
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getEditForm(Timesheet $entry, $page)
|
||||
protected function getEditForm(Timesheet $entry, $page, string $redirectRoute)
|
||||
{
|
||||
return $this->createForm(TimesheetEditForm::class, $entry, [
|
||||
'action' => $this->generateUrl('admin_timesheet_edit', [
|
||||
'id' => $entry->getId(),
|
||||
'page' => $page
|
||||
'page' => $page,
|
||||
]),
|
||||
'method' => 'POST',
|
||||
'duration_only' => $this->isDurationOnlyMode(),
|
||||
'include_user' => true
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
'include_user' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
* Controller used to manage users in the admin part of the site.
|
||||
*
|
||||
* @Route(path="/admin/user")
|
||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Security("is_granted('view_user')")
|
||||
*/
|
||||
class UserController extends AbstractController
|
||||
{
|
||||
@@ -54,7 +53,8 @@ class UserController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_user", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated", methods={"GET"})
|
||||
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -84,11 +84,15 @@ class UserController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create', user)")
|
||||
* @Security("is_granted('create_user')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$editForm = $this->createEditForm($user);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
@@ -124,8 +128,6 @@ class UserController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing user.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_user_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', userToDelete)")
|
||||
*
|
||||
@@ -136,6 +138,7 @@ class UserController extends AbstractController
|
||||
*/
|
||||
public function deleteAction(User $userToDelete, Request $request)
|
||||
{
|
||||
// $userToDelete MUST not be called $user, as $user is always the current user!
|
||||
$stats = $this->getDoctrine()->getRepository(Timesheet::class)->getUserStatistics($userToDelete);
|
||||
|
||||
$deleteForm = $this->createFormBuilder()
|
||||
|
||||
@@ -79,14 +79,9 @@ class CalendarController extends AbstractController
|
||||
->setUser($this->getUser())
|
||||
->setState(TimesheetQuery::STATE_ALL)
|
||||
->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->setEnd($end)
|
||||
;
|
||||
|
||||
// running entries should only occur for the current month, but they won't
|
||||
// be found if we add the end to the query
|
||||
if ((new \DateTime())->getTimestamp() > $end->getTimestamp()) {
|
||||
$query->setEnd($end);
|
||||
}
|
||||
|
||||
$repository = $this->getDoctrine()->getRepository(Timesheet::class);
|
||||
|
||||
/* @var $entries Timesheet[] */
|
||||
|
||||
@@ -60,6 +60,8 @@ class HelpController extends AbstractController
|
||||
|
||||
$content = file_get_contents($chapterFile);
|
||||
|
||||
$content = preg_replace('/\[(.+)\]\(([\w\-\/]+)(\.md)(\#[\w\-]+){0,1}\)/i', '[$1]($2$4)', $content);
|
||||
|
||||
return $this->render('help/index.html.twig', [
|
||||
'breadcrumb' => $breadcrumb,
|
||||
'chapter' => $chapter,
|
||||
|
||||
@@ -28,7 +28,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* Controller used to manage invoices.
|
||||
*
|
||||
* @Route(path="/invoice")
|
||||
* @Security("is_granted('ROLE_TEAMLEAD')")
|
||||
* @Security("is_granted('view_invoice') or is_granted('view_invoice_template')")
|
||||
*/
|
||||
class InvoiceController extends AbstractController
|
||||
{
|
||||
@@ -67,6 +67,7 @@ class InvoiceController extends AbstractController
|
||||
$end = new \DateTime('last day of this month');
|
||||
|
||||
$query = new InvoiceQuery();
|
||||
$query->setOrder(InvoiceQuery::ORDER_ASC);
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setUser($this->getUser());
|
||||
@@ -77,7 +78,7 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="invoice", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', 'invoice')")
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -111,7 +112,7 @@ class InvoiceController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/print", name="invoice_print", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create', 'invoice')")
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -212,7 +213,7 @@ 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')")
|
||||
* @Security("is_granted('view_invoice_template')")
|
||||
*
|
||||
* @param $page
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -244,7 +245,7 @@ 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')")
|
||||
* @Security("is_granted('create_invoice_template')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @param InvoiceTemplate|null $template
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Form\UserApiTokenType;
|
||||
use App\Form\UserEditType;
|
||||
use App\Form\UserPasswordType;
|
||||
@@ -19,6 +20,7 @@ use App\Form\UserRolesType;
|
||||
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\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -28,10 +30,15 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
* User profile controller
|
||||
*
|
||||
* @Route(path="/profile")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Security("is_granted('view_own_profile') or is_granted('view_other_profile')")
|
||||
*/
|
||||
class ProfileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
@@ -40,9 +47,10 @@ class ProfileController extends AbstractController
|
||||
/**
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder)
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +170,10 @@ class ProfileController extends AbstractController
|
||||
*/
|
||||
public function savePreferencesAction(User $profile, Request $request)
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($profile);
|
||||
$this->dispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
|
||||
$original = [];
|
||||
foreach ($profile->getPreferences() as $preference) {
|
||||
$original[$preference->getName()] = $preference;
|
||||
@@ -194,8 +206,11 @@ class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
// switch locale if neccessary
|
||||
$locale = $profile->getPreferenceValue('language', $request->getLocale());
|
||||
// switch locale ONLY if updated profile is the current user
|
||||
$locale = $request->getLocale();
|
||||
if ($this->getUser()->getId() === $profile->getId()) {
|
||||
$locale = $profile->getPreferenceValue('language', $locale);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('user_profile_preferences', [
|
||||
'_locale' => $locale,
|
||||
@@ -269,6 +284,10 @@ class ProfileController extends AbstractController
|
||||
*/
|
||||
private function createPreferencesForm(User $user)
|
||||
{
|
||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||
$event = new PrepareUserEvent($user);
|
||||
$this->dispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
|
||||
return $this->createForm(
|
||||
UserPreferencesForm::class,
|
||||
$user,
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -24,24 +23,24 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
* Controller used to manage timesheets.
|
||||
*
|
||||
* @Route(path="/timesheet")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*/
|
||||
class TimesheetController extends AbstractController
|
||||
{
|
||||
use TimesheetControllerTrait;
|
||||
|
||||
/**
|
||||
* @param bool $durationOnly
|
||||
* @param int $hardLimit
|
||||
*/
|
||||
public function __construct(bool $durationOnly)
|
||||
public function __construct(int $hardLimit)
|
||||
{
|
||||
$this->setDurationMode($durationOnly);
|
||||
$this->setHardLimit($hardLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
@@ -72,7 +71,7 @@ class TimesheetController extends AbstractController
|
||||
|
||||
return $this->render('timesheet/index.html.twig', [
|
||||
'entries' => $entries,
|
||||
'page' => $page,
|
||||
'page' => $query->getPage(),
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
@@ -81,6 +80,7 @@ class TimesheetController extends AbstractController
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="timesheet_export", methods={"GET"})
|
||||
* @Security("is_granted('export_own_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -114,8 +114,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The "main button and fly-out" for displaying (and stopping) active entries.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function activeEntriesAction()
|
||||
@@ -144,8 +142,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to re-start a timesheet entry.
|
||||
*
|
||||
* @Route(path="/start/{id}", name="timesheet_start", requirements={"id" = "\d+"}, methods={"GET", "POST"})
|
||||
* @Security("is_granted('start', timesheet)")
|
||||
*
|
||||
@@ -171,6 +167,8 @@ class TimesheetController extends AbstractController
|
||||
} else {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
$this->stopActiveEntries($user);
|
||||
|
||||
$entityManager->flush();
|
||||
$this->flashSuccess('timesheet.start.success');
|
||||
}
|
||||
@@ -182,8 +180,6 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to edit an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/edit", name="timesheet_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', entry)")
|
||||
*
|
||||
@@ -193,29 +189,35 @@ class TimesheetController extends AbstractController
|
||||
*/
|
||||
public function editAction(Timesheet $entry, Request $request)
|
||||
{
|
||||
$route = 'timesheet';
|
||||
|
||||
if (null !== $request->get('page')) {
|
||||
return $this->edit($entry, $request, 'timesheet_paginated', 'timesheet/edit.html.twig');
|
||||
$route = 'timesheet_paginated';
|
||||
} elseif ('calendar' === $request->get('origin')) {
|
||||
$route = 'calendar';
|
||||
}
|
||||
|
||||
return $this->edit($entry, $request, 'timesheet', 'timesheet/edit.html.twig');
|
||||
return $this->edit($entry, $request, $route, 'timesheet/edit.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to create a new entry by form.
|
||||
*
|
||||
* @Route(path="/create", name="timesheet_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
return $this->create($request, 'timesheet', 'timesheet/edit.html.twig');
|
||||
$route = 'timesheet';
|
||||
if ('calendar' === $request->get('origin')) {
|
||||
$route = 'calendar';
|
||||
}
|
||||
|
||||
return $this->create($request, $route, 'timesheet/edit.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
@@ -240,31 +242,32 @@ class TimesheetController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param string $redirectRoute
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getCreateForm(Timesheet $entry)
|
||||
protected function getCreateForm(Timesheet $entry, string $redirectRoute)
|
||||
{
|
||||
return $this->createForm(TimesheetEditForm::class, $entry, [
|
||||
'action' => $this->generateUrl('timesheet_create'),
|
||||
'method' => 'POST',
|
||||
'duration_only' => $this->isDurationOnlyMode(),
|
||||
'action' => $this->generateUrl('timesheet_create', ['origin' => $redirectRoute]),
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param int $page
|
||||
* @param string $redirectRoute
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getEditForm(Timesheet $entry, $page)
|
||||
protected function getEditForm(Timesheet $entry, $page, string $redirectRoute)
|
||||
{
|
||||
return $this->createForm(TimesheetEditForm::class, $entry, [
|
||||
'action' => $this->generateUrl('timesheet_edit', [
|
||||
'id' => $entry->getId(),
|
||||
'page' => $page
|
||||
'page' => $page,
|
||||
'origin' => $redirectRoute,
|
||||
]),
|
||||
'method' => 'POST',
|
||||
'duration_only' => $this->isDurationOnlyMode(),
|
||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Doctrine\Common\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
@@ -22,24 +23,24 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
trait TimesheetControllerTrait
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
* @var int
|
||||
*/
|
||||
private $durationOnly = false;
|
||||
private $hardLimit = 1;
|
||||
|
||||
/**
|
||||
* @param bool $durationOnly
|
||||
* @param int $hardLimit
|
||||
*/
|
||||
protected function setDurationMode(bool $durationOnly)
|
||||
protected function setHardLimit(int $hardLimit)
|
||||
{
|
||||
$this->durationOnly = $durationOnly;
|
||||
$this->hardLimit = $hardLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @return int
|
||||
*/
|
||||
protected function isDurationOnlyMode()
|
||||
protected function getHardLimit()
|
||||
{
|
||||
return $this->durationOnly;
|
||||
return $this->hardLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +77,7 @@ trait TimesheetControllerTrait
|
||||
*/
|
||||
protected function edit(Timesheet $entry, Request $request, $redirectRoute, $renderTemplate)
|
||||
{
|
||||
$editForm = $this->getEditForm($entry, $request->get('page'));
|
||||
$editForm = $this->getEditForm($entry, $request->get('page'), $request->get('origin', 'timesheet'));
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
@@ -153,7 +154,7 @@ trait TimesheetControllerTrait
|
||||
}
|
||||
}
|
||||
|
||||
$createForm = $this->getCreateForm($entry);
|
||||
$createForm = $this->getCreateForm($entry, $redirectRoute);
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||
@@ -169,11 +170,16 @@ trait TimesheetControllerTrait
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->stopActiveEntries($entry->getUser());
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
$this->flashSuccess('action.update.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('timesheet.start.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute($redirectRoute);
|
||||
}
|
||||
@@ -184,18 +190,31 @@ trait TimesheetControllerTrait
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @throws \App\Repository\RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
protected function stopActiveEntries(User $user)
|
||||
{
|
||||
$this->getRepository()->stopActiveEntries($user, $this->getHardLimit());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param string $redirectRoute
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
abstract protected function getCreateForm(Timesheet $entry);
|
||||
abstract protected function getCreateForm(Timesheet $entry, string $redirectRoute);
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param int $page
|
||||
* @param string $redirectRoute
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
abstract protected function getEditForm(Timesheet $entry, $page);
|
||||
abstract protected function getEditForm(Timesheet $entry, $page, string $redirectRoute);
|
||||
|
||||
/**
|
||||
* Adds a "successful" flash message to the stack.
|
||||
|
||||
@@ -47,7 +47,6 @@ class UserFixtures extends Fixture
|
||||
private $encoder;
|
||||
|
||||
/**
|
||||
* AppFixtures constructor.
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder)
|
||||
|
||||
@@ -40,11 +40,43 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
||||
$container->setParameter('kimai.invoice.documents', $config['invoice']['documents']);
|
||||
$container->setParameter('kimai.defaults', $config['defaults']);
|
||||
|
||||
$this->createPermissionParameter($config['permissions'], $container);
|
||||
$this->createThemeParameter($config['theme'], $container);
|
||||
$this->createUserParameter($config['user'], $container);
|
||||
$this->createTimesheetParameter($config['timesheet'], $container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs some pre-compilation on the configured permissions from kimai.yaml
|
||||
* to save us from constant array lookups from during runtime.
|
||||
*
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
protected function createPermissionParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
foreach ($config['maps'] as $role => $sets) {
|
||||
if (!isset($config['roles'][$role])) {
|
||||
$exception = new InvalidConfigurationException(
|
||||
'Configured permission set includes unknown role "' . $role . '"'
|
||||
);
|
||||
$exception->setPath('kimai.permissions.maps.' . $role);
|
||||
throw $exception;
|
||||
}
|
||||
foreach ($sets as $set) {
|
||||
if (!isset($config['sets'][$set])) {
|
||||
$exception = new InvalidConfigurationException(
|
||||
'Configured permission set "' . $set . '" for role "' . $role . '" is unknown'
|
||||
);
|
||||
$exception->setPath('kimai.permissions.maps.' . $role);
|
||||
throw $exception;
|
||||
}
|
||||
$config['roles'][$role] = array_unique(array_merge($config['roles'][$role], $config['sets'][$set]));
|
||||
}
|
||||
}
|
||||
$container->setParameter('kimai.permissions', $config['roles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
@@ -86,6 +118,8 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
||||
$container->setParameter('kimai.timesheet.rounding', $config['rounding']);
|
||||
$container->setParameter('kimai.timesheet.duration_only', $config['duration_only']);
|
||||
$container->setParameter('kimai.timesheet.markdown', $config['markdown_content']);
|
||||
$container->setParameter('kimai.timesheet.active_entries.soft_limit', $config['active_entries']['soft_limit']);
|
||||
$container->setParameter('kimai.timesheet.active_entries.hard_limit', $config['active_entries']['hard_limit']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,8 +23,6 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
*/
|
||||
protected $allowedEngines = [
|
||||
'mysql',
|
||||
'oracle',
|
||||
'postgres',
|
||||
'sqlite'
|
||||
];
|
||||
|
||||
@@ -37,15 +35,6 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
$engine = null;
|
||||
|
||||
// TODO - this does return the wrong connection. it used to be mysql, even if
|
||||
// TODO - getenv('DATABASE_URL') returned an sqlite:// connection string
|
||||
/*
|
||||
$dbConfig = $container->getExtensionConfig('doctrine');
|
||||
if (isset($dbConfig[0]['dbal']['driver'])) {
|
||||
$engine = str_replace('pdo_', '', $dbConfig[0]['dbal']['driver']);
|
||||
}
|
||||
*/
|
||||
|
||||
if (null === $engine) {
|
||||
$dbConfig = explode('://', getenv('DATABASE_URL'));
|
||||
$engine = $dbConfig['0'] ?: null;
|
||||
|
||||
@@ -27,7 +27,9 @@ class TwigContextCompilerPass implements CompilerPassInterface
|
||||
$theme = $container->getParameter('kimai.theme');
|
||||
$durationOnly = $container->getParameter('kimai.timesheet.duration_only');
|
||||
|
||||
$twig->addMethodCall('addGlobal', ['kimai_context', $theme]);
|
||||
$twig->addMethodCall('addGlobal', ['kimai_context', array_merge($theme, [
|
||||
'active_warning' => $container->getParameter('kimai.timesheet.active_entries.soft_limit')
|
||||
])]);
|
||||
$twig->addMethodCall('addGlobal', ['duration_only', $durationOnly]);
|
||||
|
||||
if ($container->hasDefinition('twig.loader.native_filesystem')) {
|
||||
|
||||
@@ -40,6 +40,7 @@ class Configuration implements ConfigurationInterface
|
||||
->append($this->getDashboardNode())
|
||||
->append($this->getWidgetsNode())
|
||||
->append($this->getDefaultsNode())
|
||||
->append($this->getPermissionsNode())
|
||||
->end()
|
||||
->end();
|
||||
|
||||
@@ -84,7 +85,6 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
|
||||
->arrayNode('rates')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
@@ -111,6 +111,30 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->arrayNode('active_entries')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->integerNode('soft_limit')
|
||||
->defaultValue(1)
|
||||
->validate()
|
||||
->ifTrue(function ($value) {
|
||||
return $value <= 0;
|
||||
})
|
||||
->thenInvalid('The soft_limit must be at least 1')
|
||||
->end()
|
||||
->end()
|
||||
->integerNode('hard_limit')
|
||||
->defaultValue(1)
|
||||
->validate()
|
||||
->ifTrue(function ($value) {
|
||||
return $value <= 0;
|
||||
})
|
||||
->thenInvalid('The hard_limit must be at least 1')
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
@@ -211,6 +235,7 @@ class Configuration implements ConfigurationInterface
|
||||
->children()
|
||||
->integerNode('active_warning')
|
||||
->defaultValue(3)
|
||||
->setDeprecated('The node "%node%" at path "%path%" is deprecated, please use "kimai.timesheet.active_entries.soft_limit" instead.')
|
||||
->end()
|
||||
->scalarNode('box_color')
|
||||
->defaultValue('green')
|
||||
@@ -329,4 +354,51 @@ class Configuration implements ConfigurationInterface
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getPermissionsNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('permissions');
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->arrayNode('sets')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->performNoDeepMerging()
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('maps')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->performNoDeepMerging()
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('roles')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->performNoDeepMerging()
|
||||
->arrayPrototype()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository")
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @Assert\Callback("validate")
|
||||
*/
|
||||
class Timesheet
|
||||
{
|
||||
@@ -163,9 +164,9 @@ class Timesheet
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
// FIXME test and then remove it, this should not be neccessary
|
||||
if (null === $end) {
|
||||
$this->duration = 0;
|
||||
$this->rate = 0;
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -345,12 +346,8 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* These validations are used in places, where we don't use a form yet (like the API).
|
||||
*
|
||||
* @param ExecutionContextInterface $context
|
||||
* @param mixed $payload
|
||||
*
|
||||
* @Assert\Callback
|
||||
* @param $payload
|
||||
*/
|
||||
public function validate(ExecutionContextInterface $context, $payload)
|
||||
{
|
||||
@@ -376,21 +373,21 @@ class Timesheet
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($activity->getVisible() === false) {
|
||||
if (null === $this->getEnd() && $activity->getVisible() === false) {
|
||||
$context->buildViolation('Cannot start a disabled activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($project->getVisible() === false) {
|
||||
if (null === $this->getEnd() && $project->getVisible() === false) {
|
||||
$context->buildViolation('Cannot start a disabled project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($project->getCustomer()->getVisible() === false) {
|
||||
if (null === $this->getEnd() && $project->getCustomer()->getVisible() === false) {
|
||||
$context->buildViolation('Cannot start a disabled customer.')
|
||||
->atPath('customer')
|
||||
->setTranslationDomain('validators')
|
||||
@@ -398,18 +395,25 @@ class Timesheet
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $this->getBegin() && null !== $this->getEnd()) {
|
||||
$context->buildViolation('You must submit a begin date before an end date is added.')
|
||||
if (null === $this->getBegin()) {
|
||||
$context->buildViolation('You must submit a begin date.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
} else {
|
||||
if (null !== $this->getBegin() && null !== $this->getEnd() && $this->getEnd()->getTimestamp() < $this->getBegin()->getTimestamp()) {
|
||||
$context->buildViolation('End date must not be earlier then start date.')
|
||||
->atPath('end')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null !== $this->getBegin() && null !== $this->getEnd() && $this->getEnd()->getTimestamp() < $this->getBegin()->getTimestamp()) {
|
||||
$context->buildViolation('End date must not be earlier then start date.')
|
||||
->atPath('end')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
if (time() < $this->getBegin()->getTimestamp()) {
|
||||
$context->buildViolation('The begin date cannot be in the future.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,11 @@ class UserPreference
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $enabled = true;
|
||||
|
||||
/**
|
||||
* @var Constraint[]
|
||||
*/
|
||||
@@ -181,6 +186,25 @@ class UserPreference
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $enabled
|
||||
* @return UserPreference
|
||||
*/
|
||||
public function setEnabled(bool $enabled)
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the constraints which are used for validation of the value.
|
||||
*
|
||||
|
||||
@@ -10,23 +10,48 @@
|
||||
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.
|
||||
*/
|
||||
class ConfigureAdminMenuEvent extends ConfigureMenuEvent
|
||||
class ConfigureAdminMenuEvent extends Event
|
||||
{
|
||||
public const CONFIGURE = 'app.admin_menu_configure';
|
||||
|
||||
/**
|
||||
* This function will either return a MenuItem or null.
|
||||
*
|
||||
* In case this returns null, the user has not the ROLE_ADMIN.
|
||||
*
|
||||
* @return MenuItemModel|null
|
||||
* @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->getMenu()->getRootItem('admin');
|
||||
return $this->menu;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,51 @@
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* The ConfigureMainMenuEvent is used for populating the main navigation.
|
||||
*/
|
||||
class ConfigureMainMenuEvent extends ConfigureMenuEvent
|
||||
class ConfigureMainMenuEvent extends Event
|
||||
{
|
||||
public const CONFIGURE = 'app.main_menu_configure';
|
||||
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
private $request;
|
||||
/**
|
||||
* @var SidebarMenuEvent
|
||||
*/
|
||||
private $event;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param SidebarMenuEvent $event
|
||||
*/
|
||||
public function __construct(
|
||||
Request $request,
|
||||
SidebarMenuEvent $event
|
||||
) {
|
||||
$this->request = $request;
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SidebarMenuEvent
|
||||
*/
|
||||
public function getMenu()
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +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\Event\SidebarMenuEvent;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* The ConfigureMenuEvent is used for populating navigations.
|
||||
*/
|
||||
abstract class ConfigureMenuEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
private $request;
|
||||
/**
|
||||
* @var SidebarMenuEvent
|
||||
*/
|
||||
private $event;
|
||||
|
||||
/**
|
||||
* ConfigureMenuEvent constructor.
|
||||
* @param Request $request
|
||||
* @param SidebarMenuEvent $event
|
||||
*/
|
||||
public function __construct(
|
||||
Request $request,
|
||||
SidebarMenuEvent $event
|
||||
) {
|
||||
$this->request = $request;
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SidebarMenuEvent
|
||||
*/
|
||||
public function getMenu()
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
}
|
||||
42
src/Event/PrepareUserEvent.php
Normal file
42
src/Event/PrepareUserEvent.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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 App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if a user profile is loaded and we full data including dynamic user preferences
|
||||
*/
|
||||
class PrepareUserEvent extends Event
|
||||
{
|
||||
public const PREPARE = 'app.prepare_user';
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use App\Entity\UserPreference;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Class UserPreferenceEvent
|
||||
* This event should be used, if further user preferences should added dynamically
|
||||
*/
|
||||
class UserPreferenceEvent extends Event
|
||||
{
|
||||
@@ -30,7 +30,6 @@ class UserPreferenceEvent extends Event
|
||||
protected $preferences;
|
||||
|
||||
/**
|
||||
* UserPreferenceEvent constructor.
|
||||
* @param User $user
|
||||
* @param UserPreference[] $preferences
|
||||
*/
|
||||
@@ -41,6 +40,7 @@ class UserPreferenceEvent extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not set the preferences directly to the user object, but ONLY via addUserPreference()
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
|
||||
@@ -61,8 +61,6 @@ class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
public function onSetupNavbar(SidebarMenuEvent $event)
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
$isLoggedIn = $this->security->isGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
$isTeamlead = $isLoggedIn && $this->security->isGranted('ROLE_TEAMLEAD');
|
||||
|
||||
$event->addItem(
|
||||
new MenuItemModel('dashboard', 'menu.homepage', 'dashboard', [], 'fas fa-tachometer-alt')
|
||||
@@ -76,17 +74,18 @@ class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
)
|
||||
);
|
||||
|
||||
if ($isTeamlead) {
|
||||
$admin = new MenuItemModel('admin', 'menu.admin', '', [], 'fas fa-wrench');
|
||||
$event->addItem($admin);
|
||||
$admin = new MenuItemModel('admin', 'menu.admin', '', [], 'fas fa-wrench');
|
||||
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureAdminMenuEvent::CONFIGURE,
|
||||
new ConfigureAdminMenuEvent(
|
||||
$request,
|
||||
$event
|
||||
)
|
||||
);
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureAdminMenuEvent::CONFIGURE,
|
||||
new ConfigureAdminMenuEvent(
|
||||
$request,
|
||||
$admin
|
||||
)
|
||||
);
|
||||
|
||||
if ($admin->hasChildren()) {
|
||||
$event->addItem($admin);
|
||||
}
|
||||
|
||||
$this->activateByRoute(
|
||||
|
||||
@@ -53,26 +53,23 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
$auth = $this->security;
|
||||
|
||||
$isLoggedIn = $auth->isGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
$isUser = $isLoggedIn && $auth->isGranted('ROLE_USER');
|
||||
$isTeamlead = $isLoggedIn && $auth->isGranted('ROLE_TEAMLEAD');
|
||||
|
||||
if (!$isLoggedIn || !$isUser) {
|
||||
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $event->getMenu();
|
||||
$menu->addItem(
|
||||
new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], 'far fa-clock')
|
||||
);
|
||||
|
||||
if (!$isTeamlead) {
|
||||
return;
|
||||
if ($auth->isGranted('view_own_timesheet')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], 'far fa-clock')
|
||||
);
|
||||
}
|
||||
|
||||
$menu->addItem(
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
||||
);
|
||||
if ($auth->isGranted('view_invoice')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,33 +77,42 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
*/
|
||||
public function onAdminMenuConfigure(ConfigureAdminMenuEvent $event)
|
||||
{
|
||||
$menu = $event->getAdminMenu();
|
||||
$auth = $this->security;
|
||||
|
||||
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED') || !$auth->isGranted('ROLE_TEAMLEAD')) {
|
||||
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu->addChild(
|
||||
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'far fa-clock')
|
||||
);
|
||||
$menu = $event->getAdminMenu();
|
||||
|
||||
if (!$auth->isGranted('ROLE_ADMIN')) {
|
||||
return;
|
||||
if ($auth->isGranted('view_other_timesheet')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'far fa-clock')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fas fa-user')
|
||||
);
|
||||
}
|
||||
|
||||
$menu->addChild(
|
||||
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fas fa-users')
|
||||
)->addChild(
|
||||
new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], 'fas fa-project-diagram')
|
||||
)->addChild(
|
||||
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
||||
);
|
||||
if ($auth->isGranted('view_customer')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fas fa-users')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_project')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('project_admin', 'menu.admin_project', 'admin_project', [], 'fas fa-project-diagram')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_activity')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Event\UserPreferenceEvent;
|
||||
use App\Form\Type\CalendarViewType;
|
||||
use App\Form\Type\LanguageType;
|
||||
@@ -18,16 +19,11 @@ use App\Form\Type\SkinType;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
|
||||
use Symfony\Component\HttpKernel\Event\KernelEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
|
||||
/**
|
||||
* Class UserPreferenceSubscriber
|
||||
*/
|
||||
class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
@@ -35,20 +31,26 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $voter;
|
||||
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* UserPreferenceSubscriber constructor.
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param TokenStorageInterface $storage
|
||||
* @param AuthorizationCheckerInterface $voter
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage)
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage, AuthorizationCheckerInterface $voter)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->storage = $storage;
|
||||
$this->voter = $voter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,30 +59,40 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::CONTROLLER => ['loadUserPreferences', 200]
|
||||
PrepareUserEvent::PREPARE => ['loadUserPreferences', 200]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return UserPreference[]
|
||||
*/
|
||||
public function getDefaultPreferences()
|
||||
public function getDefaultPreferences(User $user)
|
||||
{
|
||||
return [
|
||||
(new UserPreference())
|
||||
->setName(UserPreference::HOURLY_RATE)
|
||||
->setValue(0)
|
||||
->setType(IntegerType::class)
|
||||
->addConstraint(new Range(['min' => 0])),
|
||||
/*
|
||||
$enableHourlyRate = false;
|
||||
|
||||
if ($this->voter->isGranted('hourly-rate', $user)) {
|
||||
$enableHourlyRate = true;
|
||||
}
|
||||
|
||||
/*
|
||||
(new UserPreference())
|
||||
->setName('timezone')
|
||||
->setValue(date_default_timezone_get())
|
||||
->setType(TimezoneType::class),
|
||||
*/
|
||||
*/
|
||||
|
||||
return [
|
||||
(new UserPreference())
|
||||
->setName(UserPreference::HOURLY_RATE)
|
||||
->setValue(0)
|
||||
->setType(MoneyType::class)
|
||||
->setEnabled($enableHourlyRate)
|
||||
->addConstraint(new Range(['min' => 0])),
|
||||
|
||||
(new UserPreference())
|
||||
->setName('language')
|
||||
->setValue('en') // TODO fetch from services.yaml
|
||||
->setValue('en')
|
||||
->setType(LanguageType::class),
|
||||
|
||||
(new UserPreference())
|
||||
@@ -116,31 +128,32 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
* @param PrepareUserEvent $event
|
||||
*/
|
||||
public function loadUserPreferences(KernelEvent $event)
|
||||
public function loadUserPreferences(PrepareUserEvent $event)
|
||||
{
|
||||
if (!$this->canHandleEvent($event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
$user = $event->getUser();
|
||||
|
||||
$prefs = [];
|
||||
foreach ($user->getPreferences() as $preference) {
|
||||
$prefs[$preference->getName()] = $preference;
|
||||
}
|
||||
|
||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences());
|
||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user));
|
||||
$this->eventDispatcher->dispatch(UserPreferenceEvent::CONFIGURE, $event);
|
||||
|
||||
foreach ($event->getPreferences() as $preference) {
|
||||
/* @var UserPreference[] $prefs */
|
||||
if (isset($prefs[$preference->getName()])) {
|
||||
/* @var UserPreference $pref */
|
||||
$prefs[$preference->getName()]
|
||||
->setType($preference->getType())
|
||||
->setConstraints($preference->getConstraints())
|
||||
->setEnabled($preference->isEnabled())
|
||||
;
|
||||
} else {
|
||||
$prefs[$preference->getName()] = $preference;
|
||||
@@ -151,24 +164,15 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
* @param PrepareUserEvent $event
|
||||
* @return bool
|
||||
*/
|
||||
protected function canHandleEvent(KernelEvent $event): bool
|
||||
protected function canHandleEvent(PrepareUserEvent $event): bool
|
||||
{
|
||||
// Ignore sub-requests
|
||||
if (!$event->isMasterRequest()) {
|
||||
if (null === ($user = $event->getUser())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore events like the toolbar where we do not have a token
|
||||
if (null === $this->storage->getToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
return ($user instanceof User);
|
||||
}
|
||||
}
|
||||
|
||||
89
src/EventSubscriber/UserProfileSubscriber.php
Normal file
89
src/EventSubscriber/UserProfileSubscriber.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\KernelEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class UserProfileSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param TokenStorageInterface $storage
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->storage = $storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::CONTROLLER => ['prepareUserProfile', 200]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
*/
|
||||
public function prepareUserProfile(KernelEvent $event)
|
||||
{
|
||||
if (!$this->canHandleEvent($event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
$event = new PrepareUserEvent($user);
|
||||
$this->eventDispatcher->dispatch(PrepareUserEvent::PREPARE, $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
* @return bool
|
||||
*/
|
||||
protected function canHandleEvent(KernelEvent $event): bool
|
||||
{
|
||||
// Ignore sub-requests
|
||||
if (!$event->isMasterRequest()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore events like the toolbar where we do not have a token
|
||||
if (null === $this->storage->getToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
return ($user instanceof User);
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,12 @@ use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -38,10 +40,12 @@ class ActivityEditForm extends AbstractType
|
||||
|
||||
$project = null;
|
||||
$customer = null;
|
||||
$currency = false;
|
||||
|
||||
if (null !== $entry->getProject()) {
|
||||
$project = $entry->getProject();
|
||||
$customer = $project->getCustomer();
|
||||
$currency = $customer->getCurrency();
|
||||
}
|
||||
|
||||
$builder
|
||||
@@ -59,27 +63,47 @@ class ActivityEditForm extends AbstractType
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->builderForEntityType($customer);
|
||||
},
|
||||
'data' => $customer ? $customer : null,
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_project',
|
||||
'data-api-url' => ['get_projects', ['customer' => '-s-']],
|
||||
],
|
||||
'project_enabled' => true,
|
||||
])
|
||||
->add('project', ProjectType::class, [
|
||||
'label' => 'label.project',
|
||||
'required' => false,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project) {
|
||||
return $repo->builderForEntityType($project);
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
|
||||
return $repo->builderForEntityType($project, $customer);
|
||||
},
|
||||
])
|
||||
->add('fixedRate', NumberType::class, [
|
||||
]);
|
||||
|
||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($project) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['customer']) || empty($data['customer'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data, $project) {
|
||||
return $repo->builderForEntityType($project, $data['customer']);
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
$builder
|
||||
->add('fixedRate', MoneyType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
->add('hourlyRate', MoneyType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
// boolean
|
||||
->add('visible', YesNoType::class, [
|
||||
@@ -87,7 +111,7 @@ class ActivityEditForm extends AbstractType
|
||||
])
|
||||
;
|
||||
|
||||
if ($entry->getId() === null) {
|
||||
if (null === $entry->getId()) {
|
||||
$builder->add('create_more', CheckboxType::class, [
|
||||
'label' => 'label.create_more',
|
||||
'required' => false,
|
||||
|
||||
@@ -15,7 +15,7 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -34,6 +34,9 @@ class CustomerEditForm extends AbstractType
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
/** @var Customer $customer */
|
||||
$customer = $options['data'];
|
||||
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'label.name',
|
||||
@@ -71,10 +74,12 @@ class CustomerEditForm extends AbstractType
|
||||
->add('fax', TelType::class, [
|
||||
'label' => 'label.fax',
|
||||
'required' => false,
|
||||
'attr' => ['icon' => 'fax'],
|
||||
])
|
||||
->add('mobile', TelType::class, [
|
||||
'label' => 'label.mobile',
|
||||
'required' => false,
|
||||
'attr' => ['icon' => 'mobile'],
|
||||
])
|
||||
->add('mail', EmailType::class, [
|
||||
'label' => 'label.email',
|
||||
@@ -87,13 +92,15 @@ class CustomerEditForm extends AbstractType
|
||||
->add('timezone', TimezoneType::class, [
|
||||
'label' => 'label.timezone',
|
||||
])
|
||||
->add('fixedRate', NumberType::class, [
|
||||
->add('fixedRate', MoneyType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
'currency' => $customer->getCurrency() ?? false,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
->add('hourlyRate', MoneyType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
'currency' => $customer->getCurrency() ?? false,
|
||||
])
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'label.visible',
|
||||
|
||||
94
src/Form/Extension/SelectWithApiDataExtension.php
Normal file
94
src/Form/Extension/SelectWithApiDataExtension.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Extension;
|
||||
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* Support Remote-API calls for Entity select-boxes.
|
||||
*/
|
||||
class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
{
|
||||
/**
|
||||
* @var UrlGeneratorInterface
|
||||
*/
|
||||
private $router;
|
||||
|
||||
/**
|
||||
* @param UrlGeneratorInterface $router
|
||||
*/
|
||||
public function __construct(UrlGeneratorInterface $router)
|
||||
{
|
||||
$this->router = $router;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getExtendedType()
|
||||
{
|
||||
return EntityType::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
if (!isset($options['api_data'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$apiData = $options['api_data'];
|
||||
|
||||
if (!is_array($apiData)) {
|
||||
throw new \InvalidArgumentException('Option "api_data" must be an array for form "' . $form->getName() . '"');
|
||||
}
|
||||
|
||||
if (!isset($apiData['select'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($apiData['route'])) {
|
||||
throw new \InvalidArgumentException('Missing "route" option for "api_data" option for form "' . $form->getName() . '"');
|
||||
}
|
||||
|
||||
if (!isset($apiData['route_params'])) {
|
||||
$apiData['route_params'] = [];
|
||||
}
|
||||
|
||||
$formPrefix = $form->getParent()->getName();
|
||||
if (!empty($formPrefix)) {
|
||||
$formPrefix .= '_';
|
||||
}
|
||||
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||
'data-related-select' => $formPrefix . $apiData['select'],
|
||||
'data-api-url' => $this->router->generate($apiData['route'], $apiData['route_params']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsResolver $resolver
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefined(['api_data']);
|
||||
$resolver->setAllowedTypes('api_data', 'array');
|
||||
//$resolver->setDefault('api_data', []);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ use App\Repository\CustomerRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -37,8 +36,11 @@ class ProjectEditForm extends AbstractType
|
||||
$entry = $options['data'];
|
||||
|
||||
$customer = null;
|
||||
$currency = false;
|
||||
|
||||
if ($entry->getId() !== null) {
|
||||
$customer = $entry->getCustomer();
|
||||
$currency = $customer->getCurrency();
|
||||
}
|
||||
|
||||
$builder
|
||||
@@ -59,18 +61,20 @@ class ProjectEditForm extends AbstractType
|
||||
return $repo->builderForEntityType($customer);
|
||||
},
|
||||
])
|
||||
->add('fixedRate', NumberType::class, [
|
||||
->add('fixedRate', MoneyType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
->add('hourlyRate', MoneyType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('budget', MoneyType::class, [
|
||||
'label' => 'label.budget',
|
||||
'currency' => $customer ? $customer->getCurrency() : $builder->getOption('currency'),
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'label.visible',
|
||||
|
||||
@@ -20,7 +20,7 @@ use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
@@ -32,6 +32,33 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
class TimesheetEditForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
private $customers;
|
||||
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $projects;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $durationOnly = false;
|
||||
|
||||
/**
|
||||
* @param CustomerRepository $customer
|
||||
* @param ProjectRepository $project
|
||||
* @param bool $durationOnly
|
||||
*/
|
||||
public function __construct(CustomerRepository $customer, ProjectRepository $project, bool $durationOnly)
|
||||
{
|
||||
$this->customers = $customer;
|
||||
$this->projects = $project;
|
||||
$this->durationOnly = $durationOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -40,6 +67,7 @@ class TimesheetEditForm extends AbstractType
|
||||
$activity = null;
|
||||
$project = null;
|
||||
$customer = null;
|
||||
$currency = false;
|
||||
$end = null;
|
||||
|
||||
if (isset($options['data'])) {
|
||||
@@ -48,12 +76,16 @@ class TimesheetEditForm extends AbstractType
|
||||
|
||||
$activity = $entry->getActivity();
|
||||
$project = $entry->getProject();
|
||||
$customer = null === $entry->getProject() ? null : $entry->getProject()->getCustomer();
|
||||
$customer = null === $project ? null : $project->getCustomer();
|
||||
|
||||
if (null === $project && null !== $activity) {
|
||||
$project = $activity->getProject();
|
||||
}
|
||||
|
||||
if (null !== $customer) {
|
||||
$currency = $customer->getCurrency();
|
||||
}
|
||||
|
||||
$end = $entry->getEnd();
|
||||
}
|
||||
|
||||
@@ -64,12 +96,14 @@ class TimesheetEditForm extends AbstractType
|
||||
'html5' => false,
|
||||
'format' => 'yyyy-MM-dd HH:mm',
|
||||
'with_seconds' => false,
|
||||
'attr' => ['autocomplete' => 'off', 'data-datetimepicker' => 'on'],
|
||||
'attr' => ['autocomplete' => 'off', 'data-datetimepicker' => 'on', 'placeholder' => 'yyyy-MM-dd HH:mm'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($options['duration_only']) {
|
||||
$builder->add('duration', DurationType::class);
|
||||
$builder->add('duration', DurationType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
} else {
|
||||
$builder->add('end', DateTimeType::class, [
|
||||
'label' => 'label.end',
|
||||
@@ -78,102 +112,127 @@ class TimesheetEditForm extends AbstractType
|
||||
'html5' => false,
|
||||
'format' => 'yyyy-MM-dd HH:mm',
|
||||
'with_seconds' => false,
|
||||
'attr' => ['autocomplete' => 'off', 'data-datetimepicker' => 'on'],
|
||||
'attr' => ['autocomplete' => 'off', 'data-datetimepicker' => 'on', 'placeholder' => 'yyyy-MM-dd HH:mm'],
|
||||
]);
|
||||
}
|
||||
|
||||
$projectOptions = [];
|
||||
|
||||
if ($this->customers->countCustomer(true) > 1) {
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
// documentation is for NelmioApiDocBundle
|
||||
'documentation' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Customer ID',
|
||||
],
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->builderForEntityType($customer);
|
||||
},
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
'placeholder' => null === $customer ? '' : null,
|
||||
'mapped' => false,
|
||||
'project_enabled' => true,
|
||||
]);
|
||||
} else {
|
||||
$projectOptions['group_by'] = null;
|
||||
}
|
||||
|
||||
if ($this->projects->countProject(true) > 1) {
|
||||
$projectOptions['placeholder'] = null;
|
||||
} else {
|
||||
$projectOptions['group_by'] = null;
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
// documentation is for NelmioApiDocBundle
|
||||
'documentation' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Customer ID',
|
||||
],
|
||||
'label' => 'label.customer',
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->builderForEntityType($customer);
|
||||
},
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_project',
|
||||
'data-api-url' => ['get_projects', ['customer' => '-s-']],
|
||||
],
|
||||
])
|
||||
->add('project', ProjectType::class, [
|
||||
->add(
|
||||
'project',
|
||||
ProjectType::class,
|
||||
array_merge($projectOptions, [
|
||||
'activity_enabled' => true,
|
||||
// documentation is for NelmioApiDocBundle
|
||||
'documentation' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Project ID',
|
||||
],
|
||||
'required' => true,
|
||||
'placeholder' => '',
|
||||
'label' => 'label.project',
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project) {
|
||||
return $repo->builderForEntityType($project);
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
|
||||
return $repo->builderForEntityType($project, $customer);
|
||||
},
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_activity',
|
||||
'data-api-url' => ['get_activities', ['project' => '-s-']],
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($project) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['customer']) || empty($data['customer'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'activity_enabled' => true,
|
||||
'group_by' => null,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data, $project) {
|
||||
return $repo->builderForEntityType($project, $data['customer']);
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
$builder
|
||||
->add('activity', ActivityType::class, [
|
||||
// documentation is for NelmioApiDocBundle
|
||||
'placeholder' => null,
|
||||
'documentation' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Activity ID',
|
||||
],
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity) {
|
||||
return $repo->builderForEntityType($activity);
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity, $project) {
|
||||
return $repo->builderForEntityType($activity, $project);
|
||||
},
|
||||
])
|
||||
;
|
||||
|
||||
// replaces the activity select after submission, to make sure only activities for the selected project are displayed
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($activity) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['project']) || empty($data['project'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('activity', ActivityType::class, [
|
||||
'placeholder' => null,
|
||||
'query_builder' => function (ActivityRepository $repo) use ($data, $activity) {
|
||||
return $repo->builderForEntityType($activity, $data['project']);
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
$builder
|
||||
->add('description', TextareaType::class, [
|
||||
'label' => 'label.description',
|
||||
'required' => false,
|
||||
])
|
||||
->add('fixedRate', NumberType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
/*
|
||||
$builder->get('customer')->addEventListener(
|
||||
FormEvents::POST_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$customer = $event->getForm()->getData();
|
||||
$event->getForm()->getParent()->add('project', ProjectType::class, [
|
||||
'required' => true,
|
||||
'placeholder' => '',
|
||||
'label' => 'label.project',
|
||||
'query_builder' => function (ProjectRepository $repo) use ($customer) {
|
||||
return $repo->builderForEntityType(null, $customer);
|
||||
},
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_activity',
|
||||
'data-api-url' => ['get_activities', ['project' => '-s-']],
|
||||
],
|
||||
|
||||
if ($options['include_rate']) {
|
||||
$builder
|
||||
->add('fixedRate', MoneyType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
])
|
||||
->add('hourlyRate', MoneyType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
'currency' => $currency,
|
||||
]);
|
||||
}
|
||||
);
|
||||
*/
|
||||
$builder->get('project')->addEventListener(
|
||||
FormEvents::POST_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$project = $event->getForm()->getData();
|
||||
$event->getForm()->getParent()->add('activity', ActivityType::class, [
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($project) {
|
||||
return $repo->builderForEntityType(null, $project);
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class);
|
||||
@@ -190,9 +249,11 @@ class TimesheetEditForm extends AbstractType
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'timesheet_edit',
|
||||
'duration_only' => false,
|
||||
'duration_only' => $this->durationOnly,
|
||||
'include_user' => false,
|
||||
'include_rate' => true,
|
||||
'docu_chapter' => 'timesheet',
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Form\Type\BeginDateType;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\EndDateType;
|
||||
use App\Form\Type\PageSizeType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\UserRoleType;
|
||||
@@ -23,8 +25,7 @@ use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
@@ -65,10 +66,13 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
{
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'project_enabled' => true,
|
||||
'project_visibility' => ProjectQuery::SHOW_BOTH,
|
||||
'query_builder' => function (CustomerRepository $repo) {
|
||||
$query = new CustomerQuery();
|
||||
$query->setVisibility(CustomerQuery::SHOW_BOTH); // this field is the reason for the query here
|
||||
$query->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
@@ -77,10 +81,15 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param null|string $label
|
||||
*/
|
||||
protected function addVisibilityChoice(FormBuilderInterface $builder)
|
||||
protected function addVisibilityChoice(FormBuilderInterface $builder, ?string $label = null)
|
||||
{
|
||||
$builder->add('visibility', VisibilityType::class, []);
|
||||
$builder->add('visibility', VisibilityType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'label' => $label
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,14 +117,8 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
*/
|
||||
protected function addStartDateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('begin', DateType::class, [
|
||||
'label' => 'label.begin',
|
||||
'widget' => 'single_text',
|
||||
'html5' => false,
|
||||
$builder->add('begin', BeginDateType::class, [
|
||||
'required' => false,
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'attr' => ['autocomplete' => 'off', 'data-datepicker' => 'on'],
|
||||
'empty_data' => (new \DateTime('first day of this month'))->format('Y-M-d')
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -124,14 +127,8 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
*/
|
||||
protected function addEndDateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('end', DateType::class, [
|
||||
'label' => 'label.end',
|
||||
'widget' => 'single_text',
|
||||
'html5' => false,
|
||||
$builder->add('end', EndDateType::class, [
|
||||
'required' => false,
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'attr' => ['autocomplete' => 'off', 'data-datepicker' => 'on'],
|
||||
'empty_data' => (new \DateTime('last day of this month'))->format('Y-M-d')
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -140,10 +137,11 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
*/
|
||||
protected function addProjectChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('project', ChoiceType::class, [
|
||||
'group_by' => null,
|
||||
$builder->add('project', ProjectType::class, [
|
||||
'required' => false,
|
||||
'label' => 'label.project',
|
||||
'activity_enabled' => true,
|
||||
'activity_visibility' => ActivityQuery::SHOW_BOTH,
|
||||
'choices' => [],
|
||||
]);
|
||||
|
||||
$builder->addEventListener(
|
||||
@@ -157,11 +155,14 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
$event->getForm()->add('project', ProjectType::class, [
|
||||
'group_by' => null,
|
||||
'required' => false,
|
||||
'activity_enabled' => true,
|
||||
'activity_visibility' => ActivityQuery::SHOW_BOTH,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data) {
|
||||
$query = new ProjectQuery();
|
||||
$query->setCustomer($data['customer']);
|
||||
$query->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->setVisibility(ProjectQuery::SHOW_BOTH);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
@@ -176,7 +177,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
protected function addActivityChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('activity', ActivityType::class, [
|
||||
'group_by' => null,
|
||||
'required' => false,
|
||||
'query_builder' => function (ActivityRepository $repo) {
|
||||
$query = new ActivityQuery();
|
||||
@@ -184,6 +184,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
$query->setGlobalsOnly(true);
|
||||
$query->setOrderGlobalsFirst(true);
|
||||
$query->setVisibility(ActivityQuery::SHOW_BOTH);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
@@ -198,7 +199,6 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
}
|
||||
|
||||
$event->getForm()->add('activity', ActivityType::class, [
|
||||
'group_by' => null,
|
||||
'required' => false,
|
||||
'query_builder' => function (ActivityRepository $repo) use ($data) {
|
||||
$query = new ActivityQuery();
|
||||
@@ -206,6 +206,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
$query->setProject($data['project']);
|
||||
$query->setOrderGlobalsFirst(true);
|
||||
$query->setVisibility(ActivityQuery::SHOW_BOTH);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
@@ -213,4 +214,14 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addHiddenPagination(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('page', HiddenType::class, [
|
||||
'empty_data' => 1
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class ActivityToolbarForm extends AbstractToolbarForm
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ class CustomerToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,8 @@ class InvoiceToolbarForm extends AbstractToolbarForm
|
||||
protected function addTemplateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('template', InvoiceTemplateType::class, [
|
||||
'required' => true
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class ProjectToolbarForm extends AbstractToolbarForm
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,5 +29,6 @@ class TimesheetAdminToolbarForm extends TimesheetToolbarForm
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +41,8 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
'label' => 'label.entryState',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
|
||||
@@ -24,8 +24,9 @@ class UserToolbarForm extends AbstractToolbarForm
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addVisibilityChoice($builder, 'label.active');
|
||||
$this->addUserRoleChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
50
src/Form/Type/BeginDateType.php
Normal file
50
src/Form/Type/BeginDateType.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to display the begin-date input field.
|
||||
*/
|
||||
class BeginDateType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.begin',
|
||||
'widget' => 'single_text',
|
||||
'html5' => false,
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('attr', function (Options $options) {
|
||||
return [
|
||||
'autocomplete' => 'off',
|
||||
'data-datepicker' => 'on',
|
||||
'placeholder' => $options['format'],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return DateType::class;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ namespace App\Form\Type;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -32,8 +34,22 @@ class CustomerType extends AbstractType
|
||||
'query_builder' => function (CustomerRepository $repo) {
|
||||
return $repo->builderForEntityType(null);
|
||||
},
|
||||
'project_enabled' => false,
|
||||
'project_visibility' => ProjectQuery::SHOW_VISIBLE,
|
||||
//'attr' => ['class' => 'selectpicker', 'data-size' => 10, 'data-live-search' => true, 'data-width' => '100%']
|
||||
]);
|
||||
|
||||
$resolver->setDefault('api_data', function (Options $options) {
|
||||
if (true === $options['project_enabled']) {
|
||||
return [
|
||||
'select' => 'project',
|
||||
'route' => 'get_projects',
|
||||
'route_params' => ['customer' => '-s-', 'orderBy' => 'name', 'visible' => $options['project_visibility']],
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
50
src/Form/Type/EndDateType.php
Normal file
50
src/Form/Type/EndDateType.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to display the end-date input field.
|
||||
*/
|
||||
class EndDateType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.end',
|
||||
'widget' => 'single_text',
|
||||
'html5' => false,
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('attr', function (Options $options) {
|
||||
return [
|
||||
'autocomplete' => 'off',
|
||||
'data-datepicker' => 'on',
|
||||
'placeholder' => $options['format'],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return DateType::class;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class PageSizeType extends AbstractType
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.pageSize',
|
||||
'choices' => [10 => 10, 25 => 25, 50 => 50, 75 => 75, 100 => 100],
|
||||
//'attr' => ['class' => 'selectpicker', 'data-live-search' => false, 'data-width' => '100%']
|
||||
'placeholder' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ namespace App\Form\Type;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -57,8 +59,22 @@ class ProjectType extends AbstractType
|
||||
'query_builder' => function (ProjectRepository $repo) {
|
||||
return $repo->builderForEntityType(null);
|
||||
},
|
||||
'activity_enabled' => false,
|
||||
'activity_visibility' => ActivityQuery::SHOW_VISIBLE,
|
||||
//'attr' => ['class' => 'selectpicker', 'data-size' => 10, 'data-live-search' => true, 'data-width' => '100%']
|
||||
]);
|
||||
|
||||
$resolver->setDefault('api_data', function (Options $options) {
|
||||
if (true === $options['activity_enabled']) {
|
||||
return [
|
||||
'select' => 'activity',
|
||||
'route' => 'get_activities',
|
||||
'route_params' => ['project' => '-s-', 'orderBy' => 'name', 'visible' => $options['activity_visibility']],
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,23 +35,31 @@ class UserPreferenceType extends AbstractType
|
||||
/** @var UserPreference $preference */
|
||||
$preference = $event->getData();
|
||||
|
||||
if ($preference instanceof UserPreference) {
|
||||
// prevents unconfigured values from showing up in the form
|
||||
if ($preference->getType() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$required = true;
|
||||
if (CheckboxType::class == $preference->getType()) {
|
||||
$required = false;
|
||||
}
|
||||
|
||||
$event->getForm()->add('value', $preference->getType(), [
|
||||
'label' => 'label.' . $preference->getName(),
|
||||
'constraints' => $preference->getConstraints(),
|
||||
'required' => $required,
|
||||
]);
|
||||
if (!($preference instanceof UserPreference)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevents unconfigured values from showing up in the form
|
||||
if ($preference->getType() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$required = true;
|
||||
if (CheckboxType::class == $preference->getType()) {
|
||||
$required = false;
|
||||
}
|
||||
|
||||
$type = $preference->getType();
|
||||
if (!$preference->isEnabled()) {
|
||||
$type = HiddenType::class;
|
||||
}
|
||||
|
||||
$event->getForm()->add('value', $type, [
|
||||
'label' => 'label.' . $preference->getName(),
|
||||
'constraints' => $preference->getConstraints(),
|
||||
'required' => $required,
|
||||
'disabled' => !$preference->isEnabled(),
|
||||
]);
|
||||
}
|
||||
);
|
||||
$builder->add('name', HiddenType::class);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -24,9 +24,9 @@ class YesNoType extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'choices' => ['yes' => true, 'no' => false],
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'value' => true,
|
||||
'false_values' => [null, 0, false],
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,6 @@ class YesNoType extends AbstractType
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return ChoiceType::class;
|
||||
return CheckboxType::class;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Form;
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@@ -27,26 +28,21 @@ class UserEditType extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
// string - length 160
|
||||
->add('alias', TextType::class, [
|
||||
'label' => 'label.alias',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 50
|
||||
->add('title', TextType::class, [
|
||||
'label' => 'label.title',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 255
|
||||
->add('avatar', TextType::class, [
|
||||
'label' => 'label.avatar',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 160
|
||||
->add('email', TextType::class, [
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'label.email',
|
||||
])
|
||||
// boolean
|
||||
->add('enabled', YesNoType::class, [
|
||||
'label' => 'label.active',
|
||||
])
|
||||
|
||||
@@ -44,6 +44,10 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
|
||||
$timesheet->setActivity($entry->getActivity());
|
||||
}
|
||||
|
||||
if (null === $timesheet->getProject()) {
|
||||
$timesheet->setProject($entry->getProject());
|
||||
}
|
||||
|
||||
if (empty($timesheet->getDescription())) {
|
||||
$timesheet->setDescription($entry->getActivity()->getName());
|
||||
}
|
||||
|
||||
@@ -12,33 +12,14 @@ namespace App\Invoice\Renderer;
|
||||
use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Model\InvoiceModel;
|
||||
use PhpOffice\PhpWord\PhpWord;
|
||||
use PhpOffice\PhpWord\Escaper\Xml;
|
||||
use PhpOffice\PhpWord\Exception\Exception as OfficeException;
|
||||
use PhpOffice\PhpWord\TemplateProcessor;
|
||||
use Symfony\Component\HttpFoundation\File\Stream;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DocxRenderer extends AbstractRenderer implements RendererInterface
|
||||
{
|
||||
/*
|
||||
protected function setPhpWordOptions(PhpWord $phpWord)
|
||||
{
|
||||
if (!extension_loaded('zip')) {
|
||||
\PhpOffice\PhpWord\Settings::setZipClass(\PhpOffice\PhpWord\Settings::PCLZIP);
|
||||
}
|
||||
|
||||
// \PhpOffice\PhpWord\Settings::setPdfRendererPath(__DIR__ . '/../../vendor/tecnickcom/tcpdf/');
|
||||
// \PhpOffice\PhpWord\Settings::setPdfRendererName(\PhpOffice\PhpWord\Settings::PDF_RENDERER_TCPDF);
|
||||
// \PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(true);
|
||||
// $phpWord->getSettings()->setThemeFontLang(new Language(Language::EN_US));
|
||||
|
||||
$properties = $phpWord->getDocInfo();
|
||||
$properties->setCreator('Kimai 2');
|
||||
$properties->setDescription('Created with Kimai 2, the open-source time-tracking software! Get more information at www.kimai.org.');
|
||||
$properties->setCreated(time());
|
||||
$properties->setModified(time());
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param InvoiceDocument $document
|
||||
* @param InvoiceModel $model
|
||||
@@ -46,18 +27,33 @@ class DocxRenderer extends AbstractRenderer implements RendererInterface
|
||||
*/
|
||||
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
||||
{
|
||||
\PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(false);
|
||||
|
||||
$filename = basename($document->getFilename());
|
||||
|
||||
$xmlEscaper = new Xml();
|
||||
$template = new TemplateProcessor($document->getFilename());
|
||||
foreach ($this->modelToReplacer($model) as $key => $value) {
|
||||
$template->setValue($key, $value);
|
||||
|
||||
foreach ($this->modelToReplacer($model) as $search => $replace) {
|
||||
$replace = $xmlEscaper->escape($replace);
|
||||
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
||||
|
||||
$template->setValue($search, $replace);
|
||||
}
|
||||
|
||||
try {
|
||||
$template->cloneRow('entry.description', count($model->getCalculator()->getEntries()));
|
||||
} catch (OfficeException $ex) {
|
||||
$template->cloneRow('entry.row', count($model->getCalculator()->getEntries()));
|
||||
}
|
||||
|
||||
$template->cloneRow('entry.description', count($model->getCalculator()->getEntries()));
|
||||
$i = 1;
|
||||
foreach ($model->getCalculator()->getEntries() as $entry) {
|
||||
$values = $this->timesheetToArray($entry);
|
||||
foreach ($values as $search => $replace) {
|
||||
$replace = $xmlEscaper->escape($replace);
|
||||
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
||||
|
||||
$template->setValue($search . '#' . $i, $replace);
|
||||
}
|
||||
$i++;
|
||||
|
||||
@@ -73,7 +73,10 @@ trait RendererTrait
|
||||
*/
|
||||
protected function modelToReplacer(InvoiceModel $model)
|
||||
{
|
||||
return [
|
||||
$customer = $model->getCustomer();
|
||||
$project = $model->getQuery()->getProject();
|
||||
|
||||
$values = [
|
||||
'invoice.due_date' => $this->getFormattedDateTime($model->getDueDate()),
|
||||
'invoice.date' => $this->getFormattedDateTime($model->getInvoiceDate()),
|
||||
'invoice.number' => $model->getNumberGenerator()->getInvoiceNumber(),
|
||||
@@ -95,16 +98,32 @@ trait RendererTrait
|
||||
'query.end' => $this->getFormattedDateTime($model->getQuery()->getEnd()),
|
||||
'query.month' => $this->getFormattedMonthName($model->getQuery()->getBegin()),
|
||||
'query.year' => $model->getQuery()->getBegin()->format('Y'),
|
||||
|
||||
'customer.address' => $model->getCustomer()->getAddress(),
|
||||
'customer.name' => $model->getCustomer()->getName(),
|
||||
'customer.contact' => $model->getCustomer()->getContact(),
|
||||
'customer.company' => $model->getCustomer()->getCompany(),
|
||||
'customer.number' => $model->getCustomer()->getNumber(),
|
||||
'customer.country' => $model->getCustomer()->getCountry(),
|
||||
'customer.homepage' => $model->getCustomer()->getHomepage(),
|
||||
'customer.comment' => $model->getCustomer()->getComment(),
|
||||
];
|
||||
|
||||
if (null !== $project) {
|
||||
$values = array_merge($values, [
|
||||
'project.id' => $project->getId(),
|
||||
'project.name' => $project->getName(),
|
||||
'project.comment' => $project->getComment(),
|
||||
'project.order_number' => $project->getOrderNumber(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (null !== $customer) {
|
||||
$values = array_merge($values, [
|
||||
'customer.id' => $customer->getId(),
|
||||
'customer.address' => $customer->getAddress(),
|
||||
'customer.name' => $customer->getName(),
|
||||
'customer.contact' => $customer->getContact(),
|
||||
'customer.company' => $customer->getCompany(),
|
||||
'customer.number' => $customer->getNumber(),
|
||||
'customer.country' => $customer->getCountry(),
|
||||
'customer.homepage' => $customer->getHomepage(),
|
||||
'customer.comment' => $customer->getComment(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,6 +161,7 @@ trait RendererTrait
|
||||
$end = $timesheet->getEnd();
|
||||
|
||||
return [
|
||||
'entry.row' => '',
|
||||
'entry.description' => $description,
|
||||
'entry.amount' => $amount,
|
||||
'entry.rate' => $this->getFormattedMoney($hourlyRate),
|
||||
@@ -157,6 +177,7 @@ trait RendererTrait
|
||||
'entry.date' => $this->getFormattedDateTime($begin),
|
||||
'entry.user_id' => $user->getId(),
|
||||
'entry.user_name' => $user->getUsername(),
|
||||
'entry.user_title' => $user->getTitle(),
|
||||
'entry.user_alias' => $user->getAlias(),
|
||||
'entry.activity' => $activity->getName(),
|
||||
'entry.activity_id' => $activity->getId(),
|
||||
|
||||
@@ -56,10 +56,11 @@ final class Version20181031220003 extends AbstractMigration
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
} else {
|
||||
// project table
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3');
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT NOT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
|
||||
// timesheet table
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD project_id INT DEFAULT NULL AFTER activity_id');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
}
|
||||
|
||||
@@ -80,7 +81,12 @@ final class Version20181031220003 extends AbstractMigration
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
} else {
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B181C06096');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +100,7 @@ final class Version20181031220003 extends AbstractMigration
|
||||
|
||||
$timesheet = $this->getTableName('timesheet');
|
||||
$projects = $this->getTableName('projects');
|
||||
$customers = $this->getTableName('customers');
|
||||
|
||||
if ($platform === 'sqlite') {
|
||||
// project table
|
||||
@@ -117,7 +124,9 @@ final class Version20181031220003 extends AbstractMigration
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
|
||||
} else {
|
||||
// project table
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' DROP FOREIGN KEY FK_407F12069395C3F3');
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' ADD CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE');
|
||||
// timesheet table
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B1166D1F9C');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet);
|
||||
|
||||
@@ -123,6 +123,7 @@ class ActivityRepository extends AbstractRepository
|
||||
$query->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->setProject($project);
|
||||
$query->setOrderGlobalsFirst(true);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
if (null === $activity && $project === null) {
|
||||
$query->setGlobalsOnly(true);
|
||||
@@ -142,8 +143,13 @@ class ActivityRepository extends AbstractRepository
|
||||
$qb->select('a', 'p', 'c')
|
||||
->from(Activity::class, 'a')
|
||||
->leftJoin('a.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->orderBy('a.' . $query->getOrderBy(), $query->getOrder());
|
||||
->leftJoin('p.customer', 'c');
|
||||
|
||||
if ($query->isOrderGlobalsFirst()) {
|
||||
$qb->orderBy('a.project', 'ASC');
|
||||
}
|
||||
|
||||
$qb->addOrderBy('a.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
$where = $qb->expr()->andX();
|
||||
|
||||
@@ -202,12 +208,10 @@ class ActivityRepository extends AbstractRepository
|
||||
$qb->setParameter('activity', $entity);
|
||||
}
|
||||
|
||||
if ($query->isOrderGlobalsFirst()) {
|
||||
$qb->orderBy('a.project', 'ASC');
|
||||
if ($or->count() > 0) {
|
||||
$qb->andWhere($or);
|
||||
}
|
||||
|
||||
$qb->andWhere($or);
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,10 +35,15 @@ class CustomerRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|bool $visible
|
||||
* @return int
|
||||
*/
|
||||
public function countCustomer()
|
||||
public function countCustomer($visible = null)
|
||||
{
|
||||
if (null !== $visible) {
|
||||
return $this->count(['visible' => (int) $visible]);
|
||||
}
|
||||
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
@@ -94,6 +99,7 @@ class CustomerRepository extends AbstractRepository
|
||||
$query = new CustomerQuery();
|
||||
$query->setHiddenEntity($entity);
|
||||
$query->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
return $this->findByQuery($query);
|
||||
}
|
||||
|
||||
@@ -35,10 +35,15 @@ class ProjectRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|bool $visible
|
||||
* @return int
|
||||
*/
|
||||
public function countProject()
|
||||
public function countProject($visible = null)
|
||||
{
|
||||
if (null !== $visible) {
|
||||
return $this->count(['visible' => (int) $visible]);
|
||||
}
|
||||
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
@@ -80,16 +85,17 @@ class ProjectRepository extends AbstractRepository
|
||||
/**
|
||||
* Returns a query builder that is used for ProjectType and your own 'query_builder' option.
|
||||
*
|
||||
* @param Project|null $entity
|
||||
* @param Customer|null $customer
|
||||
* @param Project|int|null $entity
|
||||
* @param Customer|int|null $customer
|
||||
* @return array|QueryBuilder|Pagerfanta
|
||||
*/
|
||||
public function builderForEntityType(Project $entity = null, Customer $customer = null)
|
||||
public function builderForEntityType($entity = null, $customer = null)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setHiddenEntity($entity);
|
||||
$query->setCustomer($customer);
|
||||
$query->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->setOrderBy('name');
|
||||
|
||||
return $this->findByQuery($query);
|
||||
}
|
||||
@@ -115,7 +121,6 @@ class ProjectRepository extends AbstractRepository
|
||||
}
|
||||
$qb->andWhere('p.visible = 1');
|
||||
|
||||
/** @var Project $entity */
|
||||
$entity = $query->getHiddenEntity();
|
||||
if (null !== $entity) {
|
||||
$qb->orWhere('p.id = :project')->setParameter('project', $entity);
|
||||
|
||||
@@ -260,6 +260,42 @@ class TimesheetRepository extends AbstractRepository
|
||||
return $qb->getQuery()->execute($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param int $limit
|
||||
* @return int
|
||||
* @throws RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function stopActiveEntries(User $user, int $hardLimit)
|
||||
{
|
||||
$counter = 0;
|
||||
$activeEntries = $this->getActiveEntries($user);
|
||||
|
||||
// reduce limit by one:
|
||||
// this method is only called when a new entry is started
|
||||
// -> all entries, including the new one must not exceed the $limit
|
||||
$limit = $hardLimit - 1;
|
||||
|
||||
if (count($activeEntries) > $limit) {
|
||||
$i = 1;
|
||||
foreach ($activeEntries as $activeEntry) {
|
||||
if ($i > $limit) {
|
||||
if ($hardLimit > 1) {
|
||||
throw new \Exception('timesheet.start.exceeded_limit');
|
||||
}
|
||||
|
||||
$this->stopRecording($activeEntry);
|
||||
$counter++;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetQuery $query
|
||||
* @return QueryBuilder|Pagerfanta|array
|
||||
@@ -281,19 +317,24 @@ class TimesheetRepository extends AbstractRepository
|
||||
->setParameter('user', $query->getUser());
|
||||
}
|
||||
|
||||
if (TimesheetQuery::STATE_RUNNING == $query->getState()) {
|
||||
$qb->andWhere($qb->expr()->isNull('t.end'));
|
||||
} elseif (TimesheetQuery::STATE_STOPPED == $query->getState()) {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
}
|
||||
|
||||
if (null !== $query->getBegin()) {
|
||||
$qb->andWhere('t.begin >= :begin')
|
||||
->setParameter('begin', $query->getBegin());
|
||||
}
|
||||
|
||||
if (null !== $query->getEnd()) {
|
||||
$qb->andWhere('t.end <= :end')
|
||||
if (TimesheetQuery::STATE_RUNNING == $query->getState()) {
|
||||
$qb->andWhere($qb->expr()->isNull('t.end'));
|
||||
}
|
||||
|
||||
if (TimesheetQuery::STATE_STOPPED == $query->getState()) {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
|
||||
if (null !== $query->getEnd()) {
|
||||
$qb->andWhere('t.end <= :end')
|
||||
->setParameter('end', $query->getEnd());
|
||||
}
|
||||
} elseif (null !== $query->getEnd()) {
|
||||
$qb->andWhere('t.begin <= :end')
|
||||
->setParameter('end', $query->getEnd());
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,19 @@ class UserRepository extends AbstractRepository implements UserLoaderInterface
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwritten to fetch preferences when using the Profile controller actions.
|
||||
* Depends on the query, some magic mechanisms like the ParamConverter will use this method to fetch the user.
|
||||
*/
|
||||
public function findOneBy(array $criteria, array $orderBy = null)
|
||||
{
|
||||
if (count($criteria) == 1 && isset($criteria['username'])) {
|
||||
return $this->loadUserByUsername($criteria['username']);
|
||||
}
|
||||
|
||||
return parent::findOneBy($criteria, $orderBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
@@ -43,7 +56,6 @@ class UserRepository extends AbstractRepository implements UserLoaderInterface
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
// if we join activities, the maxperpage limit will limit the list to the amount or projects + activties
|
||||
$qb->select('u')
|
||||
->from(User::class, 'u')
|
||||
->orderBy('u.' . $query->getOrderBy(), $query->getOrder());
|
||||
@@ -77,6 +89,8 @@ class UserRepository extends AbstractRepository implements UserLoaderInterface
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->select('u', 'p')
|
||||
->leftJoin('u.preferences', 'p')
|
||||
->where('u.username = :username')
|
||||
->orWhere('u.email = :username')
|
||||
->setParameter('username', $username)
|
||||
|
||||
67
src/Security/RolePermissionManager.php
Normal file
67
src/Security/RolePermissionManager.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?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\Security;
|
||||
|
||||
class RolePermissionManager
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $knownPermissions = [];
|
||||
|
||||
/**
|
||||
* @param array $permissions
|
||||
*/
|
||||
public function __construct(array $permissions)
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
|
||||
foreach ($permissions as $role => $perms) {
|
||||
$this->knownPermissions = array_merge($this->knownPermissions, $perms);
|
||||
}
|
||||
$this->knownPermissions = array_unique($this->knownPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function isRegisteredPermission($permission)
|
||||
{
|
||||
return in_array($permission, $this->knownPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @return bool
|
||||
*/
|
||||
public function roleHasPermission($role)
|
||||
{
|
||||
return isset($this->permissions[$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($role, $permission)
|
||||
{
|
||||
if (!isset($this->permissions[$role])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, $this->permissions[$role]);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ class RateCalculator implements CalculatorInterface
|
||||
public function calculate(Timesheet $record)
|
||||
{
|
||||
if (null === $record->getEnd()) {
|
||||
$record->setRate(0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,10 @@ class Extensions extends \Twig_Extension
|
||||
}
|
||||
}
|
||||
|
||||
if ($seconds < 0) {
|
||||
return '?';
|
||||
}
|
||||
|
||||
$locale = $this->getLocale();
|
||||
switch ($format) {
|
||||
case 'full':
|
||||
|
||||
@@ -30,6 +30,10 @@ class Duration
|
||||
*/
|
||||
public function format($seconds, $format = self::FORMAT_NO_SECONDS)
|
||||
{
|
||||
if (null === $seconds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hour = floor($seconds / 3600);
|
||||
$minute = floor(($seconds / 60) % 60);
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
|
||||
namespace App\Voter;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\AclDecisionManager;
|
||||
use App\Security\RolePermissionManager;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
* Abstract voter to help with checking user roles.
|
||||
* Abstract voter to help with checking user permissions.
|
||||
*/
|
||||
abstract class AbstractVoter extends Voter
|
||||
{
|
||||
@@ -22,14 +24,19 @@ abstract class AbstractVoter extends Voter
|
||||
* @var AclDecisionManager
|
||||
*/
|
||||
protected $decisionManager;
|
||||
/**
|
||||
* @var RolePermissionManager
|
||||
*/
|
||||
protected $roleManager;
|
||||
|
||||
/**
|
||||
* AbstractVoter constructor.
|
||||
* @param AclDecisionManager $decisionManager
|
||||
* @param RolePermissionManager $roleManager
|
||||
*/
|
||||
public function __construct(AclDecisionManager $decisionManager)
|
||||
public function __construct(AclDecisionManager $decisionManager, RolePermissionManager $roleManager)
|
||||
{
|
||||
$this->decisionManager = $decisionManager;
|
||||
$this->roleManager = $roleManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,4 +57,39 @@ abstract class AbstractVoter extends Voter
|
||||
{
|
||||
return $this->decisionManager->hasRole($token, [$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasPermission($role, $permission)
|
||||
{
|
||||
return $this->roleManager->hasPermission($role, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasRolePermission(User $user, $permission)
|
||||
{
|
||||
foreach ($user->getRoles() as $role) {
|
||||
if ($this->hasPermission($role, $permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function isRegisteredPermission($permission)
|
||||
{
|
||||
return $this->roleManager->isRegisteredPermission($permission);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ class ActivityVoter extends AbstractVoter
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Activity)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
@@ -30,16 +33,16 @@ class ActivityVoter extends AbstractVoter
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @param Activity $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$subject instanceof Activity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Activity) {
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -60,54 +63,10 @@ class ActivityVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($attribute) {
|
||||
case self::VIEW:
|
||||
return $this->canView($subject, $user, $token);
|
||||
case self::EDIT:
|
||||
return $this->canEdit($subject, $user, $token);
|
||||
case self::DELETE:
|
||||
return $this->canDelete($token);
|
||||
if ($subject instanceof Activity) {
|
||||
return $this->hasRolePermission($user, $attribute . '_activity');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(Activity $activity, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canEdit($activity, $user, $token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(Activity $activity, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canDelete($token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(TokenInterface $token)
|
||||
{
|
||||
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ class CustomerVoter extends AbstractVoter
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Customer)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
@@ -30,16 +33,16 @@ class CustomerVoter extends AbstractVoter
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @param Customer $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$subject instanceof Customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Customer) {
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -60,54 +63,10 @@ class CustomerVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($attribute) {
|
||||
case self::VIEW:
|
||||
return $this->canView($subject, $user, $token);
|
||||
case self::EDIT:
|
||||
return $this->canEdit($subject, $user, $token);
|
||||
case self::DELETE:
|
||||
return $this->canDelete($token);
|
||||
if ($subject instanceof Customer) {
|
||||
return $this->hasRolePermission($user, $attribute . '_customer');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(Customer $customer, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canEdit($customer, $user, $token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(Customer $customer, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canDelete($token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(TokenInterface $token)
|
||||
{
|
||||
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
|
||||
}
|
||||
}
|
||||
|
||||
72
src/Voter/InvoiceTemplateVoter.php
Normal file
72
src/Voter/InvoiceTemplateVoter.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check permissions on InvoiceTemplateVote.
|
||||
*/
|
||||
class InvoiceTemplateVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: InvoiceTemplate)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::DELETE
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param InvoiceTemplate $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!$subject instanceof InvoiceTemplate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param InvoiceTemplate $subject
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subject instanceof InvoiceTemplate) {
|
||||
return $this->hasRolePermission($user, $attribute . '_invoice_template');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +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\Voter;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check permissions on Invoices.
|
||||
*/
|
||||
class InvoiceVoter extends AbstractVoter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const CREATE = 'create';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::CREATE,
|
||||
self::EDIT,
|
||||
self::DELETE
|
||||
];
|
||||
|
||||
public const ALLOWED_SUBJECTS = [
|
||||
'invoice',
|
||||
'invoice_template'
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof InvoiceTemplate) {
|
||||
if (!is_string($subject) || !in_array($subject, self::ALLOWED_SUBJECTS)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param string|InvoiceTemplate $subject
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($attribute) {
|
||||
case self::VIEW:
|
||||
return $this->canView($user, $token);
|
||||
case self::CREATE:
|
||||
return $this->canCreate($user, $token);
|
||||
case self::EDIT:
|
||||
return $this->canEdit($user, $token);
|
||||
case self::DELETE:
|
||||
return $this->canDelete($token);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canEdit($user, $token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canCreate(User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canDelete($token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canDelete($token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(TokenInterface $token)
|
||||
{
|
||||
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_TEAMLEAD', $token);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ class ProjectVoter extends AbstractVoter
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Project)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
@@ -30,16 +33,16 @@ class ProjectVoter extends AbstractVoter
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @param Project $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$subject instanceof Project) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Project) {
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -60,54 +63,10 @@ class ProjectVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($attribute) {
|
||||
case self::VIEW:
|
||||
return $this->canView($subject, $user, $token);
|
||||
case self::EDIT:
|
||||
return $this->canEdit($subject, $user, $token);
|
||||
case self::DELETE:
|
||||
return $this->canDelete($token);
|
||||
if ($subject instanceof Project) {
|
||||
return $this->hasRolePermission($user, $attribute . '_project');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(Project $project, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canEdit($project, $user, $token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(Project $project, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($this->canDelete($token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(TokenInterface $token)
|
||||
{
|
||||
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_ADMIN', $token);
|
||||
}
|
||||
}
|
||||
|
||||
64
src/Voter/RolePermissionVoter.php
Normal file
64
src/Voter/RolePermissionVoter.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* A voter to check the free-configurable permission from "kimai.permissions".
|
||||
*/
|
||||
class RolePermissionVoter extends AbstractVoter
|
||||
{
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $subject
|
||||
* @return bool
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
// we only work on single strings that have no subject
|
||||
if (null !== $subject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// and which is not neither a user role like USER_ADMIN
|
||||
// nor an implicit role like IS_REMEMBERED / IS_FULLY_AUTHENTICATED
|
||||
if (strpos($attribute, 'ROLE_') === false && strpos($attribute, 'IS_') === false) {
|
||||
return $this->isRegisteredPermission($attribute);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param Activity $subject
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!($user instanceof User)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($user->getRoles() as $role) {
|
||||
if ($this->hasPermission($role, $attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,23 @@ class TimesheetVoter extends AbstractVoter
|
||||
{
|
||||
public const START = 'start';
|
||||
public const STOP = 'stop';
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
public const EXPORT = 'export';
|
||||
public const VIEW_RATE = 'view_rate';
|
||||
public const EDIT_RATE = 'edit_rate';
|
||||
|
||||
/**
|
||||
* support rules based on the given $subject (here: Timesheet)
|
||||
*/
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
self::START,
|
||||
self::STOP,
|
||||
self::VIEW,
|
||||
self::EDIT,
|
||||
self::DELETE
|
||||
self::DELETE,
|
||||
self::EXPORT,
|
||||
self::VIEW_RATE,
|
||||
self::EDIT_RATE,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -40,11 +47,11 @@ class TimesheetVoter extends AbstractVoter
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$subject instanceof Timesheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Timesheet) {
|
||||
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -61,40 +68,49 @@ class TimesheetVoter extends AbstractVoter
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
if (!($user instanceof User)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($attribute) {
|
||||
case self::STOP:
|
||||
return $this->canStop($subject, $user, $token);
|
||||
|
||||
case self::START:
|
||||
return $this->canStart($subject, $user, $token);
|
||||
|
||||
case self::VIEW:
|
||||
return $this->canView($subject, $user, $token);
|
||||
|
||||
case self::EDIT:
|
||||
return $this->canEdit($subject, $user, $token);
|
||||
|
||||
case self::DELETE:
|
||||
return $this->canDelete($subject, $user, $token);
|
||||
if (!($subject instanceof Timesheet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$permission = '';
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canStop(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
// if a teamlead stops an entry for another user, check that this user is part of his team
|
||||
return $this->isOwnOrTeamlead($timesheet, $user, $token);
|
||||
switch ($attribute) {
|
||||
case self::START:
|
||||
if (!$this->canStart($subject, $user, $token)) {
|
||||
return false;
|
||||
}
|
||||
$permission .= $attribute;
|
||||
break;
|
||||
|
||||
case self::VIEW_RATE:
|
||||
case self::EDIT_RATE:
|
||||
case self::STOP:
|
||||
case self::EDIT:
|
||||
case self::DELETE:
|
||||
case self::EXPORT:
|
||||
$permission .= $attribute;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
$permission .= '_';
|
||||
|
||||
// extend me for "team" support later on
|
||||
if ($subject->getUser()->getId() == $user->getId()) {
|
||||
$permission .= 'own';
|
||||
} else {
|
||||
$permission .= 'other';
|
||||
}
|
||||
|
||||
$permission .= '_timesheet';
|
||||
|
||||
return $this->hasRolePermission($user, $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,65 +134,4 @@ class TimesheetVoter extends AbstractVoter
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
return $this->isOwnOrTeamlead($timesheet, $user, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
return $this->isOwnOrTeamlead($timesheet, $user, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
if (!$this->isFullyAuthenticated($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isOwnOrAdmin($timesheet, $user, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function isOwnOrTeamlead(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($timesheet->getUser()->getId() == $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->hasRole('ROLE_TEAMLEAD', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function isOwnOrAdmin(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
if ($timesheet->getUser()->getId() == $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->hasRole('ROLE_ADMIN', $token);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user