Compare commits
113 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 | ||
|
|
417675b3df | ||
|
|
321ac74b6a | ||
|
|
b1c06eed7b | ||
|
|
8cb52e22b1 | ||
|
|
a073064900 | ||
|
|
07cceca67e | ||
|
|
540318fcd0 | ||
|
|
e9cbb872d0 | ||
|
|
f9dc42d1f9 | ||
|
|
1dabeedaa0 | ||
|
|
dc70ff128c | ||
|
|
b012b7170b | ||
|
|
0a1c2d8440 | ||
|
|
d72596f000 | ||
|
|
33029b2a11 | ||
|
|
bae24dd0ec | ||
|
|
db7de3aac1 | ||
|
|
c66dfbc653 | ||
|
|
51c207e025 | ||
|
|
e44e2442af | ||
|
|
86f40083ef | ||
|
|
f45fbbce60 | ||
|
|
c648866cb4 | ||
|
|
f8adafdabe | ||
|
|
ab4a1a0a1d | ||
|
|
34a3f4e275 | ||
|
|
55aedcd4a8 | ||
|
|
6633c550f5 | ||
|
|
0165658659 | ||
|
|
3075276a52 | ||
|
|
b46e752d0a | ||
|
|
ad171e7706 | ||
|
|
92c665436a | ||
|
|
2dbf433474 | ||
|
|
56ac642446 | ||
|
|
2f6c0d2208 | ||
|
|
a32614fff8 | ||
|
|
53053c127e | ||
|
|
a89ef41e81 | ||
|
|
eee7cb9bf4 | ||
|
|
98dc38ed99 | ||
|
|
7bcd6a6382 | ||
|
|
73e0a75585 | ||
|
|
71aa899f77 | ||
|
|
53f82a808b | ||
|
|
b035fc9ebd | ||
|
|
8cae5e8b3f | ||
|
|
6163f33abf | ||
|
|
a636faa4ab | ||
|
|
ee4df36735 | ||
|
|
4ede5463f9 | ||
|
|
4c277fe1b5 | ||
|
|
9f6079ad8a | ||
|
|
6fb6a16246 | ||
|
|
3af8070a57 | ||
|
|
ebe007eb78 | ||
|
|
55b7e192aa | ||
|
|
62f91f4d38 | ||
|
|
b1546c0568 | ||
|
|
af2d1c3a6e | ||
|
|
ca893b303a | ||
|
|
142369d651 | ||
|
|
3241c73e7c | ||
|
|
d6d9abd05e | ||
|
|
85ff2ff9de | ||
|
|
dbbb434723 | ||
|
|
7804ef83ce | ||
|
|
fb2d898e4c | ||
|
|
c6e99b35d5 | ||
|
|
3dcd38eca7 | ||
|
|
39ea44fdc3 | ||
|
|
2f84f3751a | ||
|
|
a7780dac9e | ||
|
|
e549a0824f | ||
|
|
8b8583d48b | ||
|
|
a8a8424ced | ||
|
|
0996eca3c5 | ||
|
|
e6f8bc8ae2 | ||
|
|
5ccc1c991b | ||
|
|
1dd5f2b72f |
2
.github/lock.yaml → .github/lock.yml
vendored
2
.github/lock.yaml → .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,2 +1,4 @@
|
||||
unreleased=true
|
||||
future-release=0.3
|
||||
future-release=0.7
|
||||
exclude-labels=duplicate,question,invalid,wontfix,release
|
||||
enhancement_labels=>enhancement,Enhancement,feature request
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
|
||||
# ========== KIMAI ==========
|
||||
# IDE
|
||||
.idea/
|
||||
@@ -7,11 +6,17 @@
|
||||
!bin/console
|
||||
/config/packages/local.yaml
|
||||
|
||||
/var/data/kimai.sqlite
|
||||
templates/invoice/renderer/.~lock*
|
||||
|
||||
/var/data/*.sqlite
|
||||
/var/data/*.sqlite-journal
|
||||
!/var/data/kimai_test.sqlite
|
||||
/var/coverage/
|
||||
/var/cache/*
|
||||
!var/cache/.gitkeep
|
||||
|
||||
/var/invoices/*
|
||||
|
||||
/var/log/*
|
||||
!var/log/.gitkeep
|
||||
|
||||
@@ -31,18 +36,20 @@
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
.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 ###
|
||||
|
||||
@@ -12,11 +12,13 @@ build:
|
||||
override:
|
||||
-
|
||||
command: 'vendor/bin/phpunit --coverage-clover=clover.xml tests/'
|
||||
stop_on_failure: true
|
||||
coverage:
|
||||
file: 'clover.xml'
|
||||
format: 'clover'
|
||||
-
|
||||
command: 'bin/console kimai:phpcs --checkstyle=checkstyle'
|
||||
stop_on_failure: true
|
||||
analysis:
|
||||
file: 'checkstyle'
|
||||
format: 'php-cs-checkstyle'
|
||||
@@ -33,3 +35,8 @@ filter:
|
||||
dependency_paths:
|
||||
- 'node_modules/'
|
||||
- 'vendor/'
|
||||
|
||||
build_failure_conditions:
|
||||
- 'project.metric("scrutinizer.quality", < 9.30)'
|
||||
- 'project.metric("scrutinizer.test_coverage", < 0.73)'
|
||||
- 'project.metric_change("scrutinizer.test_coverage", < -0.01)'
|
||||
@@ -18,7 +18,7 @@ matrix:
|
||||
- php: 7.2
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer self-update
|
||||
|
||||
install:
|
||||
@@ -29,6 +29,8 @@ script:
|
||||
- cp tests/.env.dist.sqlite .env
|
||||
- bin/console doctrine:database:create -n
|
||||
- bin/console doctrine:migrations:migrate -n
|
||||
- bin/console doctrine:migrations:migrate first -n
|
||||
- cp tests/.env.dist.mysql .env
|
||||
- bin/console doctrine:database:create -n
|
||||
- bin/console doctrine:migrations:migrate -n
|
||||
- bin/console doctrine:migrations:migrate first -n
|
||||
|
||||
341
CHANGELOG.md
341
CHANGELOG.md
@@ -1,20 +1,277 @@
|
||||
# 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)
|
||||
- invalid CSRF-Token [\#344](https://github.com/kevinpapst/kimai2/issues/344)
|
||||
- fix column visibility if unconfigured \(no cookie existing\) [\#423](https://github.com/kevinpapst/kimai2/pull/423) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix duration and rate for html invoices [\#414](https://github.com/kevinpapst/kimai2/pull/414) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed broken toolbar form with empty date [\#391](https://github.com/kevinpapst/kimai2/pull/391) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- After installation no graphic/background/icons shown [\#417](https://github.com/kevinpapst/kimai2/issues/417)
|
||||
- Form fields should be always visible [\#408](https://github.com/kevinpapst/kimai2/issues/408)
|
||||
- Active filter should stay open [\#405](https://github.com/kevinpapst/kimai2/issues/405)
|
||||
- Make project field optional [\#401](https://github.com/kevinpapst/kimai2/issues/401)
|
||||
- Unrecognized Options "renderer, number\_generator" under "kimai.invoice" [\#381](https://github.com/kevinpapst/kimai2/issues/381)
|
||||
- 500: Internal Server Error after login \(new install\) [\#342](https://github.com/kevinpapst/kimai2/issues/342)
|
||||
- Invoices export error [\#341](https://github.com/kevinpapst/kimai2/issues/341)
|
||||
- add the same activity for multiple projects [\#325](https://github.com/kevinpapst/kimai2/issues/325)
|
||||
- API: Endpoint for API docs needs session cookie [\#319](https://github.com/kevinpapst/kimai2/issues/319)
|
||||
- docker image for kimai2 [\#284](https://github.com/kevinpapst/kimai2/issues/284)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- redirect to user language after profile update [\#421](https://github.com/kevinpapst/kimai2/pull/421) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added more timesheet related invoice fields [\#411](https://github.com/kevinpapst/kimai2/pull/411) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- toolbar improvements [\#410](https://github.com/kevinpapst/kimai2/pull/410) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- move entries from one entity to another upon deletion [\#409](https://github.com/kevinpapst/kimai2/pull/409) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- show toolbar filter if they were submitted [\#407](https://github.com/kevinpapst/kimai2/pull/407) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- full configurable data columns in all screen sizes [\#404](https://github.com/kevinpapst/kimai2/pull/404) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added global activity support in importer [\#402](https://github.com/kevinpapst/kimai2/pull/402) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added parsedown extension for header ids \(\#388\) [\#399](https://github.com/kevinpapst/kimai2/pull/399) ([lduer](https://github.com/lduer))
|
||||
- Fix for \#397 [\#398](https://github.com/kevinpapst/kimai2/pull/398) ([tobybatch](https://github.com/tobybatch))
|
||||
- Added links to docker hub and my docker repo [\#396](https://github.com/kevinpapst/kimai2/pull/396) ([tobybatch](https://github.com/tobybatch))
|
||||
- fixed issues- & milestone links [\#395](https://github.com/kevinpapst/kimai2/pull/395) ([lduer](https://github.com/lduer))
|
||||
- added links to further docker images by the community [\#394](https://github.com/kevinpapst/kimai2/pull/394) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- beta test: added quick search box for customers, projects and activities [\#392](https://github.com/kevinpapst/kimai2/pull/392) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated the documentation, reflecting the changes in \#306 [\#387](https://github.com/kevinpapst/kimai2/pull/387) ([lduer](https://github.com/lduer))
|
||||
- Added Hungarian translation and fixed the path of kimai.yaml in translation documentation [\#385](https://github.com/kevinpapst/kimai2/pull/385) ([infeeeee](https://github.com/infeeeee))
|
||||
- improved docs and cross-linked docker [\#383](https://github.com/kevinpapst/kimai2/pull/383) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Doc update of install/update instructions re: username [\#382](https://github.com/kevinpapst/kimai2/pull/382) ([srdco](https://github.com/srdco))
|
||||
- invoice calculator to group entries by activity [\#380](https://github.com/kevinpapst/kimai2/pull/380) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- basic about screen with server and debug infos [\#378](https://github.com/kevinpapst/kimai2/pull/378) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Dev/demo dockerfile [\#377](https://github.com/kevinpapst/kimai2/pull/377) ([tobybatch](https://github.com/tobybatch))
|
||||
- Sidebar - user-profile links and language settings [\#369](https://github.com/kevinpapst/kimai2/pull/369) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable initial calendar view [\#363](https://github.com/kevinpapst/kimai2/pull/363) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- decrease minimum username to 3 character [\#362](https://github.com/kevinpapst/kimai2/pull/362) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Default country \(\#2\) [\#358](https://github.com/kevinpapst/kimai2/pull/358) ([tobybatch](https://github.com/tobybatch))
|
||||
- composer: removed deprecated symfony/lt, updated others [\#354](https://github.com/kevinpapst/kimai2/pull/354) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated admin-lte theme bundle to 2.1.1 [\#350](https://github.com/kevinpapst/kimai2/pull/350) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated timesheet and email documentation [\#349](https://github.com/kevinpapst/kimai2/pull/349) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated required dependencies [\#346](https://github.com/kevinpapst/kimai2/pull/346) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fix password reset [\#345](https://github.com/kevinpapst/kimai2/pull/345) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added arabic translations and locale support in datepicker [\#337](https://github.com/kevinpapst/kimai2/pull/337) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- API endpoint for Timesheet entries [\#332](https://github.com/kevinpapst/kimai2/pull/332) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Support global activities [\#259](https://github.com/kevinpapst/kimai2/pull/259) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.5](https://github.com/kevinpapst/kimai2/tree/0.5) (2018-09-27)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.4...0.5)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- API: Kimai metadata endpoint [\#320](https://github.com/kevinpapst/kimai2/issues/320)
|
||||
- Support Markdown in timesheet description [\#295](https://github.com/kevinpapst/kimai2/issues/295)
|
||||
- Add support for MS Office templates [\#283](https://github.com/kevinpapst/kimai2/issues/283)
|
||||
- Adding rates to customers, projects and tasks [\#271](https://github.com/kevinpapst/kimai2/issues/271)
|
||||
- Delete invoice templates [\#267](https://github.com/kevinpapst/kimai2/issues/267)
|
||||
- Sum of hours in timesheet invoice [\#262](https://github.com/kevinpapst/kimai2/issues/262)
|
||||
- Feature request - API [\#251](https://github.com/kevinpapst/kimai2/issues/251)
|
||||
- Feature request - PDF Download for monthly report [\#244](https://github.com/kevinpapst/kimai2/issues/244)
|
||||
- Add support for OpenOffice templates [\#223](https://github.com/kevinpapst/kimai2/issues/223)
|
||||
- Feature Request - Excel Output of times [\#221](https://github.com/kevinpapst/kimai2/issues/221)
|
||||
- Add hourly rate options [\#111](https://github.com/kevinpapst/kimai2/issues/111)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Excel Output not containing data [\#327](https://github.com/kevinpapst/kimai2/issues/327)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Creating database schema throws exceptions [\#322](https://github.com/kevinpapst/kimai2/issues/322)
|
||||
- Add permission voter for invoices and templates [\#293](https://github.com/kevinpapst/kimai2/issues/293)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- added copy invoice template action [\#331](https://github.com/kevinpapst/kimai2/pull/331) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix spreadsheet renderer for invoices with one entry [\#328](https://github.com/kevinpapst/kimai2/pull/328) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- invoice VAT as float and database key length [\#323](https://github.com/kevinpapst/kimai2/pull/323) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added version command + api endpoint [\#321](https://github.com/kevinpapst/kimai2/pull/321) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Timesheet export [\#317](https://github.com/kevinpapst/kimai2/pull/317) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added invoice voter [\#316](https://github.com/kevinpapst/kimai2/pull/316) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Refactored invoice system [\#306](https://github.com/kevinpapst/kimai2/pull/306) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- add stable controller tests [\#305](https://github.com/kevinpapst/kimai2/pull/305) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fixed rate and Hourly rates [\#304](https://github.com/kevinpapst/kimai2/pull/304) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- add hourly\_rate and fixed\_rate to timesheet entries [\#302](https://github.com/kevinpapst/kimai2/pull/302) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow to switch language on any page [\#299](https://github.com/kevinpapst/kimai2/pull/299) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fix duration\_short format for french translation [\#297](https://github.com/kevinpapst/kimai2/pull/297) ([jeau](https://github.com/jeau))
|
||||
- allow markdown in timesheet descriptions [\#296](https://github.com/kevinpapst/kimai2/pull/296) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added option to delete invoice template [\#294](https://github.com/kevinpapst/kimai2/pull/294) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved invoice preview [\#254](https://github.com/kevinpapst/kimai2/pull/254) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.4](https://github.com/kevinpapst/kimai2/tree/0.4) (2018-09-01)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.3...0.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Delete user [\#225](https://github.com/kevinpapst/kimai2/issues/225)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Don't allow to stop an already stopped entry [\#282](https://github.com/kevinpapst/kimai2/issues/282)
|
||||
- installed it, and then what??? [\#255](https://github.com/kevinpapst/kimai2/issues/255)
|
||||
- fixed calendar for empty timesheet descriptions [\#265](https://github.com/kevinpapst/kimai2/pull/265) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed .htaccess for apache users [\#260](https://github.com/kevinpapst/kimai2/pull/260) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Spanish Translation Contrib [\#248](https://github.com/kevinpapst/kimai2/issues/248)
|
||||
- Administration of projects throws error if locale is "de" [\#247](https://github.com/kevinpapst/kimai2/issues/247)
|
||||
- Web Installer Package for near-automated installs from browser [\#235](https://github.com/kevinpapst/kimai2/issues/235)
|
||||
- Improve dashboard display [\#280](https://github.com/kevinpapst/kimai2/issues/280)
|
||||
- Feature request - Monthly overview / report [\#207](https://github.com/kevinpapst/kimai2/issues/207)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- fixed money display in dashboard widgets [\#291](https://github.com/kevinpapst/kimai2/pull/291) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Symfony update 4.1.4 [\#290](https://github.com/kevinpapst/kimai2/pull/290) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- do not allow to stop already stopped timesheets [\#289](https://github.com/kevinpapst/kimai2/pull/289) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable formats for duration [\#287](https://github.com/kevinpapst/kimai2/pull/287) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated russian translations [\#286](https://github.com/kevinpapst/kimai2/pull/286) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Update french translation [\#279](https://github.com/kevinpapst/kimai2/pull/279) ([jeau](https://github.com/jeau))
|
||||
- Fix standard date format in french [\#278](https://github.com/kevinpapst/kimai2/pull/278) ([jeau](https://github.com/jeau))
|
||||
- new translations: spanish, french and italian [\#276](https://github.com/kevinpapst/kimai2/pull/276) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- French translation for Kimai [\#275](https://github.com/kevinpapst/kimai2/pull/275) ([jeau](https://github.com/jeau))
|
||||
- updated to AdminLTE 2.4.8 [\#274](https://github.com/kevinpapst/kimai2/pull/274) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixing migration\_v1.md url [\#270](https://github.com/kevinpapst/kimai2/pull/270) ([ltsavar](https://github.com/ltsavar))
|
||||
- dashboard widgets are configurable via config [\#269](https://github.com/kevinpapst/kimai2/pull/269) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Spanish translation for Kimai [\#266](https://github.com/kevinpapst/kimai2/pull/266) ([cobaltos](https://github.com/cobaltos))
|
||||
- moved theme settings to dynamic compiler pass [\#264](https://github.com/kevinpapst/kimai2/pull/264) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added config setting to disable password-reset and user-registration [\#261](https://github.com/kevinpapst/kimai2/pull/261) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added basic API endpoints [\#258](https://github.com/kevinpapst/kimai2/pull/258) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Hotfix for Symfony Flex incompatibility with latest composer changes [\#257](https://github.com/kevinpapst/kimai2/pull/257) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added italian translation [\#253](https://github.com/kevinpapst/kimai2/pull/253) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Delete user [\#249](https://github.com/kevinpapst/kimai2/pull/249) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- dashboard widgets from event [\#246](https://github.com/kevinpapst/kimai2/pull/246) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added timesheet-calendar view [\#236](https://github.com/kevinpapst/kimai2/pull/236) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved data fixtures [\#234](https://github.com/kevinpapst/kimai2/pull/234) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [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)
|
||||
|
||||
**Update from 0.2:**
|
||||
**Implemented enhancements:**
|
||||
|
||||
You need to adjust your `.env` file and add your `from` address for [all emails](https://github.com/kevinpapst/kimai2/blob/master/var/docs/emails.md) generated by Kimai 2:
|
||||
```
|
||||
MAILER_FROM=kimai@example.com
|
||||
```
|
||||
|
||||
Users upgrading from 0.2 need to execute:
|
||||
```
|
||||
bin/console doctrine:migrations:version --add 20180701120000
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
- 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:**
|
||||
|
||||
@@ -25,20 +282,14 @@ bin/console doctrine:migrations:migrate
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Verifying email configuration [\#226](https://github.com/kevinpapst/kimai2/issues/226)
|
||||
- Installation Issue of version1.3.1 [\#208](https://github.com/kevinpapst/kimai2/issues/208)
|
||||
- Error on bin/console doctrine:schema:create [\#191](https://github.com/kevinpapst/kimai2/issues/191)
|
||||
- Verifying email configuration [\#226](https://github.com/kevinpapst/kimai2/issues/226)
|
||||
- Add favicon [\#205](https://github.com/kevinpapst/kimai2/issues/205)
|
||||
- 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)
|
||||
- Error on bin/console doctrine:schema:create [\#191](https://github.com/kevinpapst/kimai2/issues/191)
|
||||
- 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:**
|
||||
|
||||
@@ -57,42 +308,24 @@ bin/console doctrine:migrations:migrate
|
||||
- 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)
|
||||
@@ -102,7 +335,6 @@ bin/console doctrine:migrations:migrate
|
||||
- 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)
|
||||
@@ -111,10 +343,35 @@ bin/console doctrine:migrations:migrate
|
||||
- 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:**
|
||||
|
||||
- prepare v0.2 release \#177 [\#178](https://github.com/kevinpapst/kimai2/pull/178) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- \[WIP\] Translations [\#173](https://github.com/kevinpapst/kimai2/pull/173) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- various documentation updates [\#176](https://github.com/kevinpapst/kimai2/pull/176) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code styles: header\_comment and ordered\_imports [\#175](https://github.com/kevinpapst/kimai2/pull/175) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Code sniffer rules \#158 [\#172](https://github.com/kevinpapst/kimai2/pull/172) ([kevinpapst](https://github.com/kevinpapst))
|
||||
@@ -126,8 +383,6 @@ bin/console doctrine:migrations:migrate
|
||||
- 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))
|
||||
@@ -159,7 +414,7 @@ bin/console doctrine:migrations:migrate
|
||||
- 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)
|
||||
|
||||
49
Dockerfile
Normal file
49
Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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.
|
||||
|
||||
FROM php:7.2.9-apache-stretch
|
||||
|
||||
RUN apt update && \
|
||||
apt install -y \
|
||||
git \
|
||||
libicu-dev \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
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 remove -y wget && \
|
||||
apt -y autoremove && \
|
||||
apt clean && \
|
||||
git clone https://github.com/kevinpapst/kimai2.git /opt/kimai && \
|
||||
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
|
||||
CMD /opt/kimai/bin/console server:run 0.0.0.0:8001
|
||||
10
LICENSE
10
LICENSE
@@ -1,11 +1,13 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 Kevin Papst
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
@@ -15,5 +17,5 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
160
README.md
160
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,141 +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](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](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
|
||||
- One PHP extension of [PDO-SQLite](https://php.net/manual/en/ref.pdo-sqlite.php) or [PDO-MySQL](https://php.net/manual/en/ref.pdo-mysql.php) enabled (it might work with PostgreSQL and Oracle as well, but that wasn't tested and is not officially supported)
|
||||
- The PHP extension [intl](https://php.net/manual/en/book.intl.php)
|
||||
- The [usual Symfony application requirements](http://symfony.com/doc/current/reference/requirements.html)
|
||||
- If you use MariaDB, make sure its at least v10.7.2 (see [FAQ](var/docs/faq.md))
|
||||
- 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 9
|
||||
- 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](milestones/) sections,
|
||||
current work is organized in the [Project](projects/) planning boards.
|
||||
Our roadmap is open for changes and input from the community, please [sent us](issues/) your ideas and questions.
|
||||
### Installation
|
||||
|
||||
## Installation
|
||||
There are multiple ways to install Kimai, all of them described in the [installation docu](var/docs/installation.md):
|
||||
|
||||
There are [further infos about installation](var/docs/installation.md) if you have to use FTP or want to develop with Kimai.
|
||||
- [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)
|
||||
|
||||
If you want to install Kimai v2 in your production environment, then SSH into your server and change to your webserevr root.
|
||||
You need to install Git and [Composer](https://getcomposer.org/doc/00-intro.md) if you haven't already.
|
||||
### Updating Kimai
|
||||
|
||||
First clone this repo:
|
||||
Read the following documentations before you start your upgrade:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kevinpapst/kimai2.git
|
||||
cd kimai2/
|
||||
```
|
||||
- The [update documentation](var/docs/updates.md)
|
||||
- The version specific [UPGRADING guide](UPGRADING.md)
|
||||
- The [release information](https://github.com/kevinpapst/kimai2/releases)
|
||||
|
||||
Make sure the [file permissions are correct](https://symfony.com/doc/current/setup/file_permissions.html):
|
||||
```bash
|
||||
chown -R www-data var/
|
||||
chmod -R 777 var/
|
||||
```
|
||||
## Roadmap and releases
|
||||
|
||||
It's up to you which database server you want to use, Kimai v2 supports MySQL/MariaDB and SQLite.
|
||||
Configure the database connection string in your the `.env` file:
|
||||
```
|
||||
# adjust all settings in .env to your needs
|
||||
APP_ENV=prod
|
||||
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
|
||||
```
|
||||
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!
|
||||
|
||||
Now install all dependencies for Kimai 2:
|
||||
> 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.
|
||||
|
||||
```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](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
|
||||
|
||||
**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
|
||||
|
||||
@@ -156,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
|
||||
|
||||
70
UPGRADING.md
70
UPGRADING.md
@@ -1,5 +1,75 @@
|
||||
# Upgrading Kimai 2
|
||||
|
||||
Database upgrades are currently ONLY provided for MySQL/MariaDB and SQLite.
|
||||
|
||||
If you plan on using e.g. PostgreSQL, please read more about the `bin/console doctrine:migrations:diff` and
|
||||
`bin/console doctrine:migrations:migrate` commands and contact us, so we can integrate them into the official releases.
|
||||
|
||||
A normal upgrade can be executed with these commands:
|
||||
|
||||
```bash
|
||||
git pull origin master
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
sudo -u www-data bin/console cache:clear --env=prod
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
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.
|
||||
Be aware that the API is still is development mode and shouldn't be considered stable for now.
|
||||
|
||||
## [0.5](https://github.com/kevinpapst/kimai2/releases/tag/0.5) (2018-09-27)
|
||||
|
||||
Some configuration nodes were removed, if you have one of them in your `local.yaml` you need to delete them before you start the update:
|
||||
- `kimai.invoice.calculator`
|
||||
- `kimai.invoice.renderer`
|
||||
- `kimai.invoice.number_generator`
|
||||
|
||||
The new config `kimai.invoice.documents` was introduced, holding a list of directories ([read more](https://github.com/kevinpapst/kimai2/blob/master/var/docs/invoices.md)).
|
||||
|
||||
**BC break:** InvoiceTemplate name was changed from 255 characters to 60. If you used longer invoice-template names, they will be truncated when upgrading the database.
|
||||
Please make sure that they are unique in the first 60 character before you upgrade your database with `doctrine:migrations:migrate`.
|
||||
|
||||
## [0.4](https://github.com/kevinpapst/kimai2/releases/tag/0.4) (2018-09-01)
|
||||
|
||||
In the time between 0.3 and 0.4 there was a release of composer that introduced a BC break,
|
||||
which leads to problems between Composer and Symfony Flex, resulting in an error like this when running it:
|
||||
|
||||
```
|
||||
[ErrorException]
|
||||
Declaration of Symfony\Flex\ParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with Composer\Util\RemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array &$responseHeaders = NULL)
|
||||
```
|
||||
|
||||
This can be fixed by updating Composer and Flex before executing the Kimai update:
|
||||
```
|
||||
sudo composer self-update
|
||||
sudo -u www-data composer update symfony/flex --no-plugins --no-scripts
|
||||
```
|
||||
|
||||
Then the full update can be executed as usual:
|
||||
|
||||
```bash
|
||||
git pull origin master
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
sudo -u www-data bin/console cache:clear --env=prod
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
## [0.3](https://github.com/kevinpapst/kimai2/releases/tag/0.3) (2018-07-22)
|
||||
|
||||
**Update from 0.2:**
|
||||
|
||||
18
assets/app.js
Normal file
18
assets/app.js
Normal file
@@ -0,0 +1,18 @@
|
||||
require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
|
||||
|
||||
require('./sass/app.scss');
|
||||
|
||||
require('fullcalendar');
|
||||
require('fullcalendar/dist/gcal.min');
|
||||
require('fullcalendar/dist/locale-all');
|
||||
require('fullcalendar/dist/fullcalendar.min.css');
|
||||
|
||||
// ------ for charts ------
|
||||
require('chart.js/dist/Chart.min');
|
||||
|
||||
// ------ Kimai itself ------
|
||||
require('./js/kimai.js');
|
||||
require('./js/datatable.js');
|
||||
require('./js/toolbar.js');
|
||||
require('./images/default_avatar.png');
|
||||
require('./images/signature.png');
|
||||
@@ -1,156 +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 available on most pages above the datatable */
|
||||
.toolbar form {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
/* ================================ 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;
|
||||
}
|
||||
BIN
assets/images/signature.png
Normal file
BIN
assets/images/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,10 +0,0 @@
|
||||
require('../../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
|
||||
|
||||
require('../css/app.scss');
|
||||
require('../css/datatable.scss');
|
||||
|
||||
// ------ Kimai itself ------
|
||||
require('./kimai.js');
|
||||
require('./datatable.js');
|
||||
require('./toolbar.js');
|
||||
require('../images/default_avatar.png');
|
||||
@@ -22,22 +22,26 @@ if (typeof jQuery === 'undefined') {
|
||||
$.datatable = {};
|
||||
|
||||
$(function() {
|
||||
"use strict";
|
||||
"use strict";
|
||||
|
||||
$.datatable = {
|
||||
saveVisibility: function (modalSelector) {
|
||||
var settings = {};
|
||||
var cookieName = $(modalSelector).find('form').attr('name');
|
||||
$(modalSelector).find('form').find('input:checkbox:not(:checked)').each(
|
||||
function () {
|
||||
settings[$(this).attr('name')] = $(this).is(':checked');
|
||||
$(modalSelector).find('form').each(
|
||||
function() {
|
||||
var settings = {};
|
||||
var cookieName = $(this).attr('name');
|
||||
$(this).find('input:checkbox').each(
|
||||
function () {
|
||||
settings[$(this).attr('name')] = $(this).is(':checked');
|
||||
}
|
||||
);
|
||||
if (jQuery.isEmptyObject(settings)) {
|
||||
Cookies.remove(cookieName);
|
||||
} else {
|
||||
Cookies.set(cookieName, JSON.stringify(settings), {expires: 365});
|
||||
}
|
||||
}
|
||||
);
|
||||
if (jQuery.isEmptyObject(settings)) {
|
||||
Cookies.remove(cookieName);
|
||||
} else {
|
||||
Cookies.set(cookieName, JSON.stringify(settings), {expires: 365});
|
||||
}
|
||||
$(modalSelector).modal('toggle');
|
||||
location.reload();
|
||||
},
|
||||
@@ -49,12 +53,12 @@ $(function() {
|
||||
}
|
||||
var header = $(tbl.find('th').get(column));
|
||||
if (header.css("display") === "none") {
|
||||
header.show();
|
||||
header.show('ease');
|
||||
tbl.find('tr').each(function(){
|
||||
$($(this).find('td').get(column)).show('ease');
|
||||
});
|
||||
} else {
|
||||
header.hide();
|
||||
header.hide('ease');
|
||||
tbl.find('tr').each(function(){
|
||||
$($(this).find('td').get(column)).hide('ease');
|
||||
});
|
||||
|
||||
@@ -29,8 +29,11 @@ $(function() {
|
||||
$.kimai.settings = $.extend({}, $.kimai.defaults, options);
|
||||
}
|
||||
|
||||
// set the current locale for all javascript components
|
||||
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']);
|
||||
});
|
||||
|
||||
@@ -51,12 +54,18 @@ $(function() {
|
||||
$('input[data-datepicker="on"]').daterangepicker({
|
||||
singleDatePicker: true,
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
format: "YYYY-MM-DD",
|
||||
firstDay: 1
|
||||
}
|
||||
});
|
||||
|
||||
$('input[data-datepicker="on"]').on('apply.daterangepicker', function(ev, picker) {
|
||||
$(this).val(picker.startDate.format('YYYY-MM-DD'));
|
||||
$(this).trigger("change");
|
||||
});
|
||||
|
||||
$('input[data-datetimepicker="on"]').daterangepicker({
|
||||
singleDatePicker: true,
|
||||
timePicker: true,
|
||||
@@ -69,19 +78,53 @@ $(function() {
|
||||
firstDay: 1
|
||||
}
|
||||
});
|
||||
|
||||
$('input[data-datetimepicker="on"]').on('apply.daterangepicker', function(ev, picker) {
|
||||
$(this).val(picker.startDate.format('YYYY-MM-DD HH:mm'));
|
||||
$(this).trigger("change");
|
||||
});
|
||||
|
||||
/*
|
||||
$('input').iCheck({
|
||||
checkboxClass: 'icheckbox_square-blue',
|
||||
radioClass: 'iradio_square-blue',
|
||||
increaseArea: '20%' // optional
|
||||
});
|
||||
*/
|
||||
$('select[data-related-select]').change(function() {
|
||||
var apiUrl = $(this).attr('data-api-url').replace('-s-', $(this).val());
|
||||
var targetSelect = $(this).attr('data-related-select');
|
||||
|
||||
// $.AdminLTE.pushMenu.expandOnHover();
|
||||
$.ajax({
|
||||
url: apiUrl,
|
||||
headers: {
|
||||
'X-AUTH-SESSION': true,
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
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) {
|
||||
$select.append('<option value="' + obj.id + '">' + obj.name + '</option>');
|
||||
});
|
||||
|
||||
$select.trigger('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.markdown-body :header').prepend(function() {
|
||||
$(this).prepend('<a class="anchor" href="#'+$(this).attr('id')+'"><i class="fas fa-link"></i>');
|
||||
});
|
||||
|
||||
$('.markdown-body :header').hover(function(){
|
||||
$(this).find('a.anchor').show();
|
||||
}, function(){
|
||||
$(this).find('a.anchor').hide();
|
||||
});
|
||||
},
|
||||
pauseRecord: function(selector) {
|
||||
$(selector + ' .pull-left i').hover(function () {
|
||||
@@ -98,8 +141,7 @@ $(function() {
|
||||
|
||||
// default values
|
||||
$.kimai.defaults = {
|
||||
baseUrl: '/',
|
||||
imagePath: '/images',
|
||||
locale: 'en',
|
||||
confirmDelete: 'Really delete?',
|
||||
alertSuccessAutoHide: 5000
|
||||
};
|
||||
|
||||
@@ -6,32 +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':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#project').parent().remove();
|
||||
} else {
|
||||
$('.toolbar form select#project').val('');
|
||||
}
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
break;
|
||||
case 'project':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
} else {
|
||||
$('.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
40
assets/sass/app.scss
Normal file
40
assets/sass/app.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
@media print{
|
||||
.sf-toolbar, .control-sidebar {display: none !important;}
|
||||
}
|
||||
|
||||
@import 'content';
|
||||
@import 'toolbar';
|
||||
|
||||
/* ================================ 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%;
|
||||
}
|
||||
|
||||
@import 'invoice';
|
||||
@import 'datatable';
|
||||
@import 'calendar';
|
||||
@import 'dashboard';
|
||||
@import 'navbar';
|
||||
@import 'documentation';
|
||||
7
assets/sass/calendar.scss
Normal file
7
assets/sass/calendar.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.popover-content {
|
||||
.calendar-entry {
|
||||
ul {
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
assets/sass/content.scss
Normal file
41
assets/sass/content.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&.timesheet-description {
|
||||
ul {
|
||||
padding-left: 15px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
assets/sass/dashboard.scss
Normal file
16
assets/sass/dashboard.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
section {
|
||||
&.dashboard {
|
||||
.info-box {
|
||||
.info-box-text {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
assets/sass/documentation.scss
Normal file
25
assets/sass/documentation.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.markdown-body {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
a.anchor {
|
||||
float: left;
|
||||
font-size: 0.5em;
|
||||
margin-left: -1em;
|
||||
padding-top: 0.5em;
|
||||
color: $text-color;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.markdown-body {
|
||||
padding-left: 25px;
|
||||
}
|
||||
}
|
||||
304
assets/sass/invoice.scss
Normal file
304
assets/sass/invoice.scss
Normal file
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
body.invoice_print
|
||||
{
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ================================ FREELANCER INVOICE ================================ */
|
||||
|
||||
#freelancer-invoice {
|
||||
font: 16px/1 'Open Sans', sans-serif;
|
||||
box-sizing: border-box;
|
||||
|
||||
h1 {
|
||||
font: bold 100% sans-serif;
|
||||
letter-spacing: 0.5em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font: bold 1.5em sans-serif;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* table */
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* header */
|
||||
|
||||
header {
|
||||
}
|
||||
|
||||
header:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
header address {
|
||||
float: left;
|
||||
font-style: normal;
|
||||
line-height: 1.25;
|
||||
margin: 0 1em 1em 0;
|
||||
font-weight: bold;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
article address {
|
||||
font-size: 1em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
article.address {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
article.footer {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/* article */
|
||||
|
||||
article, article address, table.meta, table.inventory {
|
||||
margin: 2em 0 2em;
|
||||
}
|
||||
|
||||
article:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
clip: rect(0 0 0 0);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
article p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* table meta & balance */
|
||||
|
||||
table.meta:after, table.balance:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* table meta */
|
||||
|
||||
table.meta {
|
||||
float: right;
|
||||
width: 50%;
|
||||
font-size: 90%;
|
||||
|
||||
th {
|
||||
text-align: right;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
/* table balance */
|
||||
|
||||
table.balance {
|
||||
float: right;
|
||||
th {
|
||||
text-align: right;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* table items */
|
||||
|
||||
table.inventory {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
margin-top: 2em;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
table.inventory th {
|
||||
font-weight: bold;
|
||||
border-width: 0 0 1px 0;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
table.inventory th,
|
||||
table.inventory td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(1),
|
||||
table.inventory th:nth-child(1) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(2),
|
||||
table.inventory th:nth-child(2) {
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(3),
|
||||
table.inventory th:nth-child(3) {
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
table.inventory td:nth-child(4),
|
||||
table.inventory th:nth-child(4) {
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
/* footer */
|
||||
|
||||
footer {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
footer table {
|
||||
border-top: 1px solid #000;
|
||||
font-size: 0.8em;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
footer th {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
footer table th:nth-child(1) {
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
footer table th:nth-child(3) {
|
||||
width: 28%;
|
||||
}
|
||||
|
||||
footer td {
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
/* aside */
|
||||
|
||||
aside {
|
||||
position: absolute;
|
||||
top: 10in;
|
||||
width: 7.5in;
|
||||
}
|
||||
|
||||
aside h1 {
|
||||
border: none;
|
||||
border-width: 0 0 1px;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
aside h1 {
|
||||
border-color: #999;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
aside p {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
aside div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
aside div p {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
aside p span {
|
||||
width: 130px;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
* {
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
body, .invoice, .wrapper, #freelancer-invoice {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#freelancer-invoice {
|
||||
margin: 1em 2em;
|
||||
|
||||
span:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.add, .cut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
assets/sass/navbar.scss
Normal file
46
assets/sass/navbar.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.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;
|
||||
}
|
||||
*/
|
||||
47
assets/sass/toolbar.scss
Normal file
47
assets/sass/toolbar.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* 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: $screen-sm-min) {
|
||||
.toolbar {
|
||||
form.navbar-form {
|
||||
font-size: $font-size-base;
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.control-label {
|
||||
width: 100%;
|
||||
}
|
||||
.input-group {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,48 +7,54 @@
|
||||
"php": "^7.1.3",
|
||||
"ext-intl": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_sqlite": "*",
|
||||
"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",
|
||||
"kevinpapst/adminlte-bundle": "^1.2",
|
||||
"fzaninotto/faker": "^1.8",
|
||||
"jms/serializer-bundle": "^2.4",
|
||||
"kevinpapst/adminlte-bundle": "~2.1",
|
||||
"nelmio/api-doc-bundle": "^3.2",
|
||||
"ocramius/proxy-manager": "2.1.1",
|
||||
"sensio/framework-extra-bundle": "^5.1",
|
||||
"phpoffice/phpspreadsheet": "^1.4",
|
||||
"phpoffice/phpword": "^0.15.0",
|
||||
"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/lts": "^4@dev",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/orm-pack": "^1.0",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
"symfony/security-bundle": "^4.0",
|
||||
"symfony/security-csrf": "^4.0",
|
||||
"symfony/serializer": "^4.0",
|
||||
"symfony/swiftmailer-bundle": "^3.1",
|
||||
"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",
|
||||
"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": {
|
||||
@@ -90,13 +96,8 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": false
|
||||
"allow-contrib": true,
|
||||
"require": "~4.1.0"
|
||||
}
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/kevinpapst/AdminThemeBundle"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
4016
composer.lock
generated
4016
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -19,4 +19,7 @@ return [
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true],
|
||||
FOS\UserBundle\FOSUserBundle::class => ['all' => true],
|
||||
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
|
||||
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
|
||||
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -23,7 +23,7 @@ admin_lte:
|
||||
# https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/docs/control_sidebar.md
|
||||
control_sidebar:
|
||||
settings:
|
||||
icon: "fas fa-cogs"
|
||||
icon: "fas fa-user"
|
||||
controller: 'App\Controller\SidebarController::settingsAction'
|
||||
home:
|
||||
icon: "fas fa-question-circle"
|
||||
@@ -31,7 +31,7 @@ admin_lte:
|
||||
|
||||
routes:
|
||||
adminlte_profile: user_profile
|
||||
adminlte_welcome: dashboard
|
||||
adminlte_welcome: homepage
|
||||
adminlte_login: fos_user_security_login
|
||||
adminlte_login_check: fos_user_security_check
|
||||
adminlte_registration: fos_user_registration_register
|
||||
|
||||
7
config/packages/dev/jms_serializer.yaml
Normal file
7
config/packages/dev/jms_serializer.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json:
|
||||
options:
|
||||
- JSON_PRETTY_PRINT
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
107
config/packages/fos_rest.yaml
Normal file
107
config/packages/fos_rest.yaml
Normal file
@@ -0,0 +1,107 @@
|
||||
fos_rest:
|
||||
# disable_csrf_role: IS_AUTHENTICATED_ANONYMOUSLY
|
||||
# access_denied_listener:
|
||||
# enabled: false
|
||||
# service: null
|
||||
# formats:
|
||||
# name: ~
|
||||
# unauthorized_challenge: null
|
||||
param_fetcher_listener:
|
||||
enabled: true
|
||||
# force: false
|
||||
# service: null
|
||||
cache_dir: '%kernel.cache_dir%/fos_rest'
|
||||
# allowed_methods_listener:
|
||||
# enabled: false
|
||||
# service: null
|
||||
routing_loader:
|
||||
default_format: json
|
||||
include_format: true
|
||||
# prefix_methods: true
|
||||
body_converter:
|
||||
enabled: true
|
||||
# validate: false
|
||||
# validation_errors_argument: validationErrors
|
||||
# service:
|
||||
# router: router
|
||||
# templating: templating
|
||||
# serializer: null
|
||||
# view_handler: fos_rest.view_handler.default
|
||||
# inflector: fos_rest.inflector.doctrine
|
||||
# validator: validator
|
||||
# serializer:
|
||||
# version: null
|
||||
# groups: []
|
||||
# serialize_null: false
|
||||
view:
|
||||
default_engine: twig
|
||||
# force_redirects:
|
||||
# name: ~
|
||||
# mime_types:
|
||||
# enabled: false
|
||||
# service: null
|
||||
# formats:
|
||||
# name: ~
|
||||
# formats:
|
||||
# name: ~
|
||||
# templating_formats:
|
||||
# name: ~
|
||||
view_response_listener:
|
||||
enabled: true
|
||||
# force: true
|
||||
# service: null
|
||||
failed_validation: 400
|
||||
# empty_content: 204
|
||||
# serialize_null: false
|
||||
# jsonp_handler:
|
||||
# callback_param: callback
|
||||
# mime_type: application/javascript+jsonp
|
||||
exception:
|
||||
enabled: true
|
||||
exception_controller: 'fos_rest.exception.controller:showAction'
|
||||
# service: null
|
||||
codes:
|
||||
# 'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
|
||||
# 'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
|
||||
# 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': 400
|
||||
messages:
|
||||
'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
|
||||
'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': true
|
||||
body_listener:
|
||||
enabled: true
|
||||
# service: null
|
||||
# default_format: null
|
||||
# throw_exception_on_unsupported_content_type: false
|
||||
# decoders:
|
||||
# name: ~
|
||||
array_normalizer:
|
||||
service: null
|
||||
forms: true
|
||||
format_listener:
|
||||
enabled: true
|
||||
# service: null
|
||||
rules:
|
||||
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json, xml ] }
|
||||
# host: null # URL host name
|
||||
# methods: null # Method for URL
|
||||
# stop: false
|
||||
# attributes: []
|
||||
# versioning:
|
||||
# enabled: false
|
||||
# default_version: ~
|
||||
# resolvers:
|
||||
# query:
|
||||
# enabled: true
|
||||
# parameter_name: version
|
||||
# custom_header:
|
||||
# enabled: true
|
||||
# header_name: X-Accept-Version
|
||||
# media_type:
|
||||
# enabled: true
|
||||
# regex: /(v|version)=(?P<version>[0-9\.]+)/
|
||||
# guessing_order:
|
||||
# - query
|
||||
# - custom_header
|
||||
# - media_type
|
||||
zone:
|
||||
- { path: ^/api/* }
|
||||
15
config/packages/jms_serializer.yaml
Normal file
15
config/packages/jms_serializer.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
jms_serializer:
|
||||
handlers:
|
||||
datetime:
|
||||
default_format: "Y-m-d H:i"
|
||||
visitors:
|
||||
xml:
|
||||
format_output: '%kernel.debug%'
|
||||
metadata:
|
||||
directories:
|
||||
FOSUB:
|
||||
namespace_prefix: "FOS\\UserBundle"
|
||||
path: "%kernel.root_dir%/../config/serializer/FOS/UserBundle"
|
||||
App:
|
||||
namespace_prefix: "App"
|
||||
path: "%kernel.root_dir%/../config/serializer/App"
|
||||
@@ -1,8 +1,18 @@
|
||||
kimai:
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Settings for the user management and login forms
|
||||
user:
|
||||
registration: true
|
||||
password_reset: true
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# All configs related to timesheet and record management
|
||||
timesheet:
|
||||
|
||||
# render timesheet descriptions with markdown
|
||||
markdown_content: false
|
||||
|
||||
# Whether we display start and end time columns (false) or durations only (true).
|
||||
# Setting this to true will also change the "edit timesheet" forms, more infos available in the configurations docu.
|
||||
duration_only: false
|
||||
@@ -25,28 +35,196 @@ kimai:
|
||||
# days: ['saturday','sunday']
|
||||
# factor: 1.5
|
||||
|
||||
# All configs related to the invoice administration
|
||||
invoice:
|
||||
renderer:
|
||||
default: 'App\Controller\InvoiceController::invoiceAction'
|
||||
timesheet: 'App\Controller\InvoiceController::timesheetAction'
|
||||
calculator:
|
||||
default: 'App\Invoice\DefaultCalculator'
|
||||
short: 'App\Invoice\ShortInvoiceCalculator'
|
||||
number_generator:
|
||||
default: 'App\Invoice\DateNumberGenerator'
|
||||
# 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)
|
||||
# documents:
|
||||
# - '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:
|
||||
date_short: "d.m.Y"
|
||||
date_short: 'd.m.Y'
|
||||
duration: '%%h:%%m Stunden'
|
||||
duration_short: '%%h:%%m h'
|
||||
en:
|
||||
date_short: "Y-m-d"
|
||||
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'
|
||||
duration_short: '%%h:%%m h'
|
||||
fr:
|
||||
date_short: 'd/m/Y'
|
||||
duration: '%%h h %%m min'
|
||||
duration_short: '%%h h %%m'
|
||||
es:
|
||||
date_short: 'd.m.Y'
|
||||
duration: '%%h:%%m h'
|
||||
duration_short: '%%h:%%m h'
|
||||
ru:
|
||||
date_short: "d.m.Y"
|
||||
date_short: 'd.m.Y'
|
||||
duration: '%%h:%%m Часов'
|
||||
duration_short: '%%h:%%m h'
|
||||
ar:
|
||||
date_short: 'Y-m-d'
|
||||
duration: '%%h:%%m hours'
|
||||
duration_short: '%%h:%%m h'
|
||||
hu:
|
||||
date_short: 'Y.m.d.'
|
||||
duration: '%%h:%%m óra'
|
||||
duration_short: '%%h:%%m h'
|
||||
|
||||
twig:
|
||||
globals:
|
||||
kimai_context:
|
||||
box_color: "green" # a color for ???
|
||||
active_warning: 3 # display a warning color if the user has at least X active recordings
|
||||
# --------------------------------------------------------------------------------
|
||||
# timesheet calendar configuration
|
||||
calendar:
|
||||
week_numbers: true
|
||||
day_limit: 4
|
||||
businessHours:
|
||||
days: [1, 2, 3, 4, 5]
|
||||
begin: '08:00'
|
||||
end: '20:00'
|
||||
|
||||
# You can configure unlimited google calendars to display events for your company (e.g. holidays)
|
||||
# Please read var/docs/configurations.md to find out more about calendar integration
|
||||
# google:
|
||||
# api_key: 'your-restricted-google-api-key'
|
||||
# sources:
|
||||
# holidays:
|
||||
# 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: ~
|
||||
# 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:
|
||||
title: dashboard.you
|
||||
order: 10
|
||||
permission: ROLE_USER
|
||||
widgets: [userDurationToday, userDurationWeek, userDurationMonth, userDurationYear]
|
||||
user_rates:
|
||||
title: ~
|
||||
order: 20
|
||||
permission: ROLE_USER
|
||||
widgets: [userAmountToday, userAmountWeek, userAmountMonth, userAmountYear]
|
||||
duration:
|
||||
title: dashboard.all
|
||||
order: 30
|
||||
permission: ROLE_TEAMLEAD
|
||||
widgets: [durationToday, durationWeek, durationMonth, durationYear]
|
||||
active_users:
|
||||
title: ~
|
||||
order: 40
|
||||
permission: ROLE_TEAMLEAD
|
||||
widgets: [activeUsersToday, activeUsersWeek, activeUsersMonth, activeUsersYear]
|
||||
rates:
|
||||
title: ~
|
||||
order: 50
|
||||
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 }
|
||||
userDurationWeek: { title: stats.durationWeek, query: duration, user: true, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: duration, color: blue }
|
||||
userDurationMonth: { title: stats.durationMonth, query: duration, user: true, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: duration, color: purple }
|
||||
userDurationYear: { title: stats.durationYear, query: duration, user: true, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: duration, color: yellow }
|
||||
userDurationTotal: { title: stats.durationTotal, query: duration, user: true, icon: duration, color: red }
|
||||
userAmountToday: { title: stats.amountToday, query: rate, user: true, begin: '00:00:00', end: '23:59:59', icon: money, color: green }
|
||||
userAmountWeek: { title: stats.amountWeek, query: rate, user: true, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: money, color: blue }
|
||||
userAmountMonth: { title: stats.amountMonth, query: rate, user: true, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: money, color: purple }
|
||||
userAmountYear: { title: stats.amountYear, query: rate, user: true, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: money, color: yellow }
|
||||
userAmountTotal: { title: stats.amountTotal, query: rate, user: true, icon: money, color: red }
|
||||
durationToday: { title: stats.durationToday, query: duration, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
|
||||
durationWeek: { title: stats.durationWeek, query: duration, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: duration, color: blue }
|
||||
durationMonth: { title: stats.durationMonth, query: duration, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: duration, color: purple }
|
||||
durationYear: { title: stats.durationYear, query: duration, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: duration, color: yellow }
|
||||
durationTotal: { title: stats.durationTotal, query: duration, icon: duration, color: red }
|
||||
amountToday: { title: stats.amountToday, query: rate, begin: '00:00:00', end: '23:59:59', icon: money, color: green }
|
||||
amountWeek: { title: stats.amountWeek, query: rate, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: money, color: blue }
|
||||
amountMonth: { title: stats.amountMonth, query: rate, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: money, color: purple }
|
||||
amountYear: { title: stats.amountYear, query: rate, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: money, color: yellow }
|
||||
amountTotal: { title: stats.amountTotal, query: rate, icon: money, color: red }
|
||||
activeUsersToday: { title: stats.userActiveToday, query: users, begin: '00:00:00', end: '23:59:59', icon: user, color: green }
|
||||
activeUsersWeek: { title: stats.userActiveWeek, query: users, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: user, color: blue }
|
||||
activeUsersMonth: { title: stats.userActiveMonth, query: users, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: user, color: purple }
|
||||
activeUsersYear: { title: stats.userActiveYear, query: users, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: user, color: yellow }
|
||||
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
|
||||
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }
|
||||
|
||||
# Default settings used to populate forms
|
||||
#defaults:
|
||||
# customer:
|
||||
# timezone: Europe/Berlin
|
||||
# country: DE
|
||||
# currency: EUR
|
||||
46
config/packages/nelmio_api_doc.yaml
Normal file
46
config/packages/nelmio_api_doc.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
nelmio_api_doc:
|
||||
models:
|
||||
use_jms: true
|
||||
names:
|
||||
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer] }
|
||||
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
|
||||
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project] }
|
||||
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
|
||||
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
|
||||
- { alias: TimesheetFormEntity, type: App\Form\TimesheetEditForm, groups: [Default, Entity, Timesheet] }
|
||||
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] }
|
||||
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
|
||||
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
||||
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
|
||||
areas:
|
||||
path_patterns:
|
||||
- ^/api(?!/doc)
|
||||
documentation:
|
||||
# host: '{{hostname}}'
|
||||
schemes: [http, https]
|
||||
info:
|
||||
title: Kimai 2 - API Docs
|
||||
description: REST API description for the Kimai 2 time-tracking software. Do not rely on the example values/models, they are currently generated wrong. Please fetch the request and response structure from the API itself.
|
||||
version: 0.2
|
||||
# parameters:
|
||||
# hostname:
|
||||
# name: hostname
|
||||
# in: url
|
||||
# description: the hostname where your api runs
|
||||
# required: true
|
||||
# type: string
|
||||
securityDefinitions:
|
||||
X-AUTH-USER:
|
||||
type: apiKey
|
||||
description: 'Value: {Username}'
|
||||
name: X-AUTH-USER
|
||||
in: header
|
||||
X-AUTH-TOKEN:
|
||||
type: apiKey
|
||||
description: 'Value: {API Token}'
|
||||
name: X-AUTH-TOKEN
|
||||
in: header
|
||||
security:
|
||||
- X-AUTH-USER: []
|
||||
- X-AUTH-TOKEN: []
|
||||
6
config/packages/prod/jms_serializer.yaml
Normal file
6
config/packages/prod/jms_serializer.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json:
|
||||
options:
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
@@ -2,7 +2,6 @@ security:
|
||||
encoders:
|
||||
App\Entity\User: bcrypt
|
||||
|
||||
# https://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
|
||||
providers:
|
||||
fos_userbundle:
|
||||
id: fos_user.user_provider.username_email
|
||||
@@ -13,39 +12,28 @@ security:
|
||||
security: false
|
||||
|
||||
secured_area:
|
||||
# this firewall applies to all URLs
|
||||
pattern: ^/
|
||||
|
||||
# make sure only allowed users have access to their time-tracking
|
||||
user_checker: App\Security\UserChecker
|
||||
anonymous: true
|
||||
stateless: false
|
||||
|
||||
# but the firewall does not require login on every page
|
||||
# denying access is done in access_control or in your controllers
|
||||
anonymous: ~
|
||||
guard:
|
||||
authenticators:
|
||||
- App\Security\TokenAuthenticator
|
||||
|
||||
remember_me:
|
||||
secret: '%kernel.secret%'
|
||||
lifetime: 604800
|
||||
path: /
|
||||
|
||||
# This allows the user to login by submitting a username and password
|
||||
# Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html
|
||||
form_login:
|
||||
# fos user bundle handles the form login
|
||||
provider: fos_userbundle
|
||||
# The route name that the login form submits to
|
||||
check_path: fos_user_security_check
|
||||
# The name of the route where the login form lives
|
||||
# When the user tries to access a protected page, they are redirected here
|
||||
login_path: fos_user_security_login
|
||||
# Secure the login form against CSRF
|
||||
# Reference: http://symfony.com/doc/current/cookbook/security/csrf_in_login_form.html
|
||||
csrf_token_generator: security.csrf.token_manager
|
||||
|
||||
logout:
|
||||
# The route name the user can go to in order to logout
|
||||
path: fos_user_security_logout
|
||||
# The name of the route to redirect to after logging out
|
||||
target: homepage
|
||||
|
||||
role_hierarchy:
|
||||
@@ -59,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 }
|
||||
|
||||
3
config/packages/sensio_framework_extra.yaml
Normal file
3
config/packages/sensio_framework_extra.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
sensio_framework_extra:
|
||||
router:
|
||||
annotations: false
|
||||
@@ -2,3 +2,5 @@ twig:
|
||||
#paths: ['%kernel.project_dir%/templates']
|
||||
debug: '%kernel.debug%'
|
||||
strict_variables: '%kernel.debug%'
|
||||
form_themes:
|
||||
- 'form/kimai-theme.html.twig'
|
||||
|
||||
@@ -4,22 +4,3 @@ home:
|
||||
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
||||
route: homepage
|
||||
permanent: true
|
||||
|
||||
homepage:
|
||||
path: /{_locale}
|
||||
defaults:
|
||||
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
||||
route: timesheet
|
||||
permanent: true
|
||||
|
||||
fos_user_security:
|
||||
prefix: /{_locale}
|
||||
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
|
||||
|
||||
fos_user_registration:
|
||||
prefix: /{_locale}/register
|
||||
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
|
||||
|
||||
fos_user_resetting:
|
||||
prefix: /{_locale}/resetting
|
||||
resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
|
||||
|
||||
@@ -5,4 +5,9 @@ controllers:
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
_locale: '%locale%'
|
||||
|
||||
app.api:
|
||||
resource: '../src/API/'
|
||||
type: rest
|
||||
prefix: /api
|
||||
|
||||
12
config/routes/fos_user.yaml
Normal file
12
config/routes/fos_user.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# Expose security related features like login and logout
|
||||
fos_user_security:
|
||||
prefix: /{_locale}
|
||||
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
|
||||
|
||||
# The features "user registration" and "password-reset" are enabled by default.
|
||||
#
|
||||
# You can disable them by setting the config keys in file "config/kimai.yaml":
|
||||
# - kimai.user.registration: false
|
||||
# - kimai.user.password_reset: false
|
||||
#
|
||||
# The routes for these functions are added dynamically in src/Kernel.php
|
||||
11
config/routes/nelmio_api_doc.yaml
Normal file
11
config/routes/nelmio_api_doc.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Expose the Kimai API documentation as HTML
|
||||
app.swagger_ui:
|
||||
path: /api/doc
|
||||
methods: GET
|
||||
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
|
||||
|
||||
# Expose the Kimai API documentation as swagger compliant JSON
|
||||
app.swagger:
|
||||
path: /api/doc.json
|
||||
methods: GET
|
||||
defaults: { _controller: nelmio_api_doc.controller.swagger }
|
||||
15
config/routes/test/test_routes.yaml
Normal file
15
config/routes/test/test_routes.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
app.test.activity_recent:
|
||||
path: /{_locale}/activities/recent
|
||||
controller: App\Controller\ActivityController::recentActivitiesAction
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
app.test.sidebar_settings:
|
||||
path: /{_locale}/sidebar/settings
|
||||
controller: App\Controller\SidebarController::settingsAction
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
32
config/serializer/App/Entity.Activity.yml
Normal file
32
config/serializer/App/Entity.Activity.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
App\Entity\Activity:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, comment, visible, project, fixedRate, hourlyRate]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
name:
|
||||
include: true
|
||||
groups: [Default]
|
||||
comment:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
visible:
|
||||
include: true
|
||||
groups: [Default]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
project:
|
||||
include: false
|
||||
exclude: true
|
||||
groups: [Default]
|
||||
virtual_properties:
|
||||
getProject:
|
||||
serialized_name: project
|
||||
exp: "object.getProject() === null ? null : object.getProject().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
58
config/serializer/App/Entity.Customer.yml
Normal file
58
config/serializer/App/Entity.Customer.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
App\Entity\Customer:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixedRate, hourlyRate]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
name:
|
||||
include: true
|
||||
groups: [Default]
|
||||
number:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
comment:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
visible:
|
||||
include: true
|
||||
groups: [Default]
|
||||
company:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
contact:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
address:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
country:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
currency:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
phone:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
fax:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
mobile:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
mail:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
homepage:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
timezone:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
38
config/serializer/App/Entity.Project.yml
Normal file
38
config/serializer/App/Entity.Project.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
App\Entity\Project:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, comment, visible, budget, orderNumber, customer, fixedRate, hourlyRate]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
name:
|
||||
include: true
|
||||
groups: [Default]
|
||||
comment:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
visible:
|
||||
include: true
|
||||
groups: [Default]
|
||||
budget:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
orderNumber:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
customer:
|
||||
include: false
|
||||
exclude: true
|
||||
groups: [Default]
|
||||
virtual_properties:
|
||||
getCustomer:
|
||||
serialized_name: customer
|
||||
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
56
config/serializer/App/Entity.Timesheet.yml
Normal file
56
config/serializer/App/Entity.Timesheet.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
App\Entity\Timesheet:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, begin, end, duration, rate, activity, project, user, description, fixedRate, hourlyRate]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
begin:
|
||||
include: true
|
||||
groups: [Default]
|
||||
end:
|
||||
include: true
|
||||
groups: [Default]
|
||||
duration:
|
||||
include: true
|
||||
groups: [Default]
|
||||
description:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
rate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
activity:
|
||||
include: false
|
||||
exclude: true
|
||||
groups: [Default]
|
||||
project:
|
||||
include: false
|
||||
exclude: true
|
||||
groups: [Default]
|
||||
user:
|
||||
include: false
|
||||
exclude: true
|
||||
groups: [Default]
|
||||
virtual_properties:
|
||||
getActivity:
|
||||
serialized_name: activity
|
||||
exp: "object.getActivity() === null ? null : object.getActivity().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
getProject:
|
||||
serialized_name: project
|
||||
exp: "object.getProject() === null ? null : object.getProject().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
getUser:
|
||||
serialized_name: user
|
||||
exp: "object.getUser().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
15
config/serializer/App/Entity.User.yml
Normal file
15
config/serializer/App/Entity.User.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
App\Entity\User:
|
||||
exclusion_policy: All
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
alias:
|
||||
include: true
|
||||
groups: [Default]
|
||||
title:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
avatar:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
32
config/serializer/FOS/UserBundle/Model.User.yml
Normal file
32
config/serializer/FOS/UserBundle/Model.User.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
FOS\UserBundle\Model\User:
|
||||
exclusion_policy: NONE
|
||||
properties:
|
||||
enabled:
|
||||
include: true
|
||||
groups: [Default]
|
||||
groups:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
roles:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
email:
|
||||
exclude: true
|
||||
emailCanonical:
|
||||
exclude: true
|
||||
usernameCanonical:
|
||||
exclude: true
|
||||
password:
|
||||
exclude: true
|
||||
plainPassword:
|
||||
exclude: true
|
||||
registeredAt:
|
||||
exclude: true
|
||||
lastLogin:
|
||||
exclude: true
|
||||
confirmationToken:
|
||||
exclude: true
|
||||
passwordRequestedAt:
|
||||
exclude: true
|
||||
salt:
|
||||
exclude: true
|
||||
@@ -2,7 +2,7 @@
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: en|de|ru
|
||||
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
|
||||
@@ -32,17 +32,29 @@ services:
|
||||
# APPLICATION CORE
|
||||
# ================================================================================
|
||||
|
||||
App\Invoice\ServiceInvoice:
|
||||
class: App\Invoice\ServiceInvoice
|
||||
arguments: ['%kimai.invoice%']
|
||||
|
||||
# a route listener, that injects the default locale into the route
|
||||
App\EventSubscriber\RedirectToLocaleSubscriber:
|
||||
class: App\EventSubscriber\RedirectToLocaleSubscriber
|
||||
arguments: ['@router', '%app_locales%', '%locale%']
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
|
||||
|
||||
App\Calendar\Service:
|
||||
arguments:
|
||||
$config: "%kimai.calendar%"
|
||||
|
||||
App\Repository\WidgetRepository:
|
||||
arguments:
|
||||
$widgets: "%kimai.widgets%"
|
||||
|
||||
App\Controller\DashboardController:
|
||||
arguments:
|
||||
$dashboard: "%kimai.dashboard%"
|
||||
|
||||
App\Twig\MarkdownExtension:
|
||||
arguments:
|
||||
$timesheetAsMarkdown: "%kimai.timesheet.markdown%"
|
||||
|
||||
App\Controller\Admin\CustomerController:
|
||||
arguments:
|
||||
$defaults: "%kimai.defaults%"
|
||||
|
||||
# ================================================================================
|
||||
# DATABASE
|
||||
# ================================================================================
|
||||
@@ -66,24 +78,36 @@ services:
|
||||
# FORMS
|
||||
# ================================================================================
|
||||
|
||||
# form type to edit user roles
|
||||
App\Form\Type\UserRoleType:
|
||||
class: App\Form\Type\UserRoleType
|
||||
arguments: ["%security.role_hierarchy.roles%"]
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
App\Form\Type\LanguageType:
|
||||
arguments: ["%app_locales%"]
|
||||
|
||||
App\Form\Extension\DocumentationLinkExtension:
|
||||
tags:
|
||||
- { name: form.type_extension, extended_type: Symfony\Component\Form\Extension\Core\Type\FormType }
|
||||
|
||||
App\Form\Extension\EnhancedChoiceTypeExtension:
|
||||
arguments: ["%kimai.theme.select_type%"]
|
||||
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
|
||||
# ================================================================================
|
||||
|
||||
App\Twig\Extensions:
|
||||
arguments:
|
||||
$locales: "%app_locales%"
|
||||
$languages: "%kimai.languages%"
|
||||
|
||||
App\Twig\DateExtensions:
|
||||
arguments:
|
||||
@@ -101,3 +125,49 @@ services:
|
||||
|
||||
App\Timesheet\Calculator\RateCalculator:
|
||||
arguments: ["%kimai.timesheet.rates%"]
|
||||
|
||||
# ================================================================================
|
||||
# SECURITY & VOTER
|
||||
# ================================================================================
|
||||
|
||||
App\Security\RolePermissionManager:
|
||||
arguments: ["%kimai.permissions%"]
|
||||
|
||||
# ================================================================================
|
||||
# REPOSITORIES
|
||||
# ================================================================================
|
||||
|
||||
App\Repository\TimesheetRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Timesheet']
|
||||
|
||||
App\Repository\UserRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\User']
|
||||
|
||||
App\Repository\ActivityRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Activity']
|
||||
|
||||
App\Repository\ProjectRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Project']
|
||||
|
||||
App\Repository\CustomerRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Customer']
|
||||
|
||||
App\Repository\InvoiceTemplateRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\InvoiceTemplate']
|
||||
|
||||
App\Repository\InvoiceDocumentRepository:
|
||||
class: App\Repository\InvoiceDocumentRepository
|
||||
arguments: ['%kimai.invoice.documents%']
|
||||
|
||||
|
||||
@@ -2,8 +2,4 @@ services:
|
||||
_defaults:
|
||||
public: true
|
||||
|
||||
# If you need to access services in a test, create an alias
|
||||
# and then fetch that alias from the container. As a convention,
|
||||
# aliases are prefixed with test. For example:
|
||||
#
|
||||
# test.App\Service\MyService: '@App\Service\MyService'
|
||||
test.PasswordEncoder: "@security.encoder_factory"
|
||||
@@ -4,7 +4,7 @@ App\Entity\User:
|
||||
- App\Validator\Constraints\Role: ~
|
||||
username:
|
||||
- NotBlank: ~
|
||||
- Length: { min: 5, max: 60 }
|
||||
- Length: { min: 3, max: 60 }
|
||||
email:
|
||||
- NotBlank: ~
|
||||
- Email: ~
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "~5.1.1",
|
||||
"@symfony/webpack-encore": "^0.20.1",
|
||||
"admin-lte": "~2.3.11",
|
||||
"admin-lte": "~2.4.8",
|
||||
"bootstrap-sass": "^3.3.7",
|
||||
"bootstrap-select": "^1.13.1",
|
||||
"chart.js": "1.1.*",
|
||||
"chart.js": "~2.7.2",
|
||||
"daterangepicker": "^3.0.3",
|
||||
"fullcalendar": "^3.9.0",
|
||||
"icheck": "^1.0.2",
|
||||
"jquery": "^3.3.1",
|
||||
"jquery-slimscroll": "^1.3.8",
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<env name="DATABASE_PREFIX" value="kimai2_" force="true"/>
|
||||
<!-- define your env variables for the test env here -->
|
||||
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
||||
|
||||
<!-- ###+ doctrine/doctrine-bundle ### -->
|
||||
<env name="DATABASE_URL" value="sqlite:///var/data/kimai_test.sqlite" force="true"/>
|
||||
<!-- ###- doctrine/doctrine-bundle ### -->
|
||||
@@ -37,9 +39,11 @@
|
||||
<directory suffix=".php">src/</directory>
|
||||
<directory suffix=".php">templates/</directory>
|
||||
<exclude>
|
||||
<directory suffix=".php">src/Migrations/</directory>
|
||||
<directory suffix=".php">assets/</directory>
|
||||
<directory suffix=".php">bin/</directory>
|
||||
<directory suffix=".php">config/</directory>
|
||||
<directory suffix=".php">node_modules/</directory>
|
||||
<directory suffix=".php">public/</directory>
|
||||
<directory suffix=".php">tests/</directory>
|
||||
<directory suffix=".php">translations/</directory>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||
# start page (path "/") because otherwise Apache will apply the rewriting rules
|
||||
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||
DirectoryIndex app.php
|
||||
DirectoryIndex index.php
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
@@ -30,7 +30,7 @@ DirectoryIndex app.php
|
||||
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||
# following RewriteCond (best solution)
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} ^$
|
||||
RewriteRule ^app\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
|
||||
RewriteRule ^index\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
|
||||
|
||||
# If the requested filename exists, simply serve it.
|
||||
# We only want to let Apache serve files and not directories.
|
||||
@@ -38,7 +38,7 @@ DirectoryIndex app.php
|
||||
RewriteRule .? - [L]
|
||||
|
||||
# Rewrite all other queries to the front controller.
|
||||
RewriteRule .? %{ENV:BASE}/app.php [L]
|
||||
RewriteRule .? %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_rewrite.c>
|
||||
@@ -46,7 +46,7 @@ DirectoryIndex app.php
|
||||
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||
# the start page to the front controller explicitly so that the website
|
||||
# and the generated links can still be used.
|
||||
RedirectMatch 302 ^/$ /app.php/
|
||||
RedirectMatch 302 ^/$ /index.php/
|
||||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB |
BIN
public/build/images/signature.png
Normal file
BIN
public/build/images/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"build/app.js": "/build/app.js?df75851c817558ca8803",
|
||||
"build/app.css": "/build/app.css?b1944e8d2023603075ed8d913c21a8e8",
|
||||
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
|
||||
"build/images/boxed-bg.jpg": "/build/images/boxed-bg.jpg?7799dece",
|
||||
"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",
|
||||
"build/images/fa-solid-900.svg": "/build/images/fa-solid-900.svg?666a82cb",
|
||||
@@ -14,7 +13,7 @@
|
||||
"build/fonts/fa-brands-400.woff": "/build/fonts/fa-brands-400.woff?e9f83339",
|
||||
"build/fonts/glyphicons-halflings-regular.woff2": "/build/fonts/glyphicons-halflings-regular.woff2?448c34a5",
|
||||
"build/fonts/fa-solid-900.eot": "/build/fonts/fa-solid-900.eot?0b93480e",
|
||||
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
|
||||
"build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?d9e29124",
|
||||
"build/fonts/fa-regular-400.woff2": "/build/fonts/fa-regular-400.woff2?930c1264",
|
||||
"build/images/fa-regular-400.svg": "/build/images/fa-regular-400.svg?4e41caaf",
|
||||
@@ -24,5 +23,6 @@
|
||||
"build/fonts/fa-brands-400.woff2": "/build/fonts/fa-brands-400.woff2?66f625f1",
|
||||
"build/fonts/glyphicons-halflings-regular.ttf": "/build/fonts/glyphicons-halflings-regular.ttf?e18bbf61",
|
||||
"build/images/fa-brands-400.svg": "/build/images/fa-brands-400.svg?5da7e33c",
|
||||
"build/images/default_avatar.png": "/build/images/default_avatar.png?fc197350"
|
||||
"build/images/default_avatar.png": "/build/images/default_avatar.png?fc197350",
|
||||
"build/images/signature.png": "/build/images/signature.png?5f58501e"
|
||||
}
|
||||
127
src/API/ActivityController.php
Normal file
127
src/API/ActivityController.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Activity")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class ActivityController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param ActivityRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing activities",
|
||||
* @SWG\Schema(ref="#/definitions/ActivityCollection"),
|
||||
* )
|
||||
* @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
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setOrderGlobalsFirst(true)
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one activity entity",
|
||||
* @SWG\Schema(ref="#/definitions/ActivityEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$data = $this->repository->find($id);
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
18
src/API/BaseApiController.php
Normal file
18
src/API/BaseApiController.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
|
||||
abstract class BaseApiController extends Controller
|
||||
{
|
||||
}
|
||||
112
src/API/CustomerController.php
Normal file
112
src/API/CustomerController.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Customer")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class CustomerController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param CustomerRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing customer",
|
||||
* @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)
|
||||
->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);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one customer entity",
|
||||
* @SWG\Schema(ref="#/definitions/CustomerEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$data = $this->repository->find($id);
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
72
src/API/HealthcheckController.php
Normal file
72
src/API/HealthcheckController.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Constants;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
|
||||
class HealthcheckController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="A simple route that returns a 'pong', which you can use for testing the API",
|
||||
* examples={"{'message': 'pong'}"}
|
||||
* )
|
||||
*
|
||||
* @Rest\Get(path="/ping")
|
||||
*/
|
||||
public function pingAction()
|
||||
{
|
||||
$view = new View(['message' => 'pong'], 200);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns version information about the current release",
|
||||
* )
|
||||
*
|
||||
* @Rest\Get(path="/version")
|
||||
*/
|
||||
public function versionAction()
|
||||
{
|
||||
$version = [
|
||||
'version' => Constants::VERSION,
|
||||
'candidate' => Constants::STATUS,
|
||||
'semver' => Constants::VERSION . '-' . Constants::STATUS,
|
||||
'name' => Constants::NAME,
|
||||
'copyright' => 'Kimai 2 - ' . Constants::VERSION . ' ' . Constants::STATUS . ' (' . Constants::NAME . ') by Kevin Papst and contributors.',
|
||||
];
|
||||
|
||||
return $this->viewHandler->handle(new View($version, 200));
|
||||
}
|
||||
}
|
||||
22
src/API/NotFoundException.php
Normal file
22
src/API/NotFoundException.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class NotFoundException extends NotFoundHttpException
|
||||
{
|
||||
public function __construct(string $message = 'Not found', \Exception $previous = null, int $code = 0, array $headers = [])
|
||||
{
|
||||
parent::__construct($message, $previous, $code, $headers);
|
||||
}
|
||||
}
|
||||
118
src/API/ProjectController.php
Normal file
118
src/API/ProjectController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Project")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class ProjectController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param ProjectRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing projects",
|
||||
* @SWG\Schema(ref="#/definitions/ProjectCollection"),
|
||||
* )
|
||||
* @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
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$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);
|
||||
}
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one project entity",
|
||||
* @SWG\Schema(ref="#/definitions/ProjectEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$data = $this->repository->find($id);
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
216
src/API/TimesheetController.php
Normal file
216
src/API/TimesheetController.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Timesheet")
|
||||
*/
|
||||
class TimesheetController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $hardLimit;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param TimesheetRepository $repository
|
||||
* @param int $hardLimit
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, int $hardLimit)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->hardLimit = $hardLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing timesheets for the user",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetCollection"),
|
||||
* )
|
||||
* @Rest\QueryParam(name="customer", requirements="\d+", strict=true, nullable=true, description="Customer ID to filter timesheets")
|
||||
* @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter timesheets")
|
||||
* @Rest\QueryParam(name="activity", requirements="\d+", strict=true, nullable=true, description="Activity ID to filter timesheets")
|
||||
* @Rest\QueryParam(name="page", requirements="\d+", strict=true, nullable=true, description="The page to display, renders a 404 if not found (default: 1)")
|
||||
* @Rest\QueryParam(name="size", requirements="\d+", strict=true, nullable=true, description="The amount of entries for each page (default: 25)")
|
||||
* @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)
|
||||
{
|
||||
$query = new TimesheetQuery();
|
||||
$query->setUser($this->getUser());
|
||||
$query->setResultType(TimesheetQuery::RESULT_TYPE_PAGER);
|
||||
|
||||
if (null !== ($customer = $paramFetcher->get('customer'))) {
|
||||
$query->setCustomer($customer);
|
||||
}
|
||||
|
||||
if (null !== ($project = $paramFetcher->get('project'))) {
|
||||
$query->setProject($project);
|
||||
}
|
||||
|
||||
if (null !== ($activity = $paramFetcher->get('activity'))) {
|
||||
$query->setActivity($activity);
|
||||
}
|
||||
|
||||
if (null !== ($page = $paramFetcher->get('page'))) {
|
||||
$query->setPage($page);
|
||||
}
|
||||
|
||||
if (null !== ($size = $paramFetcher->get('size'))) {
|
||||
$query->setPageSize($size);
|
||||
}
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
if (null !== ($orderBy = $paramFetcher->get('orderBy'))) {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
/** @var Pagerfanta $data */
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$data = (array) $data->getCurrentPageResults();
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one timesheet entity",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity")
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$data = $this->repository->find($id);
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Post(
|
||||
* description="Creates a new timesheet entry and returns it afterwards",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetFormEntity"),
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the new created timesheet entry",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity"),
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function postAction(Request $request)
|
||||
{
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setUser($this->getUser());
|
||||
$timesheet->setBegin(new \DateTime());
|
||||
|
||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||
'csrf_protection' => false,
|
||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||
]);
|
||||
|
||||
$form->setData($timesheet);
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
if (null !== $timesheet->getId()) {
|
||||
return new Response('This method does not support updates', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!$this->isGranted('start', $timesheet)) {
|
||||
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();
|
||||
|
||||
$view = new View($timesheet, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$view = new View($form);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
91
src/API/UserController.php
Normal file
91
src/API/UserController.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("User")
|
||||
*/
|
||||
class UserController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param UserRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all registered users",
|
||||
* @SWG\Schema(ref="#/definitions/UserCollection"),
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction()
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'User']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Return one user entity",
|
||||
* @SWG\Schema(ref="#/definitions/UserEntity"),
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$data = $this->repository->find($id);
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'User']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
66
src/Calendar/Config.php
Normal file
66
src/Calendar/Config.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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\Calendar;
|
||||
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getBusinessDays()
|
||||
{
|
||||
return $this->config['businessHours']['days'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBusinessTimeBegin()
|
||||
{
|
||||
return $this->config['businessHours']['begin'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBusinessTimeEnd()
|
||||
{
|
||||
return $this->config['businessHours']['end'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDayLimit()
|
||||
{
|
||||
return $this->config['day_limit'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isShowWeekNumbers()
|
||||
{
|
||||
return $this->config['week_numbers'];
|
||||
}
|
||||
}
|
||||
48
src/Calendar/Google.php
Normal file
48
src/Calendar/Google.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Calendar;
|
||||
|
||||
class Google
|
||||
{
|
||||
/**
|
||||
* @var Source[]
|
||||
*/
|
||||
protected $sources = [];
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $apiKey = null;
|
||||
|
||||
/**
|
||||
* @param string $apiKey
|
||||
* @param Source[] $sources
|
||||
*/
|
||||
public function __construct($apiKey, $sources = [])
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
$this->sources = $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getSources()
|
||||
{
|
||||
return $this->sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getApiKey()
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
}
|
||||
59
src/Calendar/Service.php
Normal file
59
src/Calendar/Service.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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\Calendar;
|
||||
|
||||
class Service
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Service constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Config
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return new Config($this->config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Google
|
||||
*/
|
||||
public function getGoogle()
|
||||
{
|
||||
$apiKey = $this->config['google']['api_key'] ?? null;
|
||||
$sources = [];
|
||||
|
||||
if (isset($this->config['google']['sources'])) {
|
||||
foreach ($this->config['google']['sources'] as $name => $config) {
|
||||
$source = new Source();
|
||||
$source
|
||||
->setColor($config['color'])
|
||||
->setUri($config['id'])
|
||||
->setId($name)
|
||||
;
|
||||
|
||||
$sources[] = $source;
|
||||
}
|
||||
}
|
||||
|
||||
return new Google($apiKey, $sources);
|
||||
}
|
||||
}
|
||||
83
src/Calendar/Source.php
Normal file
83
src/Calendar/Source.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?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\Calendar;
|
||||
|
||||
class Source
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $uri;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $color;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return Source
|
||||
*/
|
||||
public function setId(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUri(): ?string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @return Source
|
||||
*/
|
||||
public function setUri(string $uri)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $color
|
||||
* @return Source
|
||||
*/
|
||||
public function setColor(string $color)
|
||||
{
|
||||
$this->color = $color;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
268
src/Calendar/TimesheetEntity.php
Normal file
268
src/Calendar/TimesheetEntity.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?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\Calendar;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
class TimesheetEntity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $id;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
protected $start;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
protected $end;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $title;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $customer;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $project;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $activity;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $borderColor;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $backgroundColor;
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
*/
|
||||
public function __construct(Timesheet $entry)
|
||||
{
|
||||
$this->id = $entry->getId();
|
||||
$this->start = $entry->getBegin();
|
||||
$this->title = $entry->getActivity()->getName();
|
||||
$this->description = $entry->getDescription();
|
||||
$this->customer = $entry->getProject()->getCustomer()->getName();
|
||||
$this->project = $entry->getProject()->getName();
|
||||
$this->activity = $entry->getActivity()->getName();
|
||||
|
||||
if (null === $entry->getEnd()) {
|
||||
// TODO move these colors to the controller
|
||||
$this->borderColor = '367fa9'; //'#f39c12';
|
||||
$this->backgroundColor = '#3c8dbc'; //'#f39c12';
|
||||
} else {
|
||||
$this->end = $entry->getEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setId(int $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getStart(): \DateTime
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $start
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setStart(\DateTime $start)
|
||||
{
|
||||
$this->start = $start;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getEnd(): ?\DateTime
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime|null $end
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setEnd(?\DateTime $end)
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setTitle(string $title)
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setDescription(string $description)
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCustomer(): string
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $customer
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setCustomer(string $customer)
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getProject(): string
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $project
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setProject(string $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getActivity(): string
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $activity
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setActivity(string $activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBorderColor(): ?string
|
||||
{
|
||||
return $this->borderColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $borderColor
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setBorderColor(?string $borderColor)
|
||||
{
|
||||
$this->borderColor = $borderColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBackgroundColor(): ?string
|
||||
{
|
||||
return $this->backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $backgroundColor
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setBackgroundColor(?string $backgroundColor)
|
||||
{
|
||||
$this->backgroundColor = $backgroundColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ class CreateUserCommand extends Command
|
||||
$pwd = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($pwd);
|
||||
|
||||
$errors = $this->validator->validate($user);
|
||||
$errors = $this->validator->validate($user, null, ['registration']);
|
||||
if ($errors->count() > 0) {
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolation $error */
|
||||
foreach ($errors as $error) {
|
||||
@@ -149,13 +149,13 @@ class CreateUserCommand extends Command
|
||||
/* @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
$passwordQuestion = new Question('Please enter the password');
|
||||
$passwordQuestion = new Question('Please enter the password: ');
|
||||
$passwordQuestion->setHidden(true);
|
||||
$passwordQuestion->setHiddenFallback(false);
|
||||
$passwordQuestion->setValidator(function (?string $value) {
|
||||
$password = trim($value);
|
||||
if (empty($password) || strlen($password) < 6) {
|
||||
throw new \Exception('The password is too short, must be at least 6 character');
|
||||
if (empty($password)) {
|
||||
throw new \Exception('The password may not be empty');
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
@@ -81,15 +81,14 @@ class KimaiImporterCommand extends Command
|
||||
* @var Activity[]
|
||||
*/
|
||||
protected $activities = [];
|
||||
/**
|
||||
* activityId => activity[]
|
||||
* @var array
|
||||
*/
|
||||
protected $unassignedActivities = [];
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $debug = false;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $oldActivities = [];
|
||||
|
||||
/**
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
@@ -168,6 +167,8 @@ class KimaiImporterCommand extends Command
|
||||
$activities = null;
|
||||
$records = null;
|
||||
$activityToProject = null;
|
||||
$fixedRates = null;
|
||||
$rates = null;
|
||||
|
||||
$bytesStart = memory_get_usage(true);
|
||||
|
||||
@@ -219,6 +220,22 @@ class KimaiImporterCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$fixedRates = $this->fetchAllFromImport('fixedRates');
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to load fixedRates: ' . $ex->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$rates = $this->fetchAllFromImport('rates');
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to load rates: ' . $ex->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$bytesCached = memory_get_usage(true);
|
||||
|
||||
$io->success('Fetched Kimai v1 data, trying to import now ...');
|
||||
@@ -230,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;
|
||||
}
|
||||
@@ -240,45 +257,41 @@ 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;
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importProjects($io, $projects);
|
||||
$counter = $this->importProjects($io, $projects, $fixedRates, $rates);
|
||||
$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;
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importActivities($io, $activities, $activityToProject);
|
||||
$counter = $this->importActivities($io, $activities, $activityToProject, $fixedRates, $rates);
|
||||
$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;
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importTimesheetRecords($io, $records);
|
||||
$counter = $this->importTimesheetRecords($io, $records, $fixedRates, $rates);
|
||||
$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;
|
||||
}
|
||||
|
||||
// TODO support fixedRates (projectID, activityID, rate)
|
||||
// TODO support rates (userID, projectID, activityID, rate)
|
||||
// TODO dump yaml config from configuration (adminmail, currency_name, date_format_0, language, roundPrecision)
|
||||
// TODO support preferences (ui.lang, timezone)
|
||||
// TODO support expenses - new database required
|
||||
|
||||
$bytesImported = memory_get_usage(true);
|
||||
@@ -294,22 +307,35 @@ class KimaiImporterCommand extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the ghiven database connection for import has an underlying database with a compatible structure.
|
||||
* This is checked againstto the Kimai version and database revision.
|
||||
* Checks if the given database connection for import has an underlying database with a compatible structure.
|
||||
* This is checked against the Kimai version and database revision.
|
||||
*
|
||||
* @param SymfonyStyle $io
|
||||
* @param $requiredVersion
|
||||
* @param $requiredRevision
|
||||
* @param string $requiredVersion
|
||||
* @param string $requiredRevision
|
||||
* @return bool
|
||||
* @throws \Doctrine\DBAL\DBALException
|
||||
*/
|
||||
protected function checkDatabaseVersion(SymfonyStyle $io, $requiredVersion, $requiredRevision)
|
||||
{
|
||||
$versionQuery = 'SELECT `value` from ' . $this->dbPrefix . 'configuration WHERE `option` = "version"';
|
||||
$revisionQuery = 'SELECT `value` from ' . $this->dbPrefix . 'configuration WHERE `option` = "revision"';
|
||||
$optionColumn = $this->connection->quoteIdentifier('option');
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
|
||||
$version = $this->getImportConnection()->query($versionQuery)->fetchColumn();
|
||||
$revision = $this->getImportConnection()->query($revisionQuery)->fetchColumn();
|
||||
$version = $this->connection->createQueryBuilder()
|
||||
->select('value')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration'))
|
||||
->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($qb->expr()->eq($optionColumn, ':option'))
|
||||
->setParameter('option', 'revision')
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
if (1 == version_compare($requiredVersion, $version)) {
|
||||
$io->error(
|
||||
@@ -359,26 +385,23 @@ 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $table
|
||||
* @param string $table
|
||||
* @return array
|
||||
* @throws \Doctrine\DBAL\DBALException
|
||||
*/
|
||||
protected function fetchAllFromImport($table)
|
||||
{
|
||||
return $this->getImportConnection()->query('SELECT * from ' . $this->dbPrefix . $table)->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Connection
|
||||
*/
|
||||
protected function getImportConnection()
|
||||
{
|
||||
return $this->connection;
|
||||
return $this->connection->createQueryBuilder()
|
||||
->select('*')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . $table))
|
||||
->execute()
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -508,8 +527,8 @@ class KimaiImporterCommand extends Command
|
||||
* ["phone"]=> NULL
|
||||
* ["fax"]=> NULL
|
||||
* ["mobile"]=> NULL
|
||||
* --- ["mail"]=> NULL
|
||||
* --- ["homepage"]=> NULL
|
||||
* ["mail"]=> NULL
|
||||
* ["homepage"]=> NULL
|
||||
* ["trash"]=> string(1) "0"
|
||||
* ["timezone"]=> string(13) "Europe/Berlin"
|
||||
*
|
||||
@@ -538,7 +557,9 @@ class KimaiImporterCommand extends Command
|
||||
->setComment($oldCustomer['comment'])
|
||||
->setCompany($oldCustomer['company'])
|
||||
->setFax($oldCustomer['fax'])
|
||||
->setHomepage($oldCustomer['homepage'])
|
||||
->setMobile($oldCustomer['mobile'])
|
||||
->setMail($oldCustomer['mail'])
|
||||
->setPhone($oldCustomer['phone'])
|
||||
->setContact($oldCustomer['contact'])
|
||||
->setAddress($oldCustomer['street'] . PHP_EOL . $oldCustomer['zipcode'] . ' ' . $oldCustomer['city'])
|
||||
@@ -586,10 +607,12 @@ class KimaiImporterCommand extends Command
|
||||
*
|
||||
* @param SymfonyStyle $io
|
||||
* @param array $projects
|
||||
* @param array $fixedRates
|
||||
* @param array $rates
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function importProjects(SymfonyStyle $io, $projects)
|
||||
protected function importProjects(SymfonyStyle $io, $projects, array $fixedRates, array $rates)
|
||||
{
|
||||
$counter = 0;
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
@@ -611,6 +634,24 @@ class KimaiImporterCommand extends Command
|
||||
->setVisible($isActive)
|
||||
;
|
||||
|
||||
foreach ($fixedRates as $fixedRow) {
|
||||
if ($fixedRow['activityID'] !== null || $fixedRow['projectID'] === null) {
|
||||
continue;
|
||||
}
|
||||
if ($fixedRow['projectID'] == $oldProject['projectID']) {
|
||||
$project->setFixedRate($fixedRow['rate']);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rates as $ratesRow) {
|
||||
if ($ratesRow['userID'] !== null || $ratesRow['activityID'] !== null || $ratesRow['projectID'] === null) {
|
||||
continue;
|
||||
}
|
||||
if ($ratesRow['projectID'] == $oldProject['projectID']) {
|
||||
$project->setHourlyRate($ratesRow['rate']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->validateImport($io, $project)) {
|
||||
throw new \Exception('Failed to validate project: ' . $project->getName());
|
||||
}
|
||||
@@ -654,23 +695,41 @@ class KimaiImporterCommand extends Command
|
||||
* @param SymfonyStyle $io
|
||||
* @param array $activities
|
||||
* @param array $activityToProject
|
||||
* @param array $fixedRates
|
||||
* @param array $rates
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function importActivities(SymfonyStyle $io, array $activities, array $activityToProject)
|
||||
protected function importActivities(SymfonyStyle $io, array $activities, array $activityToProject, array $fixedRates, array $rates)
|
||||
{
|
||||
$counter = 0;
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
|
||||
// remember which activity has at least one assigned project
|
||||
$oldActivityMapping = [];
|
||||
foreach ($activityToProject as $mapping) {
|
||||
$oldActivityMapping[$mapping['activityID']] = $mapping['projectID'];
|
||||
$oldActivityMapping[$mapping['activityID']][] = $mapping['projectID'];
|
||||
}
|
||||
|
||||
// create global activities
|
||||
foreach ($activities as $oldActivity) {
|
||||
$this->oldActivities[$oldActivity['activityID']] = $oldActivity;
|
||||
if (isset($oldActivityMapping[$oldActivity['activityID']])) {
|
||||
$projectId = $oldActivityMapping[$oldActivity['activityID']];
|
||||
$project = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createActivity($io, $entityManager, $oldActivity, $fixedRates, $rates, null);
|
||||
++$counter;
|
||||
}
|
||||
|
||||
$io->success('Created global activities: ' . $counter);
|
||||
|
||||
// create project specific activities
|
||||
foreach ($activities as $oldActivity) {
|
||||
if (!isset($oldActivityMapping[$oldActivity['activityID']])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($oldActivityMapping[$oldActivity['activityID']] as $projectId) {
|
||||
if (!isset($this->projects[$projectId])) {
|
||||
throw new \Exception(
|
||||
'Invalid project linked to activity ' . $oldActivity['name'] . ': ' . $projectId
|
||||
@@ -679,11 +738,8 @@ class KimaiImporterCommand extends Command
|
||||
|
||||
$project = $this->projects[$projectId];
|
||||
|
||||
$this->unassignedActivities[$oldActivity['activityID']] = $oldActivity;
|
||||
$this->createActivity($io, $entityManager, $project, $oldActivity);
|
||||
$this->createActivity($io, $entityManager, $oldActivity, $fixedRates, $rates, $project);
|
||||
++$counter;
|
||||
} else {
|
||||
$this->unassignedActivities[$oldActivity['activityID']] = $oldActivity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,20 +749,26 @@ class KimaiImporterCommand extends Command
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param ObjectManager $entityManager
|
||||
* @param Project $project
|
||||
* @param array $oldActivity
|
||||
* @param array $fixedRates
|
||||
* @param array $rates
|
||||
* @param Project $project
|
||||
* @return Activity
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function createActivity(
|
||||
SymfonyStyle $io,
|
||||
ObjectManager $entityManager,
|
||||
Project $project,
|
||||
array $oldActivity
|
||||
array $oldActivity,
|
||||
array $fixedRates,
|
||||
array $rates,
|
||||
?Project $project
|
||||
) {
|
||||
$activityId = $oldActivity['activityID'];
|
||||
if (isset($this->activities[$activityId][$project->getId()])) {
|
||||
return $this->activities[$activityId][$project->getId()];
|
||||
$projectId = null !== $project ? $project->getId() : null;
|
||||
|
||||
if (isset($this->activities[$activityId][$projectId])) {
|
||||
return $this->activities[$activityId][$projectId];
|
||||
}
|
||||
|
||||
$isActive = (bool) $oldActivity['visible'] && !(bool) $oldActivity['trash'];
|
||||
@@ -724,6 +786,32 @@ class KimaiImporterCommand extends Command
|
||||
->setProject($project)
|
||||
;
|
||||
|
||||
foreach ($fixedRates as $fixedRow) {
|
||||
if ($fixedRow['activityID'] === null) {
|
||||
continue;
|
||||
}
|
||||
if ($fixedRow['projectID'] !== null && $fixedRow['projectID'] !== $projectId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($fixedRow['activityID'] == $oldActivity['activityID']) {
|
||||
$activity->setFixedRate($fixedRow['rate']);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rates as $ratesRow) {
|
||||
if ($ratesRow['userID'] !== null || $ratesRow['activityID'] === null) {
|
||||
continue;
|
||||
}
|
||||
if ($ratesRow['projectID'] !== null && $ratesRow['projectID'] !== $projectId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ratesRow['activityID'] == $oldActivity['activityID']) {
|
||||
$activity->setHourlyRate($ratesRow['rate']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->validateImport($io, $activity)) {
|
||||
throw new \Exception('Failed to validate activity: ' . $activity->getName());
|
||||
}
|
||||
@@ -742,7 +830,7 @@ class KimaiImporterCommand extends Command
|
||||
if (!isset($this->activities[$activityId])) {
|
||||
$this->activities[$activityId] = [];
|
||||
}
|
||||
$this->activities[$activityId][$project->getId()] = $activity;
|
||||
$this->activities[$activityId][$projectId] = $activity;
|
||||
|
||||
return $activity;
|
||||
}
|
||||
@@ -772,14 +860,19 @@ class KimaiImporterCommand extends Command
|
||||
*
|
||||
* @param SymfonyStyle $io
|
||||
* @param array $records
|
||||
* @param array $fixedRates
|
||||
* @param array $rates
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function importTimesheetRecords(SymfonyStyle $io, array $records)
|
||||
protected function importTimesheetRecords(SymfonyStyle $io, array $records, array $fixedRates, array $rates)
|
||||
{
|
||||
$counter = 0;
|
||||
$activityCounter = 0;
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$total = count($records);
|
||||
|
||||
$io->writeln('Importing timesheets, please wait');
|
||||
|
||||
foreach ($records as $oldRecord) {
|
||||
$activity = null;
|
||||
@@ -794,31 +887,47 @@ class KimaiImporterCommand extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
$customerId = $project->getCustomer()->getId();
|
||||
|
||||
if (isset($this->activities[$activityId][$projectId])) {
|
||||
$activity = $this->activities[$activityId][$projectId];
|
||||
} elseif (isset($this->activities[$activityId][null])) {
|
||||
$activity = $this->activities[$activityId][null];
|
||||
}
|
||||
|
||||
if (null === $activity && isset($this->unassignedActivities[$activityId])) {
|
||||
$oldActivity = $this->unassignedActivities[$activityId];
|
||||
$activity = $this->createActivity($io, $entityManager, $project, $oldActivity);
|
||||
if (null === $activity && isset($this->oldActivities[$activityId])) {
|
||||
$oldActivity = $this->oldActivities[$activityId];
|
||||
$activity = $this->createActivity($io, $entityManager, $oldActivity, $fixedRates, $rates, $project);
|
||||
++$activityCounter;
|
||||
}
|
||||
|
||||
if (null === $activity) {
|
||||
$io->error('Could not create timesheet record, missing activity with ID: ' . $activityId);
|
||||
$io->error('Could not create timesheet record, missing activity with ID: ' . $activityId . '/' . $projectId . '/' . $customerId);
|
||||
continue;
|
||||
}
|
||||
|
||||
$duration = $oldRecord['end'] - $oldRecord['start'];
|
||||
|
||||
$rate = $oldRecord['fixedRate'];
|
||||
if ((empty($rate) || 0.00 == $rate) && !empty($oldRecord['rate'])) {
|
||||
$hourlyRate = (float) $oldRecord['rate'];
|
||||
$rate = (float) $hourlyRate * ($duration / 3600);
|
||||
$rate = round($rate, 2);
|
||||
$timesheet = new Timesheet();
|
||||
|
||||
$fixedRate = $oldRecord['fixedRate'];
|
||||
if (!empty($fixedRate) && 0.00 != $fixedRate) {
|
||||
$timesheet->setFixedRate($fixedRate);
|
||||
}
|
||||
|
||||
$hourlyRate = $oldRecord['rate'];
|
||||
if (!empty($hourlyRate) && 0.00 != $hourlyRate) {
|
||||
$timesheet->setHourlyRate($hourlyRate);
|
||||
}
|
||||
|
||||
if ($timesheet->getFixedRate() !== null) {
|
||||
$timesheet->setRate($timesheet->getFixedRate());
|
||||
} elseif ($timesheet->getHourlyRate() !== null) {
|
||||
$hourlyRate = $timesheet->getHourlyRate();
|
||||
$rate = (float) $hourlyRate * ($duration / 3600);
|
||||
$timesheet->setRate(round($rate, 2));
|
||||
}
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
->setDescription($oldRecord['description'] ?: ($oldRecord['comment'] ?: null))
|
||||
->setUser($this->users[$oldRecord['userID']])
|
||||
@@ -826,11 +935,11 @@ class KimaiImporterCommand extends Command
|
||||
->setEnd(new \DateTime('@' . $oldRecord['end']))
|
||||
->setDuration($duration)
|
||||
->setActivity($activity)
|
||||
->setRate($rate)
|
||||
->setProject($project)
|
||||
;
|
||||
|
||||
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 {
|
||||
@@ -841,17 +950,23 @@ 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());
|
||||
}
|
||||
|
||||
if (0 == $counter % 500) {
|
||||
$io->writeln('Imported ' . $counter . ' timesheet records, import ongoing ...');
|
||||
$io->write('.');
|
||||
if (0 == $counter % 80) {
|
||||
$io->writeln(' (' . $counter . '/' . $total . ')');
|
||||
$entityManager->clear(Timesheet::class);
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 80 - ($counter % 80); $i++) {
|
||||
$io->write(' ');
|
||||
}
|
||||
$io->writeln(' (' . $counter . '/' . $total . ')');
|
||||
|
||||
if ($activityCounter > 0) {
|
||||
$io->success('Created new (previously unattached) activities during timesheet import: ' . $activityCounter);
|
||||
$io->success('Created new activities during timesheet import: ' . $activityCounter);
|
||||
}
|
||||
|
||||
return $counter;
|
||||
|
||||
73
src/Command/VersionCommand.php
Normal file
73
src/Command/VersionCommand.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Constants;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Command used to fetch Kimai version information.
|
||||
*/
|
||||
class VersionCommand extends Command
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:version')
|
||||
->setDescription('Receive version information')
|
||||
->setHelp('This command allows you to fetch various version information about Kimai.')
|
||||
->addOption('name', null, InputOption::VALUE_NONE, 'Display the major release name')
|
||||
->addOption('candidate', null, InputOption::VALUE_NONE, 'Display the current version candidate (e.g. "stable" or "dev")')
|
||||
->addOption('short', null, InputOption::VALUE_NONE, 'Display the version only')
|
||||
->addOption('semver', null, InputOption::VALUE_NONE, 'Semantical versioning (SEMVER) compatible version string')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if ($input->getOption('semver')) {
|
||||
$io->writeln(Constants::VERSION . '-' . Constants::STATUS);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->getOption('short')) {
|
||||
$io->writeln(Constants::VERSION);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->getOption('name')) {
|
||||
$io->writeln(Constants::NAME);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->getOption('candidate')) {
|
||||
$io->writeln(Constants::STATUS);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$io->writeln('Kimai 2 - ' . Constants::VERSION . ' ' . Constants::STATUS . ' (' . Constants::NAME . ') by Kevin Papst and contributors.');
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,23 @@ namespace App;
|
||||
class Constants
|
||||
{
|
||||
/**
|
||||
* Currently only used for informational purpose in the footer
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0 dev';
|
||||
public const VERSION = '0.7';
|
||||
/**
|
||||
* The release name, will only change for new major version
|
||||
*/
|
||||
public const NAME = 'Ayumi';
|
||||
/**
|
||||
* The current release status, either "stable" or "dev"
|
||||
*/
|
||||
public const STATUS = 'stable';
|
||||
/**
|
||||
* Used in multiple views
|
||||
*/
|
||||
public const GITHUB = 'https://github.com/kevinpapst/kimai2/';
|
||||
/**
|
||||
* Used in multiple views
|
||||
*/
|
||||
public const HOMEPAGE = 'https://v2.kimai.org';
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Translation\DataCollectorTranslator;
|
||||
|
||||
/**
|
||||
* The abstract base controller.
|
||||
@@ -27,29 +27,13 @@ abstract class AbstractController extends Controller
|
||||
public const ROLE_ADMIN = 'ROLE_ADMIN';
|
||||
|
||||
/**
|
||||
* @return object|\Symfony\Component\Translation\DataCollectorTranslator|\Symfony\Component\Translation\IdentityTranslator
|
||||
* @return DataCollectorTranslator
|
||||
*/
|
||||
protected function getTranslator()
|
||||
private function getTranslator()
|
||||
{
|
||||
return $this->container->get('translator');
|
||||
}
|
||||
|
||||
/**
|
||||
* A translated helper for denyAccessUnlessGranted()
|
||||
*
|
||||
* @param mixed $attributes
|
||||
* @param mixed $subject
|
||||
* @param string $translation
|
||||
* @param array $parameter
|
||||
* @throws AccessDeniedException
|
||||
*/
|
||||
protected function denyUnlessGranted($attributes, $subject = null, $translation = 'access.denied', $parameter = [])
|
||||
{
|
||||
$error = $this->getTranslator()->trans($translation, $parameter, self::DOMAIN_ERROR);
|
||||
// TODO try & catch and add to audit log?
|
||||
$this->denyAccessUnlessGranted($attributes, $subject, $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a "successful" flash message to the stack.
|
||||
*
|
||||
@@ -58,52 +42,51 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a "warning" flash message to the stack.
|
||||
*
|
||||
* @param $translationKey
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a "error" flash message to the stack.
|
||||
*
|
||||
* @param $translationKey
|
||||
* @param string $translationKey
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@ namespace App\Controller;
|
||||
use App\Entity\Activity;
|
||||
use App\Repository\ActivityRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Controller used to manage activity contents in the public part of the site.
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class ActivityController extends Controller
|
||||
class ActivityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @return ActivityRepository
|
||||
@@ -32,17 +32,16 @@ class ActivityController extends Controller
|
||||
/**
|
||||
* The flyout to render recent activities and quick-start new recordings.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @return Response
|
||||
*/
|
||||
public function recentActivitiesAction()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
// TODO make days configurable
|
||||
$activeEntries = $this->getRepository()->getRecentActivities($user, new \DateTime('-30 days'));
|
||||
$entries = $this->getRepository()->getRecentActivities($user, new \DateTime('-30 days'));
|
||||
|
||||
return $this->render(
|
||||
'navbar/recent-activities.html.twig',
|
||||
['activities' => $activeEntries]
|
||||
['entries' => $entries]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
149
src/Controller/Admin/AboutController.php
Normal file
149
src/Controller/Admin/AboutController.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Constants;
|
||||
use App\Controller\AbstractController;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used for executing system relevant tasks.
|
||||
*
|
||||
* @Route(path="/admin/about")
|
||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||
*/
|
||||
class AboutController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $projectDirectory;
|
||||
|
||||
/**
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct(string $projectDirectory)
|
||||
{
|
||||
$this->projectDirectory = $projectDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="", name="about", methods={"GET"})
|
||||
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
{
|
||||
$phpInfo = $this->getPhpInfo();
|
||||
unset($phpInfo[0]);
|
||||
unset($phpInfo[1]);
|
||||
|
||||
$ini = [
|
||||
'allow_url_fopen',
|
||||
'allow_url_include',
|
||||
'default_charset',
|
||||
'default_mimetype',
|
||||
'display_errors',
|
||||
'error_log',
|
||||
'error_reporting',
|
||||
'log_errors',
|
||||
'max_execution_time',
|
||||
'memory_limit',
|
||||
'open_basedir',
|
||||
'post_max_size',
|
||||
'sys_temp_dir',
|
||||
'date.timezone',
|
||||
];
|
||||
|
||||
$settings = [];
|
||||
foreach ($ini as $name) {
|
||||
try {
|
||||
$settings[$name] = ini_get($name);
|
||||
} catch (\Exception $ex) {
|
||||
$settings[$name] = "Couldn't load ini setting: " . $ex->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('admin/system.html.twig', [
|
||||
'modules' => get_loaded_extensions(),
|
||||
'dotenv' => [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
'DATABASE_PREFIX' => getenv('DATABASE_PREFIX'),
|
||||
'MAILER_FROM' => getenv('MAILER_FROM'),
|
||||
'MAILER_URL' => getenv('MAILER_URL'),
|
||||
'CORS_ALLOW_ORIGIN' => getenv('CORS_ALLOW_ORIGIN'),
|
||||
],
|
||||
'info' => $phpInfo,
|
||||
'settings' => $settings,
|
||||
'license' => $this->getLicense(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getLicense()
|
||||
{
|
||||
$filename = $this->projectDirectory . '/LICENSE';
|
||||
|
||||
try {
|
||||
$license = file_get_contents($filename);
|
||||
} catch (\Exception $ex) {
|
||||
$license = false;
|
||||
}
|
||||
|
||||
if (false === $license) {
|
||||
$license = 'Failed reading license file: ' . $filename . '. ' .
|
||||
'Check this instead: ' . Constants::GITHUB . 'blob/master/LICENSE';
|
||||
}
|
||||
|
||||
return $license;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author https://php.net/manual/en/function.phpinfo.php#117961
|
||||
* @return array
|
||||
*/
|
||||
private function getPhpInfo()
|
||||
{
|
||||
$plainText = function ($input) {
|
||||
return trim(html_entity_decode(strip_tags($input)));
|
||||
};
|
||||
|
||||
ob_start();
|
||||
phpinfo(1);
|
||||
|
||||
$phpinfo = ['phpinfo' => []];
|
||||
|
||||
if (preg_match_all(
|
||||
'#(?:<h2.*?>(?:<a.*?>)?(.*?)(?:<\/a>)?<\/h2>)|' .
|
||||
'(?:<tr.*?><t[hd].*?>(.*?)\s*</t[hd]>(?:<t[hd].*?>(.*?)\s*</t[hd]>(?:<t[hd].*?>(.*?)\s*</t[hd]>)?)?</tr>)#s',
|
||||
ob_get_clean(),
|
||||
$matches,
|
||||
PREG_SET_ORDER
|
||||
)) {
|
||||
foreach ($matches as $match) {
|
||||
$fn = $plainText;
|
||||
if (isset($match[3])) {
|
||||
$keys1 = array_keys($phpinfo);
|
||||
$phpinfo[end($keys1)][$fn($match[2])] = isset($match[4]) ? [$fn($match[3]), $fn($match[4])] : $fn($match[3]);
|
||||
} else {
|
||||
$keys1 = array_keys($phpinfo);
|
||||
$phpinfo[end($keys1)][] = $fn($match[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $phpinfo['phpinfo'];
|
||||
}
|
||||
}
|
||||
@@ -13,20 +13,21 @@ use App\Controller\AbstractController;
|
||||
use App\Entity\Activity;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Route("/admin/activity")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Route(path="/admin/activity")
|
||||
* @Security("is_granted('view_activity')")
|
||||
*/
|
||||
class ActivityController extends AbstractController
|
||||
{
|
||||
@@ -39,10 +40,14 @@ class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="admin_activity")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated")
|
||||
* @Method("GET")
|
||||
* @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)
|
||||
{
|
||||
@@ -63,13 +68,14 @@ class ActivityController extends AbstractController
|
||||
return $this->render('admin/activity.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/create", name="admin_activity_create")
|
||||
* @Method({"GET", "POST"})
|
||||
* @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
|
||||
@@ -80,8 +86,7 @@ class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit", name="admin_activity_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{id}/edit", name="admin_activity_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
@@ -94,10 +99,7 @@ class ActivityController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route("/{id}/delete", name="admin_activity_delete")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{id}/delete", name="admin_activity_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
@@ -109,6 +111,22 @@ class ActivityController extends AbstractController
|
||||
$stats = $this->getRepository()->getActivityStatistics($activity);
|
||||
|
||||
$deleteForm = $this->createFormBuilder()
|
||||
->add('activity', ActivityType::class, [
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity) {
|
||||
$query = new ActivityQuery();
|
||||
$query
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->setProject($activity->getProject())
|
||||
->setOrderGlobalsFirst(true)
|
||||
->addIgnoredEntity($activity)
|
||||
->setGlobalsOnly(null === $activity->getProject())
|
||||
;
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
->setAction($this->generateUrl('admin_activity_delete', ['id' => $activity->getId()]))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
@@ -116,13 +134,14 @@ class ActivityController extends AbstractController
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordAmount() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($activity);
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->getRepository()->deleteActivity($activity, $deleteForm->get('activity')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
}
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_activity', ['id' => $activity->getId()]);
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
@@ -151,7 +170,7 @@ class ActivityController extends AbstractController
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newActivity = new Activity();
|
||||
@@ -160,7 +179,7 @@ class ActivityController extends AbstractController
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$activity = $newActivity;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_activity', ['id' => $activity->getId()]);
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,22 +13,36 @@ use App\Controller\AbstractController;
|
||||
use App\Entity\Customer;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage activities in the admin part of the site.
|
||||
*
|
||||
* @Route("/admin/customer")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Route(path="/admin/customer")
|
||||
* @Security("is_granted('view_customer')")
|
||||
*/
|
||||
class CustomerController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $defaults;
|
||||
|
||||
/**
|
||||
* @param array $defaults
|
||||
*/
|
||||
public function __construct(array $defaults)
|
||||
{
|
||||
$this->defaults = $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \App\Repository\CustomerRepository
|
||||
*/
|
||||
@@ -38,9 +52,13 @@ class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="admin_customer")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated")
|
||||
* @Method("GET")
|
||||
* @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)
|
||||
{
|
||||
@@ -60,29 +78,90 @@ class CustomerController extends AbstractController
|
||||
return $this->render('admin/customer.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/create", name="admin_customer_create")
|
||||
* @Method({"GET", "POST"})
|
||||
* @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)
|
||||
{
|
||||
return $this->renderCustomerForm(new Customer(), $request);
|
||||
$customer = new Customer();
|
||||
$customer->setCountry($this->defaults['customer']['country']);
|
||||
$customer->setCurrency($this->defaults['customer']['currency']);
|
||||
$customer->setTimezone($this->defaults['customer']['timezone']);
|
||||
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit", name="admin_customer_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
* @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)
|
||||
{
|
||||
return $this->renderCustomerForm($customer, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function deleteAction(Customer $customer, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getCustomerStatistics($customer);
|
||||
|
||||
$deleteForm = $this->createFormBuilder()
|
||||
->add('customer', CustomerType::class, [
|
||||
'label' => 'label.customer',
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
$query = new CustomerQuery();
|
||||
$query
|
||||
->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->addIgnoredEntity($customer);
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
->setAction($this->generateUrl('admin_customer_delete', ['id' => $customer->getId()]))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordAmount() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
try {
|
||||
$this->getRepository()->deleteCustomer($customer, $deleteForm->get('customer')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_delete.html.twig', [
|
||||
'customer' => $customer,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
@@ -99,9 +178,9 @@ class CustomerController extends AbstractController
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer', ['id' => $customer->getId()]);
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_edit.html.twig', [
|
||||
@@ -110,45 +189,6 @@ class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route("/{id}/delete", name="admin_customer_delete")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Security("is_granted('delete', customer)")
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function deleteAction(Customer $customer, Request $request)
|
||||
{
|
||||
$stats = $this->getRepository()->getCustomerStatistics($customer);
|
||||
|
||||
$deleteForm = $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('admin_customer_delete', ['id' => $customer->getId()]))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordAmount() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_customer', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('admin/customer_delete.html.twig', [
|
||||
'customer' => $customer,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
|
||||
@@ -14,20 +14,21 @@ use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Form\Toolbar\ProjectToolbarForm;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage projects in the admin part of the site.
|
||||
*
|
||||
* @Route("/admin/project")
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Route(path="/admin/project")
|
||||
* @Security("is_granted('view_project')")
|
||||
*/
|
||||
class ProjectController extends AbstractController
|
||||
{
|
||||
@@ -40,10 +41,14 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="admin_project")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated")
|
||||
* @Method("GET")
|
||||
* @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)
|
||||
{
|
||||
@@ -64,13 +69,17 @@ class ProjectController extends AbstractController
|
||||
return $this->render('admin/project.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/create", name="admin_project_create")
|
||||
* @Method({"GET", "POST"})
|
||||
* @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)
|
||||
{
|
||||
@@ -78,9 +87,12 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit", name="admin_project_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
* @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,10 +100,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route("/{id}/delete", name="admin_project_delete")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', project)")
|
||||
*
|
||||
* @param Project $project
|
||||
@@ -103,6 +112,19 @@ class ProjectController extends AbstractController
|
||||
$stats = $this->getRepository()->getProjectStatistics($project);
|
||||
|
||||
$deleteForm = $this->createFormBuilder()
|
||||
->add('project', ProjectType::class, [
|
||||
'label' => 'label.project',
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project) {
|
||||
$query = new ProjectQuery();
|
||||
$query
|
||||
->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->setCustomer($project->getCustomer())
|
||||
->addIgnoredEntity($project);
|
||||
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
'required' => false,
|
||||
])
|
||||
->setAction($this->generateUrl('admin_project_delete', ['id' => $project->getId()]))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
@@ -110,13 +132,14 @@ class ProjectController extends AbstractController
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordAmount() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($project);
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->getRepository()->deleteProject($project, $deleteForm->get('project')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
}
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_project', ['id' => $project->getId()]);
|
||||
return $this->redirectToRoute('admin_project');
|
||||
}
|
||||
|
||||
return $this->render('admin/project_delete.html.twig', [
|
||||
@@ -142,7 +165,7 @@ class ProjectController extends AbstractController
|
||||
$entityManager->persist($project);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newProject = new Project();
|
||||
@@ -151,7 +174,7 @@ class ProjectController extends AbstractController
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$project = $newProject;
|
||||
} else {
|
||||
return $this->redirectToRoute('admin_project', ['id' => $project->getId()]);
|
||||
return $this->redirectToRoute('admin_project');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,37 +16,24 @@ use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetAdminToolbarForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used for manage timesheet entries in the admin part of the site.
|
||||
*
|
||||
* @Route("/team/timesheet")
|
||||
* @Security("is_granted('ROLE_TEAMLEAD')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Route(path="/team/timesheet")
|
||||
* @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("/", defaults={"page": 1}, name="admin_timesheet")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated")
|
||||
* @Method("GET")
|
||||
* @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
|
||||
@@ -62,6 +49,12 @@ class TimesheetController extends AbstractController
|
||||
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 */
|
||||
@@ -69,17 +62,47 @@ 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(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to stop a running entry.
|
||||
* @Route(path="/export", name="admin_timesheet_export", methods={"GET"})
|
||||
*
|
||||
* @Route("/{id}/stop", name="admin_timesheet_stop")
|
||||
* @Method({"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)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
@@ -91,10 +114,7 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to edit an existing entry.
|
||||
*
|
||||
* @Route("/{id}/edit", name="admin_timesheet_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{id}/edit", name="admin_timesheet_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
@@ -107,10 +127,8 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to create a new entry by form.
|
||||
*
|
||||
* @Route("/create", name="admin_timesheet_create")
|
||||
* @Method({"GET", "POST"})
|
||||
* @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
|
||||
@@ -121,10 +139,7 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route("/{id}/delete", name="admin_timesheet_delete")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="admin_timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
@@ -138,9 +153,9 @@ class TimesheetController extends AbstractController
|
||||
$entityManager->remove($entry);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.deleted.error', ['%reason%' => $ex->getMessage()]);
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_timesheet_paginated', ['page' => $request->get('page')]);
|
||||
@@ -148,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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,29 +10,54 @@
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Form\Toolbar\UserToolbarForm;
|
||||
use App\Form\UserCreateType;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
/**
|
||||
* Controller used to manage users in the admin part of the site.
|
||||
*
|
||||
* @Route("/admin/user")
|
||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @Route(path="/admin/user")
|
||||
* @Security("is_granted('view_user')")
|
||||
*/
|
||||
class UserController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="admin_user")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated")
|
||||
* @Method("GET")
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
protected $encoder;
|
||||
|
||||
/**
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \App\Repository\UserRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->getDoctrine()->getRepository(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -47,30 +72,33 @@ class UserController extends AbstractController
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getDoctrine()->getRepository(User::class)->findByQuery($query);
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/user.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/create", name="admin_user_create")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Security("is_granted('create', user)")
|
||||
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
|
||||
* @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);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$password = $this->get('security.password_encoder')
|
||||
->encodePassword($user, $user->getPlainPassword());
|
||||
$password = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($password);
|
||||
$user->setEnabled(true);
|
||||
$user->setRoles([User::DEFAULT_ROLE]);
|
||||
@@ -79,7 +107,7 @@ class UserController extends AbstractController
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->get('create_more')->getData() !== true) {
|
||||
return $this->redirectToRoute('user_profile_edit', ['username' => $user->getUsername()]);
|
||||
@@ -99,6 +127,47 @@ class UserController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", name="admin_user_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', userToDelete)")
|
||||
*
|
||||
* @param User $userToDelete
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
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()
|
||||
->setAction($this->generateUrl('admin_user_delete', ['id' => $userToDelete->getId()]))
|
||||
->setMethod('POST')
|
||||
->getForm();
|
||||
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordsTotal() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($userToDelete);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.delete.success');
|
||||
|
||||
return $this->redirectToRoute('admin_user');
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'admin/user_delete.html.twig',
|
||||
[
|
||||
'user' => $userToDelete,
|
||||
'stats' => $stats,
|
||||
'form' => $deleteForm->createView(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
|
||||
97
src/Controller/CalendarController.php
Normal file
97
src/Controller/CalendarController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Calendar\Service;
|
||||
use App\Calendar\TimesheetEntity;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to display calendars.
|
||||
*
|
||||
* @Route(path="/calendar")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class CalendarController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var Service
|
||||
*/
|
||||
protected $calendar;
|
||||
|
||||
/**
|
||||
* @param Service $calendar
|
||||
*/
|
||||
public function __construct(Service $calendar)
|
||||
{
|
||||
$this->calendar = $calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="calendar", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
*/
|
||||
public function userCalendar()
|
||||
{
|
||||
return $this->render('calendar/user.html.twig', [
|
||||
'config' => $this->calendar->getConfig(),
|
||||
'google' => $this->calendar->getGoogle()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/user", name="calendar_entries", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
*/
|
||||
public function calendarEntries(Request $request)
|
||||
{
|
||||
$start = $request->get('start');
|
||||
$end = $request->get('end');
|
||||
|
||||
$start = \DateTime::createFromFormat('Y-m-d', $start);
|
||||
if ($start === false) {
|
||||
$start = new \DateTime('first day of this month');
|
||||
}
|
||||
$start->setTime(0, 0, 0);
|
||||
|
||||
$end = \DateTime::createFromFormat('Y-m-d', $end);
|
||||
if ($end === false) {
|
||||
$end = clone $start;
|
||||
$end = $end->modify('last day of this month');
|
||||
}
|
||||
$end->setTime(23, 59, 59);
|
||||
|
||||
$query = new TimesheetQuery();
|
||||
$query
|
||||
->setBegin($start)
|
||||
->setUser($this->getUser())
|
||||
->setState(TimesheetQuery::STATE_ALL)
|
||||
->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER)
|
||||
->setEnd($end)
|
||||
;
|
||||
|
||||
$repository = $this->getDoctrine()->getRepository(Timesheet::class);
|
||||
|
||||
/* @var $entries Timesheet[] */
|
||||
$entries = $repository->findByQuery($query)->getQuery()->execute();
|
||||
$result = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$result[] = new TimesheetEntity($entry);
|
||||
}
|
||||
|
||||
return $this->json($result);
|
||||
}
|
||||
}
|
||||
@@ -9,139 +9,99 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Model\DashboardSection;
|
||||
use App\Repository\WidgetRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Dashboard controller for the admin area.
|
||||
*
|
||||
* @Route("/dashboard")
|
||||
* @Route(path="/dashboard")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class DashboardController extends Controller
|
||||
class DashboardController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/", defaults={}, name="dashboard")
|
||||
* @Method("GET")
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
/**
|
||||
* @var WidgetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dashboard;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param WidgetRepository $repository
|
||||
* @param array $dashboard
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, WidgetRepository $repository, array $dashboard)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->repository = $repository;
|
||||
$this->dashboard = $dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", defaults={}, name="dashboard", methods={"GET"})
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$event = new DashboardEvent($this->getUser());
|
||||
|
||||
$userStats = $this->getDoctrine()->getRepository(User::class)->getGlobalStatistics();
|
||||
foreach ($this->dashboard as $widgetRow) {
|
||||
if (empty($widgetRow['widgets'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIXME move the other widgets to the Kimai, the inheritence is wrong as Kimai
|
||||
// shouldn't know about Timesheets
|
||||
if (!$this->isGranted($widgetRow['permission'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
|
||||
$timesheetUserStats = $timesheetRepo->getUserStatistics($user);
|
||||
$timesheetGlobalStats = $timesheetRepo->getGlobalStatistics();
|
||||
$row = new DashboardSection($widgetRow['title'] ?? null);
|
||||
$row
|
||||
->setOrder($widgetRow['order'])
|
||||
->setType($widgetRow['type'])
|
||||
;
|
||||
|
||||
$activityStats = $this->getDoctrine()->getRepository(Activity::class)->getGlobalStatistics();
|
||||
$projectStats = $this->getDoctrine()->getRepository(Project::class)->getGlobalStatistics();
|
||||
$customerStats = $this->getDoctrine()->getRepository(Customer::class)->getGlobalStatistics();
|
||||
foreach ($widgetRow['widgets'] as $widgetName) {
|
||||
if (!$this->repository->has($widgetName)) {
|
||||
throw new \Exception('Unknwon widget: ' . $widgetName);
|
||||
}
|
||||
|
||||
$row->addWidget($this->repository->get($widgetName, $event->getUser()));
|
||||
}
|
||||
|
||||
$event->addSection($row);
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(
|
||||
DashboardEvent::DASHBOARD,
|
||||
$event
|
||||
);
|
||||
|
||||
$sections = $event->getSections();
|
||||
|
||||
uasort(
|
||||
$sections,
|
||||
function (DashboardSection $a, DashboardSection $b) {
|
||||
if ($a->getOrder() == $b->getOrder()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
|
||||
}
|
||||
);
|
||||
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'dashboard_widgets' => $this->getWidgets(),
|
||||
'timesheetGlobal' => $timesheetGlobalStats,
|
||||
'timesheetUser' => $timesheetUserStats,
|
||||
'activity' => $activityStats,
|
||||
'project' => $projectStats,
|
||||
'customer' => $customerStats,
|
||||
'user' => $userStats,
|
||||
'widget_rows' => $sections
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* colors: blue / yellow / purple / green / black
|
||||
* icons: bar-chart / line-chart / calendar / clock
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getWidgets()
|
||||
{
|
||||
// @codingStandardsIgnoreStart
|
||||
$widgets = [
|
||||
/*
|
||||
[
|
||||
'header' => 'dashboard.you',
|
||||
'widgets' => [
|
||||
"{{ widgets.info_box_progress('Bewilligte Stunden', 'Stunden zur Abrechnung bewilligt', 120, 10, 'star') }}",
|
||||
"{{ widgets.info_box_progress('Umsatz / Monat', '70% Increase in 30 Days', 6830, 30, 'credit-card', 'black') }}",
|
||||
"{{ widgets.info_box_progress('Stunden persönlich', 'Das ist noch nicht genug', 135, 60, 'hourglass') }}",
|
||||
"{{ widgets.info_box_progress('Anzahl Benutzer', 'Mehr ist besser!', 5, 90, 'user') }}",
|
||||
],
|
||||
],
|
||||
*/
|
||||
[
|
||||
'id' => 'profile.stats',
|
||||
'header' => 'dashboard.you',
|
||||
'widgets' => [
|
||||
"{{ widgets.info_box_counter('stats.durationThisMonth', timesheetUser.durationThisMonth|duration(true), 'far fa-hourglass', 'green') }}",
|
||||
//"{{ widgets.info_box_counter('stats.amountThisMonth', timesheetUser.amountThisMonth|money, 'money', 'blue') }}",
|
||||
"{{ widgets.info_box_counter('stats.durationTotal', timesheetUser.durationTotal|duration(true), 'far fa-hourglass', 'red') }}",
|
||||
//"{{ widgets.info_box_counter('stats.amountTotal', timesheetUser.amountTotal|money, 'money', 'yellow') }}",
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (!$this->isGranted('ROLE_TEAMLEAD', null)) {
|
||||
return $widgets;
|
||||
}
|
||||
|
||||
$widgets[] = [
|
||||
'id' => 'alluser.stats',
|
||||
'header' => 'dashboard.all',
|
||||
'widgets' => [
|
||||
"{{ widgets.info_box_counter('stats.durationThisMonth', timesheetGlobal.durationThisMonth|duration(true), 'far fa-hourglass', 'blue') }}",
|
||||
"{{ widgets.info_box_counter('stats.durationTotal', timesheetGlobal.durationTotal|duration(true), 'far fa-hourglass', 'yellow') }}",
|
||||
"{{ widgets.info_box_counter('stats.activeRecordings', timesheetGlobal.activeCurrently, 'far fa-hourglass', 'red', path('admin_timesheet', {'state': " . TimesheetQuery::STATE_RUNNING . '})) }}',
|
||||
],
|
||||
];
|
||||
|
||||
$widgets[] = [
|
||||
'id' => 'user.stats',
|
||||
'header' => '',
|
||||
'widgets' => [
|
||||
"{{ widgets.info_box_counter('stats.userTotal', user.totalAmount, 'user', 'red') }}",
|
||||
"{{ widgets.info_box_counter('stats.userActiveThisMoth', timesheetGlobal.activeThisMonth, 'user', 'yellow') }}",
|
||||
"{{ widgets.info_box_counter('stats.userActiveEver', timesheetGlobal.activeTotal, 'user', 'blue') }}",
|
||||
],
|
||||
];
|
||||
|
||||
if (!$this->isGranted('ROLE_ADMIN', null)) {
|
||||
return $widgets;
|
||||
}
|
||||
|
||||
$widgets[] = [
|
||||
'id' => 'alluser.money_stats',
|
||||
'header' => '',
|
||||
'widgets' => [
|
||||
"{{ widgets.info_box_counter('stats.amountThisMonth', timesheetGlobal.amountThisMonth|money, 'far fa-money-bill-alt', 'green') }}",
|
||||
"{{ widgets.info_box_counter('stats.amountTotal', timesheetGlobal.amountTotal|money, 'far fa-money-bill-alt', 'red') }}",
|
||||
],
|
||||
];
|
||||
|
||||
$widgets[] = [
|
||||
'id' => 'admin.stats',
|
||||
'header' => 'dashboard.admin',
|
||||
'widgets' => [
|
||||
"{{ widgets.info_box_more('stats.userTotal', user.totalAmount, ' ', path('admin_user'), 'user') }}",
|
||||
"{{ widgets.info_box_more('stats.customerTotal', customer.count, '', path('admin_customer'), 'customer', 'blue') }}",
|
||||
"{{ widgets.info_box_more('stats.projectsTotal', project.count, '', path('admin_project'), 'project', 'yellow') }}",
|
||||
"{{ widgets.info_box_more('stats.activitiesTotal', activity.count, '', path('admin_activity'), 'activity', 'purple') }}",
|
||||
],
|
||||
];
|
||||
// @codingStandardsIgnoreEnd
|
||||
|
||||
return $widgets;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,16 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Constants;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* This controller can render the markdown documentation from /var/docs/
|
||||
*
|
||||
* @Route("/help")
|
||||
* @Route(path="/help")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class HelpController extends Controller
|
||||
class HelpController extends AbstractController
|
||||
{
|
||||
public const README = 'README';
|
||||
public const DOCS_DIR = 'var/docs/';
|
||||
@@ -41,9 +39,8 @@ class HelpController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"chapter": "README"}, name="help")
|
||||
* @Route("/{chapter}", requirements={"chapter": "[a-zA-Z]*"}, name="help_chapter")
|
||||
* @Method("GET")
|
||||
* @Route(path="/", defaults={"chapter": "README"}, name="help", methods={"GET"})
|
||||
* @Route(path="/{chapter}", requirements={"chapter": "[a-zA-Z]*"}, name="help_chapter", methods={"GET"})
|
||||
*
|
||||
* @param string $chapter
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -63,6 +60,8 @@ class HelpController extends Controller
|
||||
|
||||
$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,
|
||||
|
||||
43
src/Controller/HomepageController.php
Normal file
43
src/Controller/HomepageController.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Homepage controller is a redirect controller with user specific logic.
|
||||
*
|
||||
* @Route(path="/homepage")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class HomepageController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="", defaults={}, name="homepage", methods={"GET"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
{
|
||||
// make me configurable via UserPreference
|
||||
$route = 'timesheet';
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$locale = $request->getLocale();
|
||||
$language = $user->getPreferenceValue('language', $locale);
|
||||
|
||||
return $this->redirectToRoute($route, ['_locale' => $language]);
|
||||
}
|
||||
}
|
||||
@@ -15,20 +15,20 @@ use App\Form\InvoiceTemplateForm;
|
||||
use App\Form\Toolbar\InvoiceToolbarForm;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Model\InvoiceModel;
|
||||
use App\Repository\InvoiceTemplateRepository;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage invoices.
|
||||
*
|
||||
* @Route("/invoice")
|
||||
* @Security("is_granted('ROLE_TEAMLEAD')")
|
||||
* @Route(path="/invoice")
|
||||
* @Security("is_granted('view_invoice') or is_granted('view_invoice_template')")
|
||||
*/
|
||||
class InvoiceController extends AbstractController
|
||||
{
|
||||
@@ -36,14 +36,25 @@ class InvoiceController extends AbstractController
|
||||
* @var ServiceInvoice
|
||||
*/
|
||||
protected $service;
|
||||
/**
|
||||
* @var InvoiceTemplateRepository
|
||||
*/
|
||||
protected $invoiceRepository;
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $timesheetRepository;
|
||||
|
||||
/**
|
||||
* InvoiceController constructor.
|
||||
* @param ServiceInvoice $service
|
||||
* @param InvoiceTemplateRepository $invoice
|
||||
* @param TimesheetRepository $timesheet
|
||||
*/
|
||||
public function __construct(ServiceInvoice $service)
|
||||
public function __construct(ServiceInvoice $service, InvoiceTemplateRepository $invoice, TimesheetRepository $timesheet)
|
||||
{
|
||||
$this->service = $service;
|
||||
$this->invoiceRepository = $invoice;
|
||||
$this->timesheetRepository = $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,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());
|
||||
@@ -65,15 +77,8 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \App\Repository\InvoiceTemplateRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->getDoctrine()->getRepository(InvoiceTemplate::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", name="invoice")
|
||||
* @Route(path="/", name="invoice", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -81,7 +86,7 @@ class InvoiceController extends AbstractController
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
{
|
||||
if (!$this->getRepository()->hasTemplate()) {
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
|
||||
@@ -89,31 +94,103 @@ class InvoiceController extends AbstractController
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
$query->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$entries = $this->getEntries($query);
|
||||
}
|
||||
|
||||
if (null !== $query->getCustomer()) {
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
|
||||
/* @var TimesheetRepository $timeRepo */
|
||||
$timeRepo = $this->getDoctrine()->getRepository(Timesheet::class);
|
||||
$queryBuilder = $timeRepo->findByQuery($query);
|
||||
$entries = $queryBuilder->getQuery()->getResult();
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/print", name="invoice_print", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function printAction(Request $request)
|
||||
{
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if (!$form->isSubmitted() || !$form->isValid()) {
|
||||
return $this->redirectToRoute('invoice');
|
||||
}
|
||||
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
$entries = $this->getEntries($query);
|
||||
$model = $this->prepareModel($query, $entries);
|
||||
|
||||
$document = $this->service->getDocumentByName($model->getTemplate()->getRenderer());
|
||||
if (null === $document) {
|
||||
throw new \Exception('Unknown invoice document: ' . $model->getTemplate()->getRenderer());
|
||||
}
|
||||
|
||||
foreach ($this->service->getRenderer() as $renderer) {
|
||||
if ($renderer->supports($document)) {
|
||||
return $renderer->render($document, $model);
|
||||
}
|
||||
}
|
||||
|
||||
$model = new InvoiceModel();
|
||||
$model->setQuery($query);
|
||||
$model->setEntries($entries);
|
||||
$model->setCustomer($query->getCustomer());
|
||||
$this->flashError('Cannot render invoice: ' . $model->getTemplate()->getRenderer() . ' (' . $document->getName() . ')');
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getEntries(InvoiceQuery $query)
|
||||
{
|
||||
// customer needs to be defined, as we need the currency for the invoice
|
||||
if (null === $query->getCustomer()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
|
||||
$queryBuilder = $this->timesheetRepository->findByQuery($query);
|
||||
|
||||
return $queryBuilder->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @param array $entries
|
||||
* @return InvoiceModel
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function prepareModel(InvoiceQuery $query, array $entries)
|
||||
{
|
||||
$model = new InvoiceModel();
|
||||
$model
|
||||
->setQuery($query)
|
||||
->setEntries($entries)
|
||||
->setCustomer($query->getCustomer())
|
||||
;
|
||||
|
||||
$action = null;
|
||||
if ($query->getTemplate() !== null) {
|
||||
$generator = $this->service->getNumberGeneratorByName($query->getTemplate()->getNumberGenerator());
|
||||
if (null === $generator) {
|
||||
@@ -128,29 +205,22 @@ class InvoiceController extends AbstractController
|
||||
$model->setTemplate($query->getTemplate());
|
||||
$model->setCalculator($calculator);
|
||||
$model->setNumberGenerator($generator);
|
||||
$action = $this->service->getRendererActionByName($query->getTemplate()->getRenderer());
|
||||
}
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'action' => $action,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/template", defaults={"page": 1}, name="admin_invoice_template")
|
||||
* @Route("/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated")
|
||||
* @Method({"GET", "POST"})
|
||||
*
|
||||
* TODO permission
|
||||
* @Route(path="/template", defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
|
||||
* @Route(path="/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view_invoice_template')")
|
||||
*
|
||||
* @param $page
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function listTemplateAction($page)
|
||||
{
|
||||
$templates = $this->getRepository()->findByQuery(new BaseQuery());
|
||||
$templates = $this->invoiceRepository->findByQuery(new BaseQuery());
|
||||
|
||||
return $this->render('invoice/templates.html.twig', [
|
||||
'entries' => $templates,
|
||||
@@ -159,11 +229,10 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit", name="admin_invoice_template_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
*
|
||||
* TODO permission
|
||||
* @Route(path="/template/{id}/edit", name="admin_invoice_template_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
@@ -174,22 +243,60 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/create", name="admin_invoice_template_create")
|
||||
* @Method({"GET", "POST"})
|
||||
*
|
||||
* TODO permission
|
||||
* @Route(path="/template/create", name="admin_invoice_template_create", methods={"GET", "POST"})
|
||||
* @Route(path="/template/create/{id}", name="admin_invoice_template_copy", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_invoice_template')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @param InvoiceTemplate|null $template
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createTemplateAction(Request $request)
|
||||
public function createTemplateAction(Request $request, ?InvoiceTemplate $copyFrom)
|
||||
{
|
||||
if (!$this->getRepository()->hasTemplate()) {
|
||||
if (!$this->invoiceRepository->hasTemplate()) {
|
||||
$this->flashWarning('invoice.first_template');
|
||||
}
|
||||
|
||||
return $this->renderTemplateForm(new InvoiceTemplate(), $request);
|
||||
$template = new InvoiceTemplate();
|
||||
if (null !== $copyFrom) {
|
||||
$template
|
||||
->setName('Copy of ' . $copyFrom->getName())
|
||||
->setTitle($copyFrom->getTitle())
|
||||
->setDueDays($copyFrom->getDueDays())
|
||||
->setCalculator($copyFrom->getCalculator())
|
||||
->setVat($copyFrom->getVat())
|
||||
->setRenderer($copyFrom->getRenderer())
|
||||
->setCompany($copyFrom->getCompany())
|
||||
->setPaymentTerms($copyFrom->getPaymentTerms())
|
||||
->setAddress($copyFrom->getAddress())
|
||||
->setNumberGenerator($copyFrom->getNumberGenerator())
|
||||
;
|
||||
}
|
||||
|
||||
return $this->renderTemplateForm($template, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to delete an existing template.
|
||||
*
|
||||
* @Route(path="/template/{id}/delete", name="admin_invoice_template_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', template)")
|
||||
*
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function deleteTemplate(InvoiceTemplate $template, Request $request)
|
||||
{
|
||||
try {
|
||||
$this->invoiceRepository->removeTemplate($template);
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template_paginated', ['page' => $request->get('page')]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,13 +311,14 @@ class InvoiceController extends AbstractController
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($template);
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->invoiceRepository->saveTemplate($template);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('invoice/template_edit.html.twig', [
|
||||
@@ -219,28 +327,6 @@ class InvoiceController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function invoiceAction(InvoiceModel $model)
|
||||
{
|
||||
return $this->render('invoice/renderer/print.html.twig', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function timesheetAction(InvoiceModel $model)
|
||||
{
|
||||
return $this->render('invoice/renderer/timesheet.html.twig', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
@@ -249,7 +335,10 @@ class InvoiceController extends AbstractController
|
||||
{
|
||||
return $this->createForm(InvoiceToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('invoice', []),
|
||||
'method' => 'GET',
|
||||
'method' => 'POST',
|
||||
'attr' => [
|
||||
'id' => 'invoice-print-form'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,29 +11,58 @@ 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;
|
||||
use App\Form\UserPreferencesForm;
|
||||
use App\Form\UserRolesType;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Voter\UserVoter;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
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;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
/**
|
||||
* User profile controller
|
||||
*
|
||||
* @Route("/profile")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Route(path="/profile")
|
||||
* @Security("is_granted('view_own_profile') or is_granted('view_other_profile')")
|
||||
*/
|
||||
class ProfileController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/{username}", name="user_profile")
|
||||
* @Method("GET")
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $dispatcher;
|
||||
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
protected $encoder;
|
||||
|
||||
/**
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="fos_user_profile_show", methods={"GET"})
|
||||
*/
|
||||
public function profileAction()
|
||||
{
|
||||
return $this->redirectToRoute('user_profile', ['username' => $this->getUser()->getUsername()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}", name="user_profile", methods={"GET"})
|
||||
* @Security("is_granted('view', profile)")
|
||||
*/
|
||||
public function indexAction(User $profile)
|
||||
@@ -42,8 +71,7 @@ class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{username}/edit", name="user_profile_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{username}/edit", name="user_profile_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', profile)")
|
||||
*/
|
||||
public function editAction(User $profile, Request $request)
|
||||
@@ -56,17 +84,16 @@ class ProfileController extends AbstractController
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_edit', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'settings', $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{username}/password", name="user_profile_password")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{username}/password", name="user_profile_password", methods={"GET", "POST"})
|
||||
* @Security("is_granted('password', profile)")
|
||||
*/
|
||||
public function passwordAction(User $profile, Request $request)
|
||||
@@ -75,25 +102,48 @@ class ProfileController extends AbstractController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$password = $this->get('security.password_encoder')
|
||||
->encodePassword($profile, $profile->getPlainPassword());
|
||||
$password = $this->encoder->encodePassword($profile, $profile->getPlainPassword());
|
||||
$profile->setPassword($password);
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_password', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'password', null, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{username}/roles", name="user_profile_roles")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{username}/api-token", name="user_profile_api_token", methods={"GET", "POST"})
|
||||
* @Security("is_granted('api-token', profile)")
|
||||
*/
|
||||
public function apiTokenAction(User $profile, Request $request)
|
||||
{
|
||||
$form = $this->createApiTokenForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$password = $this->encoder->encodePassword($profile, $profile->getPlainApiToken());
|
||||
$profile->setApiToken($password);
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile_api_token', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'api-token', null, null, null, null, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{username}/roles", name="user_profile_roles", methods={"GET", "POST"})
|
||||
* @Security("is_granted('roles', profile)")
|
||||
*/
|
||||
public function rolesAction(User $profile, Request $request)
|
||||
@@ -106,21 +156,24 @@ class ProfileController extends AbstractController
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_roles', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'roles', null, null, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{username}/prefs", name="user_profile_preferences")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
|
||||
* @Security("is_granted('preferences', profile)")
|
||||
*/
|
||||
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;
|
||||
@@ -147,15 +200,22 @@ class ProfileController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($preferences as $preference) {
|
||||
$preference->setUser($profile);
|
||||
$entityManager->persist($preference);
|
||||
$entityManager->flush();
|
||||
$profile->setPreferences($preferences);
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
// switch locale ONLY if updated profile is the current user
|
||||
$locale = $request->getLocale();
|
||||
if ($this->getUser()->getId() === $profile->getId()) {
|
||||
$locale = $profile->getPreferenceValue('language', $locale);
|
||||
}
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
|
||||
return $this->redirectToRoute('user_profile_preferences', [
|
||||
'_locale' => $locale,
|
||||
'username' => $profile->getUsername()
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'preferences', null, null, null, $form);
|
||||
@@ -168,6 +228,7 @@ class ProfileController extends AbstractController
|
||||
* @param Form|null $pwdForm
|
||||
* @param Form|null $rolesForm
|
||||
* @param Form|null $prefsForm
|
||||
* @param Form|null $apiTokenForm
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
@@ -177,7 +238,8 @@ class ProfileController extends AbstractController
|
||||
Form $editForm = null,
|
||||
Form $pwdForm = null,
|
||||
Form $rolesForm = null,
|
||||
Form $prefsForm = null
|
||||
Form $prefsForm = null,
|
||||
Form $apiTokenForm = null
|
||||
) {
|
||||
/* @var $timesheetRepo TimesheetRepository */
|
||||
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
|
||||
@@ -200,6 +262,10 @@ class ProfileController extends AbstractController
|
||||
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
|
||||
$viewVars['forms']['password'] = $pwdForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::API_TOKEN, $user)) {
|
||||
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
|
||||
$viewVars['forms']['api-token'] = $apiTokenForm->createView();
|
||||
}
|
||||
if ($this->isGranted(UserVoter::ROLES, $user)) {
|
||||
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
||||
$viewVars['forms']['roles'] = $rolesForm->createView();
|
||||
@@ -218,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,
|
||||
@@ -276,4 +346,21 @@ class ProfileController extends AbstractController
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createApiTokenForm(User $user)
|
||||
{
|
||||
return $this->createForm(
|
||||
UserApiTokenType::class,
|
||||
$user,
|
||||
[
|
||||
'validation_groups' => ['apiTokenUpdate'],
|
||||
'action' => $this->generateUrl('user_profile_api_token', ['username' => $user->getUsername()]),
|
||||
'method' => 'POST'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Form\UserPreferencesForm;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Sidebar controller
|
||||
@@ -20,34 +20,14 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
class SidebarController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @param Request $originalRequest
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function homeAction()
|
||||
public function settingsAction(Request $originalRequest)
|
||||
{
|
||||
return $this->render('sidebar/home.html.twig', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function settingsAction()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
return $this->render('sidebar/settings.html.twig', [
|
||||
'user' => $user,
|
||||
'user' => $this->getUser(),
|
||||
'originalRequest' => $originalRequest,
|
||||
]);
|
||||
|
||||
/*
|
||||
|
||||
$form = $this->createForm(UserPreferencesForm::class, $user, [
|
||||
'action' => $this->generateUrl('user_profile_preferences', ['username' => $user->getUsername()]),
|
||||
'method' => 'POST',
|
||||
]);
|
||||
|
||||
return $this->render('sidebar/settings.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,42 +9,42 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Timesheet;
|
||||
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\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* Controller used to manage timesheet contents in the public part of the site.
|
||||
* Controller used to manage timesheets.
|
||||
*
|
||||
* @Route("/timesheet")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
* @Route(path="/timesheet")
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*/
|
||||
class TimesheetController extends AbstractController
|
||||
{
|
||||
use TimesheetControllerTrait;
|
||||
|
||||
/**
|
||||
* TimesheetController constructor.
|
||||
* @param bool $durationOnly
|
||||
* @param int $hardLimit
|
||||
*/
|
||||
public function __construct(bool $durationOnly)
|
||||
public function __construct(int $hardLimit)
|
||||
{
|
||||
$this->setDurationMode($durationOnly);
|
||||
$this->setHardLimit($hardLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="timesheet")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated")
|
||||
* @Method("GET")
|
||||
* @Cache(smaxage="10")
|
||||
* @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_own_timesheet')")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -56,6 +56,12 @@ class TimesheetController extends AbstractController
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$query->setUser($this->getUser());
|
||||
@@ -65,15 +71,49 @@ 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(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "main button and flyout" for displaying (and stopping) active entries.
|
||||
* @Route(path="/export", name="timesheet_export", methods={"GET"})
|
||||
* @Security("is_granted('export_own_timesheet')")
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('timesheet/export.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function activeEntriesAction()
|
||||
@@ -90,8 +130,7 @@ class TimesheetController extends AbstractController
|
||||
/**
|
||||
* The route to stop a running entry.
|
||||
*
|
||||
* @Route("/{id}/stop", name="timesheet_stop")
|
||||
* @Method({"GET"})
|
||||
* @Route(path="/{id}/stop", name="timesheet_stop", methods={"GET"})
|
||||
* @Security("is_granted('stop', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
@@ -103,21 +142,36 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to stop a running entry.
|
||||
*
|
||||
* @Route("/start/{id}", name="timesheet_start", requirements={"id" = "\d+"})
|
||||
* @Method({"GET", "POST"})
|
||||
* @Security("is_granted('start', activity)")
|
||||
* @Route(path="/start/{id}", name="timesheet_start", requirements={"id" = "\d+"}, methods={"GET", "POST"})
|
||||
* @Security("is_granted('start', timesheet)")
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function startAction(Activity $activity)
|
||||
public function startAction(ValidatorInterface $validator, Timesheet $timesheet)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
try {
|
||||
$this->getRepository()->startRecording($user, $activity);
|
||||
$this->flashSuccess('timesheet.start.success');
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
->setBegin(new \DateTime())
|
||||
->setUser($user)
|
||||
->setActivity($timesheet->getActivity())
|
||||
->setProject($timesheet->getProject())
|
||||
;
|
||||
|
||||
$errors = $validator->validate($entry);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$this->flashError('timesheet.start.error', ['%reason%' => $errors[0]->getPropertyPath() . ' = ' . $errors[0]->getMessage()]);
|
||||
} else {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
$this->stopActiveEntries($user);
|
||||
|
||||
$entityManager->flush();
|
||||
$this->flashSuccess('timesheet.start.success');
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('timesheet.start.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
@@ -126,10 +180,7 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to edit an existing entry.
|
||||
*
|
||||
* @Route("/{id}/edit", name="timesheet_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{id}/edit", name="timesheet_edit", methods={"GET", "POST"})
|
||||
* @Security("is_granted('edit', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
@@ -138,28 +189,36 @@ class TimesheetController extends AbstractController
|
||||
*/
|
||||
public function editAction(Timesheet $entry, Request $request)
|
||||
{
|
||||
return $this->edit($entry, $request, 'timesheet_paginated', 'timesheet/edit.html.twig');
|
||||
$route = 'timesheet';
|
||||
|
||||
if (null !== $request->get('page')) {
|
||||
$route = 'timesheet_paginated';
|
||||
} elseif ('calendar' === $request->get('origin')) {
|
||||
$route = 'calendar';
|
||||
}
|
||||
|
||||
return $this->edit($entry, $request, $route, 'timesheet/edit.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to create a new entry by form.
|
||||
*
|
||||
* @Route("/create", name="timesheet_create")
|
||||
* @Method({"GET", "POST"})
|
||||
* @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("/{id}/delete", name="timesheet_delete")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
@@ -173,9 +232,9 @@ class TimesheetController extends AbstractController
|
||||
$entityManager->remove($entry);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('action.deleted.error', ['%reason%' => $ex->getMessage()]);
|
||||
$this->flashError('action.delete.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('timesheet_paginated', ['page' => $request->get('page')]);
|
||||
@@ -183,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()) {
|
||||
@@ -92,13 +93,11 @@ trait TimesheetControllerTrait
|
||||
$record->setEnd($end);
|
||||
}
|
||||
|
||||
// TODO validate that end is not before begin
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute($redirectRoute, ['page' => $request->get('page')]);
|
||||
}
|
||||
@@ -121,7 +120,41 @@ trait TimesheetControllerTrait
|
||||
$entry->setUser($this->getUser());
|
||||
$entry->setBegin(new \DateTime());
|
||||
|
||||
$createForm = $this->getCreateForm($entry);
|
||||
$start = $request->get('begin');
|
||||
if ($start !== null) {
|
||||
$start = \DateTime::createFromFormat('Y-m-d', $start);
|
||||
if ($start !== false) {
|
||||
$start->setTime(10, 0, 0); // TODO make me configurable
|
||||
$entry->setBegin($start);
|
||||
}
|
||||
}
|
||||
|
||||
$end = $request->get('end');
|
||||
if ($end !== null) {
|
||||
$end = \DateTime::createFromFormat('Y-m-d', $end);
|
||||
if ($end !== false) {
|
||||
$end->setTime(18, 0, 0); // TODO make me configurable
|
||||
$entry->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
$from = $request->get('from');
|
||||
if ($from !== null) {
|
||||
$from = new \DateTime($from);
|
||||
if ($from !== false) {
|
||||
$entry->setBegin($from);
|
||||
}
|
||||
}
|
||||
|
||||
$to = $request->get('to');
|
||||
if ($to !== null) {
|
||||
$to = new \DateTime($to);
|
||||
if ($to !== false) {
|
||||
$entry->setEnd($to);
|
||||
}
|
||||
}
|
||||
|
||||
$createForm = $this->getCreateForm($entry, $redirectRoute);
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||
@@ -136,14 +169,17 @@ trait TimesheetControllerTrait
|
||||
}
|
||||
}
|
||||
|
||||
// TODO validate that end is not before begin
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
|
||||
$entityManager->flush();
|
||||
try {
|
||||
$this->stopActiveEntries($entry->getUser());
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('timesheet.start.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute($redirectRoute);
|
||||
}
|
||||
@@ -154,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.
|
||||
|
||||
144
src/DataFixtures/CustomerFixtures.php
Normal file
144
src/DataFixtures/CustomerFixtures.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?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\DataFixtures;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests or while development.
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* bin/console doctrine:fixtures:load
|
||||
*/
|
||||
class CustomerFixtures extends Fixture
|
||||
{
|
||||
public const MIN_CUSTOMERS = 5;
|
||||
public const MAX_CUSTOMERS = 15;
|
||||
public const MIN_BUDGET = 0;
|
||||
public const MAX_BUDGET = 100000;
|
||||
public const MIN_GLOBAL_ACTIVITIES = 5;
|
||||
public const MAX_GLOBAL_ACTIVITIES = 50;
|
||||
public const MIN_PROJECTS_PER_CUSTOMER = 10;
|
||||
public const MAX_PROJECTS_PER_CUSTOMER = 50;
|
||||
public const MIN_ACTIVITIES_PER_PROJECT = 0;
|
||||
public const MAX_ACTIVITIES_PER_PROJECT = 15;
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$faker = Factory::create();
|
||||
|
||||
$amountCustomers = rand(self::MIN_CUSTOMERS, self::MAX_CUSTOMERS);
|
||||
for ($c = 1; $c <= $amountCustomers; $c++) {
|
||||
$visibleCustomer = 0 != $c % 3;
|
||||
$customer = $this->createCustomer($faker, $visibleCustomer);
|
||||
|
||||
$projectForCustomer = rand(self::MIN_PROJECTS_PER_CUSTOMER, self::MAX_PROJECTS_PER_CUSTOMER);
|
||||
for ($p = 1; $p <= $projectForCustomer; $p++) {
|
||||
$visibleProject = 0 != $p % 3;
|
||||
$project = $this->createProject($faker, $customer, $visibleProject);
|
||||
$manager->persist($project);
|
||||
|
||||
$activityForProject = rand(self::MIN_ACTIVITIES_PER_PROJECT, self::MAX_ACTIVITIES_PER_PROJECT);
|
||||
for ($a = 1; $a <= $activityForProject; $a++) {
|
||||
$visibleActivity = 0 != $a % 3;
|
||||
$activity = $this->createActivity($faker, $project, $visibleActivity);
|
||||
$manager->persist($activity);
|
||||
}
|
||||
}
|
||||
|
||||
$manager->persist($customer);
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
|
||||
$amountGlobalActivities = rand(self::MIN_GLOBAL_ACTIVITIES, self::MAX_GLOBAL_ACTIVITIES);
|
||||
for ($c = 1; $c <= $amountGlobalActivities; $c++) {
|
||||
$visibleActivity = 0 != $a % 3;
|
||||
$activity = $this->createActivity($faker, null, $visibleActivity);
|
||||
$manager->persist($activity);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Generator $faker
|
||||
* @param bool $visible
|
||||
* @return Customer
|
||||
*/
|
||||
private function createCustomer(Generator $faker, $visible)
|
||||
{
|
||||
$entry = new Customer();
|
||||
$entry
|
||||
->setCurrency($faker->currencyCode)
|
||||
->setName($faker->company . ($visible ? '' : ' (x)'))
|
||||
->setAddress($faker->address)
|
||||
->setComment($faker->text)
|
||||
->setNumber('C-' . $faker->ean8)
|
||||
->setCountry($faker->countryCode)
|
||||
->setTimezone($faker->timezone)
|
||||
->setVisible($visible)
|
||||
;
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Generator $faker
|
||||
* @param Customer $customer
|
||||
* @param bool $visible
|
||||
* @return Project
|
||||
*/
|
||||
private function createProject(Generator $faker, Customer $customer, $visible)
|
||||
{
|
||||
$entry = new Project();
|
||||
|
||||
$entry
|
||||
->setName($faker->catchPhrase . ($visible ? '' : ' (x)'))
|
||||
->setBudget(rand(self::MIN_BUDGET, self::MAX_BUDGET))
|
||||
->setComment($faker->text)
|
||||
->setCustomer($customer)
|
||||
->setVisible($visible)
|
||||
;
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Generator $faker
|
||||
* @param Project|null $project
|
||||
* @param bool $visible
|
||||
* @return Activity
|
||||
*/
|
||||
private function createActivity(Generator $faker, ?Project $project, $visible)
|
||||
{
|
||||
$entry = new Activity();
|
||||
$entry
|
||||
->setName($faker->bs . ($visible ? '' : ' (x)'))
|
||||
->setProject($project)
|
||||
->setComment($faker->text)
|
||||
->setVisible($visible)
|
||||
;
|
||||
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +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\DataFixtures;
|
||||
|
||||
/**
|
||||
* Defines reusable sample data.
|
||||
*/
|
||||
trait FixturesTrait
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getPhrases()
|
||||
{
|
||||
return [
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit',
|
||||
'Pellentesque vitae velit ex',
|
||||
'Mauris dapibus risus quis suscipit vulputate',
|
||||
'Eros diam egestas libero eu vulputate risus',
|
||||
'In hac habitasse platea dictumst',
|
||||
'Morbi tempus commodo mattis',
|
||||
'Ut suscipit posuere justo at vulputate',
|
||||
'Ut eleifend mauris et risus ultrices egestas',
|
||||
'Aliquam sodales odio id eleifend tristique',
|
||||
'Urna nisl sollicitudin id varius orci quam id turpis',
|
||||
'Nulla porta lobortis ligula vel egestas',
|
||||
'Curabitur aliquam euismod dolor non ornare',
|
||||
'Sed varius a risus eget aliquam',
|
||||
'Nunc viverra elit ac laoreet suscipit',
|
||||
'Pellentesque et sapien pulvinar consectetur',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getRandomPhrase()
|
||||
{
|
||||
$phrases = $this->getPhrases();
|
||||
|
||||
return $phrases[array_rand($phrases)];
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ namespace App\DataFixtures;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
@@ -27,36 +29,92 @@ class InvoiceFixtures extends Fixture
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$this->loadInvoiceTemplates($manager);
|
||||
foreach ($this->getInvoiceConfigs() as $invoiceConfig) {
|
||||
$template = new InvoiceTemplate();
|
||||
|
||||
// name, title, renderer, calculator, numberGenerator, company, vat, dueDays, address, paymentTerms
|
||||
$template
|
||||
->setName($invoiceConfig[0])
|
||||
->setTitle($invoiceConfig[1])
|
||||
->setRenderer($invoiceConfig[2])
|
||||
->setCalculator($invoiceConfig[3])
|
||||
->setNumberGenerator($invoiceConfig[4])
|
||||
->setCompany($invoiceConfig[5])
|
||||
->setVat($invoiceConfig[6])
|
||||
->setDueDays($invoiceConfig[7])
|
||||
->setAddress($invoiceConfig[8])
|
||||
->setPaymentTerms($invoiceConfig[9])
|
||||
;
|
||||
|
||||
$manager->persist($template);
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
private function loadInvoiceTemplates(ObjectManager $manager)
|
||||
private function getInvoiceConfigs()
|
||||
{
|
||||
$template = new InvoiceTemplate();
|
||||
$template
|
||||
->setName('Invoice')
|
||||
->setTitle('Your company name')
|
||||
->setCompany('Kimai, Inc.')
|
||||
->setVat(19)
|
||||
->setDueDays(14)
|
||||
->setPaymentTerms(
|
||||
'I would like to thank you for your confidence and will gladly be there for you in the future.' .
|
||||
PHP_EOL .
|
||||
'Please transfer the total amount within 14 days to the given account and use the invoice number ' .
|
||||
'as reference.'
|
||||
)
|
||||
->setAddress(
|
||||
'795 Folsom Ave, Suite 600' . PHP_EOL .
|
||||
'San Francisco, CA 94107' . PHP_EOL .
|
||||
'Phone: (804) 123-456789' . PHP_EOL .
|
||||
'Email: email@example.com'
|
||||
)
|
||||
$faker = Factory::create();
|
||||
|
||||
$paymentTerms =
|
||||
'I would like to thank you for your confidence and will gladly be there for you in the future.' .
|
||||
PHP_EOL .
|
||||
'Please transfer the total amount within 14 days to the given account and use the invoice number ' .
|
||||
'as reference.'
|
||||
;
|
||||
|
||||
$manager->persist($template);
|
||||
$manager->flush();
|
||||
$address =
|
||||
$faker->streetAddress . PHP_EOL .
|
||||
$faker->city . ', ' . $faker->stateAbbr . ' ' . $faker->postcode . PHP_EOL .
|
||||
'Phone: ' . $faker->phoneNumber . PHP_EOL .
|
||||
'Email: ' . $faker->safeEmail
|
||||
;
|
||||
|
||||
$paymentTerms_de =
|
||||
'Bitte überweisen Sie den Gesamtbetrag innerhalb von 14 Tagen nach Erhalt der Rechnung auf das unten genannte Konto. Verwenden Sie bitte als Betreff Ihrer Überweisung die Rechnungsnummer.' .
|
||||
PHP_EOL .
|
||||
PHP_EOL .
|
||||
'Ich bedanke mich für das entgegengebrachte Vertrauen. Gerne bin ich auch künftig für Sie da.' .
|
||||
PHP_EOL .
|
||||
PHP_EOL .
|
||||
'Mit freundlichen Grüßen,' .
|
||||
PHP_EOL .
|
||||
'Max Müller'
|
||||
;
|
||||
|
||||
// name, title, renderer, calculator, numberGenerator, company, vat, dueDays, address, paymentTerms
|
||||
return [
|
||||
['Invoice (HTML)', 'Company name', 'default', 'default', 'default', $faker->company, 19, 30, $address, $paymentTerms],
|
||||
['Freelancer (HTML, short)', 'Invoice', 'freelancer', 'short', 'default', $faker->company, 19, 14, $this->generateAddress($faker), $paymentTerms_de],
|
||||
['Timesheet (HTML)', 'Timesheet', 'timesheet', 'default', 'default', $faker->company, 19, 7, $this->generateAddress($faker), ''],
|
||||
['Company invoice (DOCX)', 'Invoice', 'company', 'default', 'default', 'Kimai Inc.', 19, 14, $this->generateAddress($faker, true), $this->generatePaymentTerms($faker)],
|
||||
['Export (CSV, user-group)', 'User-grouped', 'export', 'user', 'default', $faker->company, 7, 28, '', ''],
|
||||
['Export (ODS)', 'Spreadsheet', 'open-spreadsheet', 'default', 'default', $faker->company, 19, 14, '', ''],
|
||||
['Export (XLSX, user-group)', 'Spreadsheet', 'spreadsheet', 'user', 'default', $faker->company, 13, 10, '', ''],
|
||||
];
|
||||
}
|
||||
|
||||
protected function generatePaymentTerms(Generator $faker)
|
||||
{
|
||||
return
|
||||
'Acme Bank' . PHP_EOL .
|
||||
'Account: ' . $faker->bankAccountNumber . PHP_EOL .
|
||||
'IBAN: ' . $faker->iban('DE')
|
||||
;
|
||||
}
|
||||
|
||||
protected function generateAddress(Generator $faker, $lineBreaks = false)
|
||||
{
|
||||
if (!$lineBreaks) {
|
||||
return
|
||||
$faker->name . ' - ' .
|
||||
$faker->streetAddress . '-' .
|
||||
$faker->postcode . ' ' . $faker->city;
|
||||
}
|
||||
|
||||
return
|
||||
'Kimai Inc.' . PHP_EOL .
|
||||
$faker->streetAddress . PHP_EOL .
|
||||
$faker->city . ', ' . $faker->stateAbbr . ' ' . $faker->postcode
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,37 +10,108 @@
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests or while development.
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
* bin/console doctrine:fixtures:load
|
||||
*/
|
||||
class TimesheetFixtures extends Fixture
|
||||
class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
use FixturesTrait;
|
||||
public const MIN_TIMESHEETS_PER_USER = 50;
|
||||
public const MAX_TIMESHEETS_PER_USER = 500;
|
||||
public const MAX_TIMESHEETS_TOTAL = 5000;
|
||||
public const MIN_RUNNING_TIMESHEETS_PER_USER = 0;
|
||||
public const MAX_RUNNING_TIMESHEETS_PER_USER = 3;
|
||||
public const TIMERANGE_DAYS = 1095; // 3 years
|
||||
public const MIN_MINUTES_PER_ENTRY = 15;
|
||||
public const MAX_MINUTES_PER_ENTRY = 840; // 14h
|
||||
|
||||
public const AMOUNT_TIMESHEET = 5000; // timesheet entries total
|
||||
public const RATE_MIN = 10; // minimum rate for one hour
|
||||
public const RATE_MAX = 80; // maximum rate for one hour
|
||||
public const BATCH_SIZE = 100;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDependencies()
|
||||
{
|
||||
return [
|
||||
UserFixtures::class,
|
||||
CustomerFixtures::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$this->loadCustomers($manager);
|
||||
$this->loadProjects($manager);
|
||||
$this->loadActivities($manager);
|
||||
$this->loadTimesheet($manager);
|
||||
$allUser = $this->getAllUsers($manager);
|
||||
$activities = $this->getAllActivities($manager);
|
||||
$projects = $this->getAllProjects($manager);
|
||||
|
||||
$faker = Factory::create();
|
||||
|
||||
// by using array_pop we make sure that at least one activity has NO entry!
|
||||
array_pop($activities);
|
||||
|
||||
foreach ($allUser as $user) {
|
||||
// random amount of timesheet entries for every user
|
||||
$timesheetForUser = rand(self::MIN_TIMESHEETS_PER_USER, self::MAX_TIMESHEETS_PER_USER);
|
||||
for ($i = 1; $i <= $timesheetForUser; $i++) {
|
||||
if ($i > self::MAX_TIMESHEETS_TOTAL) {
|
||||
break;
|
||||
}
|
||||
|
||||
$description = null;
|
||||
if ($i % 3 == 0) {
|
||||
$description = $faker->text;
|
||||
} elseif ($i % 7 == 0) {
|
||||
$description = '';
|
||||
}
|
||||
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
$description
|
||||
);
|
||||
|
||||
$manager->persist($entry);
|
||||
|
||||
if ($i % self::BATCH_SIZE == 0) {
|
||||
//echo '['.$i.'] Timesheets for User ' . $user->getId() . PHP_EOL;
|
||||
$manager->flush();
|
||||
$manager->clear(Timesheet::class);
|
||||
}
|
||||
}
|
||||
|
||||
// create active recordings for test user
|
||||
$activeEntries = rand(self::MIN_RUNNING_TIMESHEETS_PER_USER, self::MAX_RUNNING_TIMESHEETS_PER_USER);
|
||||
for ($i = 0; $i < $activeEntries; $i++) {
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
null,
|
||||
false
|
||||
);
|
||||
$manager->persist($entry);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear(Timesheet::class);
|
||||
}
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,22 +130,6 @@ class TimesheetFixtures extends Fixture
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Customer[]
|
||||
*/
|
||||
protected function getAllCustomers(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var Customer[] $entries */
|
||||
$entries = $manager->getRepository(Customer::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Project[]
|
||||
@@ -107,338 +162,33 @@ class TimesheetFixtures extends Fixture
|
||||
return $all;
|
||||
}
|
||||
|
||||
private function loadTimesheet(ObjectManager $manager)
|
||||
{
|
||||
$allUser = $this->getAllUsers($manager);
|
||||
$amountUser = count($allUser);
|
||||
$allActivity = $this->getAllActivities($manager);
|
||||
|
||||
// by using array_pop we make sure that at least one activity has NO entry!
|
||||
array_pop($allActivity);
|
||||
|
||||
for ($i = 0; $i <= self::AMOUNT_TIMESHEET; $i++) {
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$allUser[rand(1, $amountUser)],
|
||||
$allActivity[array_rand($allActivity)],
|
||||
round($i / 2),
|
||||
true
|
||||
);
|
||||
|
||||
$manager->persist($entry);
|
||||
}
|
||||
|
||||
// by using array_pop we make sure that at least one user has NO running entry!
|
||||
array_pop($allUser);
|
||||
|
||||
// create active recodinge for test user
|
||||
foreach ($allUser as $id => $user) {
|
||||
for ($i = 0; $i < rand(1, 4); $i++) {
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$allActivity[array_rand($allActivity)]
|
||||
);
|
||||
$manager->persist($entry);
|
||||
}
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function createTimesheetEntry(User $user, Activity $activity, $startDay = 0, $setEndDate = false)
|
||||
private function createTimesheetEntry(User $user, Activity $activity, Project $project, $description, $setEndDate = true)
|
||||
{
|
||||
$start = new \DateTime();
|
||||
if ($startDay > 0) {
|
||||
$start = $start->modify('- ' . (rand(1, $startDay)) . ' days');
|
||||
}
|
||||
$start = $start->modify('- ' . (rand(1, self::TIMERANGE_DAYS)) . ' days');
|
||||
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');
|
||||
|
||||
$end = clone $start;
|
||||
$end = $end->modify('+ ' . (rand(1, 43200)) . ' seconds');
|
||||
|
||||
//$duration = $end->modify('- ' . $start->getTimestamp() . ' seconds')->getTimestamp();
|
||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||
$rate = rand(self::RATE_MIN, self::RATE_MAX);
|
||||
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
->setActivity($activity)
|
||||
->setDescription($this->getRandomPhrase())
|
||||
->setProject($activity->getProject() ?? $project)
|
||||
->setDescription($description)
|
||||
->setUser($user)
|
||||
->setRate(round(($duration / 3600) * $rate))
|
||||
->setBegin($start);
|
||||
|
||||
if ($setEndDate) {
|
||||
$end = clone $start;
|
||||
$end = $end->modify('+ ' . (rand(self::MIN_MINUTES_PER_ENTRY, self::MAX_MINUTES_PER_ENTRY)) . ' minutes');
|
||||
|
||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||
$rate = $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
|
||||
$entry
|
||||
->setEnd($end)
|
||||
->setRate(round(($duration / 3600) * $rate))
|
||||
->setDuration($duration);
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function loadCustomers(ObjectManager $manager)
|
||||
{
|
||||
$allTimezones = \DateTimeZone::listIdentifiers();
|
||||
$amountTimezone = count($allTimezones);
|
||||
|
||||
$allCustomer = $this->getCustomers();
|
||||
shuffle($allCustomer);
|
||||
$i = 1;
|
||||
|
||||
foreach ($allCustomer as $customerName) {
|
||||
$visible = 0 != $i++ % 6;
|
||||
$entry = new Customer();
|
||||
$entry
|
||||
->setCurrency($this->getRandomCurrency())
|
||||
->setName($customerName . ($visible ? '' : '.'))
|
||||
->setAddress($this->getRandomLocation())
|
||||
->setComment($this->getRandomPhrase())
|
||||
->setVisible($visible)
|
||||
->setNumber('C0815-42-' . $i)
|
||||
->setCountry('DE') // TODO randomize country ?
|
||||
->setTimezone($allTimezones[rand(1, $amountTimezone)]);
|
||||
|
||||
$manager->persist($entry);
|
||||
}
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function loadProjects(ObjectManager $manager)
|
||||
{
|
||||
$allCustomer = $this->getAllCustomers($manager);
|
||||
|
||||
foreach ($allCustomer as $id => $customer) {
|
||||
$projectForCustomer = rand(0, 7);
|
||||
for ($i = 1; $i <= $projectForCustomer; $i++) {
|
||||
$visible = 0 != $i % 5;
|
||||
$entry = new Project();
|
||||
|
||||
$entry
|
||||
->setName($this->getRandomProject() . ($visible ? '' : '.'))
|
||||
->setBudget(rand(500, 100000))
|
||||
->setComment($this->getRandomPhrase())
|
||||
->setCustomer($customer)
|
||||
->setVisible($visible);
|
||||
|
||||
$manager->persist($entry);
|
||||
}
|
||||
}
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function loadActivities(ObjectManager $manager)
|
||||
{
|
||||
$allProject = $this->getAllProjects($manager);
|
||||
|
||||
foreach ($allProject as $projectId => $project) {
|
||||
$activityCount = rand(0, 10);
|
||||
for ($i = 1; $i <= $activityCount; $i++) {
|
||||
$visible = 0 != $i % 4;
|
||||
$entry = new Activity();
|
||||
$entry
|
||||
->setName($this->getRandomActivity() . ($visible ? '' : '.'))
|
||||
->setProject($project)
|
||||
->setComment($this->getRandomPhrase())
|
||||
->setVisible($visible);
|
||||
|
||||
$manager->persist($entry);
|
||||
}
|
||||
}
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getActivities()
|
||||
{
|
||||
return [
|
||||
'Designing',
|
||||
'Programming',
|
||||
'Testing',
|
||||
'Documentation',
|
||||
'Pause',
|
||||
'Research',
|
||||
'Meeting',
|
||||
'Hosting',
|
||||
'Relaunch',
|
||||
'Support',
|
||||
'Refactoring',
|
||||
'Interview',
|
||||
'Administration',
|
||||
'DevOps',
|
||||
'Management',
|
||||
'Setup',
|
||||
'Planning',
|
||||
'Skiing',
|
||||
'Eating',
|
||||
'Watching TV',
|
||||
'Talking',
|
||||
'Cooking',
|
||||
'Writing',
|
||||
'Reading',
|
||||
'Brainstroming',
|
||||
'Post Processing',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getRandomActivity()
|
||||
{
|
||||
$all = $this->getActivities();
|
||||
|
||||
return $all[array_rand($all)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getProjects()
|
||||
{
|
||||
return [
|
||||
'User Experience',
|
||||
'Database Migration',
|
||||
'Test Automatisation',
|
||||
'Website Redesign',
|
||||
'API Development',
|
||||
'Hosting & Server',
|
||||
'Customer Relations',
|
||||
'Infrastructure',
|
||||
'Princess Cat',
|
||||
'Software Upgrade',
|
||||
'Office Management',
|
||||
'Project X',
|
||||
'Customer Excellence',
|
||||
'Crazy Monkey',
|
||||
'Interface Design',
|
||||
'Human Ressources',
|
||||
'Book Release',
|
||||
'Studio Photography',
|
||||
'Professional Art',
|
||||
'Video Production',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getRandomProject()
|
||||
{
|
||||
$all = $this->getProjects();
|
||||
|
||||
return $all[array_rand($all)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getLocations()
|
||||
{
|
||||
return [
|
||||
'Köln',
|
||||
'München',
|
||||
'New York',
|
||||
'Buenos Aires',
|
||||
'Hawai',
|
||||
'Amsterdam',
|
||||
'London',
|
||||
'San Francisco',
|
||||
'Tokyo',
|
||||
'Berlin',
|
||||
'Sao Paulo',
|
||||
'Mexico City',
|
||||
'Moscow',
|
||||
'Sankt Petersburg',
|
||||
'Taiwan',
|
||||
'Perth',
|
||||
'Sydney',
|
||||
'Mumbai',
|
||||
'Lagos',
|
||||
'Karachi',
|
||||
'Shanghai',
|
||||
'Delhi',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getRandomLocation()
|
||||
{
|
||||
$all = $this->getLocations();
|
||||
|
||||
return $all[array_rand($all)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getCustomers()
|
||||
{
|
||||
return [
|
||||
'Acme University',
|
||||
'Snake Oil',
|
||||
'Apple',
|
||||
'Microsoft',
|
||||
'Google',
|
||||
'Oracle',
|
||||
'Yahoo',
|
||||
'Twitter',
|
||||
'Zend',
|
||||
'SensioLabs',
|
||||
'Samsung',
|
||||
'Huawai',
|
||||
'Yandex',
|
||||
'Baidu',
|
||||
'Alphabet',
|
||||
'Amazon.com',
|
||||
'Berkshire Hathaway',
|
||||
'Facebook',
|
||||
'ExxonMobil',
|
||||
'Nestle',
|
||||
'Johnson & Johnson',
|
||||
'Alibaba',
|
||||
'General Electric',
|
||||
'Procter & Gamble',
|
||||
'Wal-Mart Stores',
|
||||
'Novartis',
|
||||
'Coca-Cola',
|
||||
'Wikipedia',
|
||||
'Walt Disney',
|
||||
'Merck',
|
||||
'Pfizer',
|
||||
"L'Oréal Group",
|
||||
"McDonald's",
|
||||
'China Petroleum & Chemical',
|
||||
'GlaxoSmithKline'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getCurrencies()
|
||||
{
|
||||
return [
|
||||
'EUR',
|
||||
'GBP',
|
||||
'USD',
|
||||
'RUB',
|
||||
'JPY',
|
||||
'CNY',
|
||||
'INR'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getRandomCurrency()
|
||||
{
|
||||
$all = $this->getCurrencies();
|
||||
|
||||
return $all[array_rand($all)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
/**
|
||||
@@ -22,23 +23,30 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
* Execute this command to load the data:
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*/
|
||||
class AppFixtures extends Fixture
|
||||
class UserFixtures extends Fixture
|
||||
{
|
||||
use FixturesTrait;
|
||||
|
||||
public const DEFAULT_PASSWORD = 'kitten';
|
||||
public const DEFAULT_API_TOKEN = 'api_kitten';
|
||||
public const DEFAULT_AVATAR = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y';
|
||||
|
||||
public const USERNAME_USER = 'john_user';
|
||||
public const USERNAME_TEAMLEAD = 'tony_teamlead';
|
||||
public const USERNAME_ADMIN = 'anna_admin';
|
||||
public const USERNAME_SUPER_ADMIN = 'susan_super';
|
||||
|
||||
public const AMOUNT_EXTRA_USER = 2;
|
||||
|
||||
public const MIN_RATE = 30;
|
||||
public const MAX_RATE = 120;
|
||||
|
||||
public const BATCH_SIZE = 50;
|
||||
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
private $encoder;
|
||||
|
||||
/**
|
||||
* AppFixtures constructor.
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder)
|
||||
@@ -51,17 +59,21 @@ class AppFixtures extends Fixture
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$this->loadUsers($manager);
|
||||
$this->loadDefaultAccounts($manager);
|
||||
$this->loadTestUsers($manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default users for all test cases
|
||||
*
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
private function loadUsers(ObjectManager $manager)
|
||||
private function loadDefaultAccounts(ObjectManager $manager)
|
||||
{
|
||||
$passwordEncoder = $this->encoder;
|
||||
|
||||
foreach ($this->getUserDefinition() as $userData) {
|
||||
$allUsers = $this->getUserDefinition();
|
||||
foreach ($allUsers as $userData) {
|
||||
$user = new User();
|
||||
$user
|
||||
->setAlias($userData[0])
|
||||
@@ -72,18 +84,65 @@ class AppFixtures extends Fixture
|
||||
->setAvatar($userData[5])
|
||||
->setEnabled($userData[6])
|
||||
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
|
||||
->setApiToken($passwordEncoder->encodePassword($user, self::DEFAULT_API_TOKEN))
|
||||
->setPreferences([$this->getUserPreference($user)])
|
||||
;
|
||||
|
||||
$preference = new UserPreference();
|
||||
$preference->setName(UserPreference::HOURLY_RATE);
|
||||
$preference->setValue(rand(0, 100));
|
||||
$preference->setUser($user);
|
||||
$user->setPreferences([$preference]);
|
||||
|
||||
$manager->persist($user);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return UserPreference
|
||||
*/
|
||||
private function getUserPreference(user $user)
|
||||
{
|
||||
$preference = new UserPreference();
|
||||
$preference->setName(UserPreference::HOURLY_RATE);
|
||||
$preference->setValue(rand(self::MIN_RATE, self::MAX_RATE));
|
||||
$preference->setUser($user);
|
||||
|
||||
return $preference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate randomized test users, which don't have API access.
|
||||
*
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
private function loadTestUsers(ObjectManager $manager)
|
||||
{
|
||||
$passwordEncoder = $this->encoder;
|
||||
|
||||
$faker = Factory::create();
|
||||
for ($i = 1; $i <= self::AMOUNT_EXTRA_USER; $i++) {
|
||||
$user = new User();
|
||||
$user
|
||||
->setAlias($faker->name)
|
||||
->setTitle(substr($faker->jobTitle, 0, 49))
|
||||
->setUsername($faker->userName)
|
||||
->setEmail($faker->email)
|
||||
->setRoles([User::ROLE_USER])
|
||||
->setAvatar(self::DEFAULT_AVATAR)
|
||||
->setEnabled(true)
|
||||
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
|
||||
->setPreferences([$this->getUserPreference($user)])
|
||||
;
|
||||
|
||||
if ($i % self::BATCH_SIZE == 0) {
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
|
||||
$manager->persist($user);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,29 +152,29 @@ class AppFixtures extends Fixture
|
||||
{
|
||||
return [
|
||||
[
|
||||
'Clara Haynes', 'CFO', 'clara_customer', 'clara_customer@example.com', 'ROLE_CUSTOMER',
|
||||
'Clara Haynes', 'CFO', 'clara_customer', 'clara_customer@example.com', User::ROLE_CUSTOMER,
|
||||
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y', true
|
||||
],
|
||||
[
|
||||
'John Doe', 'Developer', self::USERNAME_USER, 'john_user@example.com', 'ROLE_USER',
|
||||
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', true
|
||||
'John Doe', 'Developer', self::USERNAME_USER, 'john_user@example.com', User::ROLE_USER,
|
||||
self::DEFAULT_AVATAR, true
|
||||
],
|
||||
// inactive user to test login
|
||||
[
|
||||
'Chris Deactive', 'Developer (left company)', 'chris_user', 'chris_user@example.com', 'ROLE_USER',
|
||||
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', false
|
||||
'Chris Deactive', 'Developer (left company)', 'chris_user', 'chris_user@example.com', User::ROLE_USER,
|
||||
self::DEFAULT_AVATAR, false
|
||||
],
|
||||
[
|
||||
'Tony Maier', 'Head of Sales', self::USERNAME_TEAMLEAD, 'tony_teamlead@example.com', 'ROLE_TEAMLEAD',
|
||||
'Tony Maier', 'Head of Sales', self::USERNAME_TEAMLEAD, 'tony_teamlead@example.com', User::ROLE_TEAMLEAD,
|
||||
'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg', true
|
||||
],
|
||||
// no avatar to test default image macro
|
||||
[
|
||||
'Anna Smith', 'Administrator', self::USERNAME_ADMIN, 'anna_admin@example.com', 'ROLE_ADMIN', null, true
|
||||
'Anna Smith', 'Administrator', self::USERNAME_ADMIN, 'anna_admin@example.com', User::ROLE_ADMIN, null, true
|
||||
],
|
||||
// no alias to test twig username macro
|
||||
[
|
||||
null, 'Super Administrator', self::USERNAME_SUPER_ADMIN, 'susan_super@example.com', 'ROLE_SUPER_ADMIN',
|
||||
null, 'Super Administrator', self::USERNAME_SUPER_ADMIN, 'susan_super@example.com', User::ROLE_SUPER_ADMIN,
|
||||
'/build/images/default_avatar.png', true
|
||||
]
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user