Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
0
.github/lock.yaml → .github/lock.yml
vendored
0
.github/lock.yaml → .github/lock.yml
vendored
@@ -1,2 +1,3 @@
|
||||
unreleased=true
|
||||
future-release=0.4
|
||||
future-release=0.6
|
||||
exclude-labels=duplicate,question,invalid,wontfix,release
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -7,11 +7,16 @@
|
||||
!bin/console
|
||||
/config/packages/local.yaml
|
||||
|
||||
/var/data/kimai.sqlite
|
||||
templates/invoice/renderer/.~lock*
|
||||
|
||||
/var/data/*.sqlite
|
||||
!/var/data/kimai_test.sqlite
|
||||
/var/coverage/
|
||||
/var/cache/*
|
||||
!var/cache/.gitkeep
|
||||
|
||||
/var/invoices/*
|
||||
|
||||
/var/log/*
|
||||
!var/log/.gitkeep
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
120
CHANGELOG.md
120
CHANGELOG.md
@@ -1,5 +1,120 @@
|
||||
# Change Log
|
||||
|
||||
## [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)
|
||||
|
||||
**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:**
|
||||
|
||||
- Language setting does not work immediately [\#418](https://github.com/kevinpapst/kimai2/issues/418)
|
||||
- After installation no graphic/background/icons shown [\#417](https://github.com/kevinpapst/kimai2/issues/417)
|
||||
- Form fields should be always visible [\#408](https://github.com/kevinpapst/kimai2/issues/408)
|
||||
- Merge projects and/or activities [\#406](https://github.com/kevinpapst/kimai2/issues/406)
|
||||
- Active filter should stay open [\#405](https://github.com/kevinpapst/kimai2/issues/405)
|
||||
- Make project field optional [\#401](https://github.com/kevinpapst/kimai2/issues/401)
|
||||
- Global activities support for Kimai 1 importer [\#400](https://github.com/kevinpapst/kimai2/issues/400)
|
||||
- Unrecognized Options "renderer, number\_generator" under "kimai.invoice" [\#381](https://github.com/kevinpapst/kimai2/issues/381)
|
||||
- Invoice Grouped by Activity [\#379](https://github.com/kevinpapst/kimai2/issues/379)
|
||||
- Set the order of columns \(in my times page\) [\#371](https://github.com/kevinpapst/kimai2/issues/371)
|
||||
- Columns on small/narrow displays \(mobile\) on „my times“ page [\#370](https://github.com/kevinpapst/kimai2/issues/370)
|
||||
- Make settings more accessible [\#365](https://github.com/kevinpapst/kimai2/issues/365)
|
||||
- Data validation for register page username field. [\#360](https://github.com/kevinpapst/kimai2/issues/360)
|
||||
- Option for setting default calendar view [\#359](https://github.com/kevinpapst/kimai2/issues/359)
|
||||
- 500: Internal Server Error after login \(new install\) [\#342](https://github.com/kevinpapst/kimai2/issues/342)
|
||||
- Invoices export error [\#341](https://github.com/kevinpapst/kimai2/issues/341)
|
||||
- Activities just Duration [\#334](https://github.com/kevinpapst/kimai2/issues/334)
|
||||
- API: Add filtering/sorting/limits [\#333](https://github.com/kevinpapst/kimai2/issues/333)
|
||||
- add the same activity for multiple projects [\#325](https://github.com/kevinpapst/kimai2/issues/325)
|
||||
- API: Endpoint for API docs needs session cookie [\#319](https://github.com/kevinpapst/kimai2/issues/319)
|
||||
- API: Timesheets [\#315](https://github.com/kevinpapst/kimai2/issues/315)
|
||||
- Feature request: Quick search box for activities when creating new timesheet [\#307](https://github.com/kevinpapst/kimai2/issues/307)
|
||||
- docker image for kimai2 [\#284](https://github.com/kevinpapst/kimai2/issues/284)
|
||||
- Separate Customer / Project / Activity input when logging time [\#250](https://github.com/kevinpapst/kimai2/issues/250)
|
||||
- Interactive help after fresh installation [\#94](https://github.com/kevinpapst/kimai2/issues/94)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- 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)
|
||||
|
||||
**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)
|
||||
- API: Kimai metadata endpoint [\#320](https://github.com/kevinpapst/kimai2/issues/320)
|
||||
- Support Markdown in timesheet description [\#295](https://github.com/kevinpapst/kimai2/issues/295)
|
||||
- Add permission voter for invoices and templates [\#293](https://github.com/kevinpapst/kimai2/issues/293)
|
||||
- Add support for MS Office templates [\#283](https://github.com/kevinpapst/kimai2/issues/283)
|
||||
- Adding rates to customers, projects and tasks [\#271](https://github.com/kevinpapst/kimai2/issues/271)
|
||||
- Delete invoice templates [\#267](https://github.com/kevinpapst/kimai2/issues/267)
|
||||
- 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)
|
||||
|
||||
**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)
|
||||
|
||||
@@ -12,11 +127,10 @@
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Improve dashboard display [\#280](https://github.com/kevinpapst/kimai2/issues/280)
|
||||
- CSV export [\#263](https://github.com/kevinpapst/kimai2/issues/263)
|
||||
- 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)
|
||||
- Delete user [\#225](https://github.com/kevinpapst/kimai2/issues/225)
|
||||
- Feature request - Monthly overview / report [\#207](https://github.com/kevinpapst/kimai2/issues/207)
|
||||
|
||||
@@ -74,7 +188,6 @@
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- prepare release 0.3 [\#231](https://github.com/kevinpapst/kimai2/pull/231) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Easier creation of Tasks/Projects/User [\#230](https://github.com/kevinpapst/kimai2/pull/230) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added email configuration docs [\#228](https://github.com/kevinpapst/kimai2/pull/228) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Show revenue only for ROLE\_ADMIN [\#227](https://github.com/kevinpapst/kimai2/pull/227) ([kevinpapst](https://github.com/kevinpapst))
|
||||
@@ -148,7 +261,6 @@
|
||||
**Merged pull requests:**
|
||||
|
||||
- \[WIP\] Translations [\#173](https://github.com/kevinpapst/kimai2/pull/173) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- prepare v0.2 release \#177 [\#178](https://github.com/kevinpapst/kimai2/pull/178) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- 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))
|
||||
|
||||
41
Dockerfile
Normal file
41
Dockerfile
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.
|
||||
|
||||
FROM php:7.2.9-apache-stretch
|
||||
|
||||
WORKDIR /opt/kimai
|
||||
|
||||
RUN php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer && \
|
||||
apt update && \
|
||||
apt install -y \
|
||||
git \
|
||||
libicu-dev \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
libldb-dev \
|
||||
libpng-dev \
|
||||
unzip \
|
||||
zip \
|
||||
&& \
|
||||
docker-php-ext-install \
|
||||
gd \
|
||||
intl \
|
||||
ldap \
|
||||
pdo_mysql \
|
||||
zip && \
|
||||
apt-get -y autoremove && \
|
||||
apt-get clean && \
|
||||
git clone https://github.com/kevinpapst/kimai2.git /opt/kimai && \
|
||||
sed "s/prod/dev/g" .env.dist > .env && \
|
||||
composer install --dev --optimize-autoloader && \
|
||||
bin/console doctrine:database:create && \
|
||||
bin/console doctrine:schema:create && \
|
||||
bin/console doctrine:migrations:version --add --all && \
|
||||
bin/console cache:warmup && \
|
||||
chown -R www-data:www-data var
|
||||
|
||||
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.
|
||||
38
README.md
38
README.md
@@ -15,21 +15,23 @@ 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.
|
||||
|
||||
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.
|
||||
But we already support to [import your timesheets](var/docs/migration_v1.md) from Kimai v1.
|
||||
|
||||
It is developed with modern frameworks like [Symfony v4](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/),
|
||||
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json).
|
||||
|
||||
Kimai is a multi-language application and already translated to English. German, Italian, Frensh, Spanish and Russian.
|
||||
Kimai is a multi-language application and already translated to: English, German, Italian, French, Spanish, Russian, Arabic and Hungarian.
|
||||
If you want to support us in translating Kimai, please [read this documentation](var/docs/translations.md).
|
||||
|
||||
### 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 PHP extensions:
|
||||
- [PDO](https://php.net/manual/en/book.pdo.php) (with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled)
|
||||
- [intl](https://php.net/manual/en/book.intl.php)
|
||||
- [zip](https://php.net/manual/en/book.zip.php)
|
||||
- The [usual Symfony application requirements](http://symfony.com/doc/current/reference/requirements.html)
|
||||
- If you use MariaDB, make sure its at least v10.7.2 (see [FAQ](var/docs/faq.md))
|
||||
- If you use MariaDB, make sure its at least v10.2.7 (see [FAQ](var/docs/faq.md))
|
||||
- Kimai needs to be installed in the root directory of a domain or you need to [recompile the frontend assets](var/docs/developers.md)
|
||||
- A modern browser, Kimai v2 might be broken on old browsers like IE 10
|
||||
|
||||
@@ -37,13 +39,16 @@ If you want to support us in translating Kimai, please [read this documentation]
|
||||
|
||||
Looking for more information about using Kimai? Check out our more detailed [documentation](var/docs/).
|
||||
|
||||
You can see our development roadmap for the future in the [Milestones](milestones/) sections.
|
||||
You can see our development roadmap for the future in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
||||
|
||||
Our roadmap is open for changes and input from the community, please [sent us](issues/) your ideas and questions.
|
||||
Our roadmap is open for changes and input from the community, please [send us](https://github.com/kevinpapst/kimai2/issues) your ideas and questions.
|
||||
|
||||
## Installation
|
||||
|
||||
There are [further infos about installation](var/docs/installation.md) if you have to use FTP or want to develop with Kimai.
|
||||
> **NOTE**
|
||||
>
|
||||
> There are [further infos about installation](var/docs/installation.md) if you have to use FTP, want to develop with Kimai
|
||||
or are setting up Kimai on your personal computer. There is also a [docker image](var/docs/docker.md) available.
|
||||
|
||||
If you want to install Kimai 2 in your production environment, then SSH into your server and change to your webserver root.
|
||||
You need to install Git and [Composer](https://getcomposer.org/doc/00-intro.md) if you haven't already.
|
||||
@@ -55,18 +60,20 @@ git clone https://github.com/kevinpapst/kimai2.git
|
||||
cd kimai2/
|
||||
```
|
||||
|
||||
Make sure the [file permissions are correct](https://symfony.com/doc/current/setup/file_permissions.html):
|
||||
Make sure the [file permissions are correct](https://symfony.com/doc/current/setup/file_permissions.html) and create your `.env` file:
|
||||
```bash
|
||||
chown -R www-data var/
|
||||
chmod -R 777 var/
|
||||
chown -R :www-data .
|
||||
chmod -R g+r .
|
||||
chmod -R g+rw var/
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
It's up to you which database server you want to use, Kimai v2 supports MySQL/MariaDB and SQLite.
|
||||
It's up to you which database server you want to use, Kimai v2 supports MySQL/MariaDB and SQLite, but SQLite is [not recommended](var/docs/faq.md) for production usage.
|
||||
Configure the database connection string in your the `.env` file:
|
||||
```
|
||||
# adjust all settings in .env to your needs
|
||||
APP_ENV=prod
|
||||
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database
|
||||
```
|
||||
|
||||
Now install all dependencies for Kimai 2:
|
||||
@@ -114,6 +121,11 @@ Installation complete: enjoy time-tracking :-)
|
||||
|
||||
## Updating Kimai
|
||||
|
||||
> **NOTE**
|
||||
>
|
||||
> Don't forget tweaks that may be necessary to these instructions if you are using FTP, developing or updating on your
|
||||
personal computer instead of a server. See [further infos about installation](var/docs/installation.md).
|
||||
|
||||
**STOP**
|
||||
|
||||
1. It's important that you don't execute the Installation steps before or after your upgrade
|
||||
|
||||
20
UPGRADING.md
20
UPGRADING.md
@@ -5,7 +5,6 @@ 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
|
||||
@@ -19,6 +18,23 @@ 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.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,
|
||||
@@ -32,7 +48,7 @@ which leads to problems between Composer and Symfony Flex, resulting in an error
|
||||
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
|
||||
sudo -u www-data composer update symfony/flex --no-plugins --no-scripts
|
||||
```
|
||||
|
||||
Then the full update can be executed as usual:
|
||||
|
||||
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');
|
||||
@@ -11,13 +11,107 @@
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
@media print{
|
||||
.sf-toolbar, .control-sidebar {display: none !important;}
|
||||
/* ================================ CONTENT ================================ */
|
||||
|
||||
.content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import 'content';
|
||||
@import 'toolbar';
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================ NAVBAR ================================ */
|
||||
|
||||
.navbar-nav > li > a.ddt-large {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
li.messages-menu ul.menu li .pull-left i {
|
||||
color: #dd4b39;
|
||||
}
|
||||
|
||||
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.navbar-nav .start_record {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
.ticktac:hover i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
li.open .ticktac i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
.ticktac i.stopped{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ticktac:hover i.stopped{
|
||||
color: #4ff131;
|
||||
}
|
||||
.user-panel>.info {
|
||||
left: 40px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* ================================ TOOLBAR ================================ */
|
||||
|
||||
/* Page based action buttons in the upper right corner of the content area */
|
||||
.content-header > .breadcrumb {
|
||||
position: absolute;
|
||||
float: right;
|
||||
background: transparent;
|
||||
}
|
||||
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
|
||||
.control-sidebar {
|
||||
select {
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
div.image {
|
||||
img.img-circle {
|
||||
max-width: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The filter form is available on most pages above the datatable */
|
||||
@media (min-width: 768px) {
|
||||
.toolbar {
|
||||
form.navbar-form {
|
||||
font-size: $font-size-base;
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ================================ SIDEBAR ================================ */
|
||||
|
||||
/* Used for the Sidebar UserPanel (which is deactivated right now) */
|
||||
@@ -32,8 +126,40 @@ footer.main-footer {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
@import 'invoice';
|
||||
@import 'datatable';
|
||||
@import 'calendar';
|
||||
@import 'dashboard';
|
||||
@import 'navbar';
|
||||
/* ================================ 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;
|
||||
}
|
||||
|
||||
@@ -1,36 +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.
|
||||
*/
|
||||
|
||||
.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,19 +0,0 @@
|
||||
require('../../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
|
||||
|
||||
require('../css/app.scss');
|
||||
require('../css/datatable.scss');
|
||||
require('../css/calendar.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('./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,6 +29,9 @@ $(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) {
|
||||
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,8 +78,42 @@ $(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");
|
||||
});
|
||||
|
||||
$('select[data-related-select]').change(function() {
|
||||
var apiUrl = $(this).attr('data-api-url').replace('-s-', $(this).val());
|
||||
var targetSelect = $(this).attr('data-related-select');
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl,
|
||||
headers: {
|
||||
'X-AUTH-SESSION': true,
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
$('#' + targetSelect).find('option').remove().end().find('optgroup').remove().end();
|
||||
$.each(data, function(i, obj) {
|
||||
$('#' + targetSelect).append('<option value="' + obj.id + '">' + obj.name + '</option>');
|
||||
});
|
||||
$('#' + targetSelect).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();
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -98,8 +141,7 @@ $(function() {
|
||||
|
||||
// default values
|
||||
$.kimai.defaults = {
|
||||
baseUrl: '/',
|
||||
imagePath: '/images',
|
||||
locale: 'en',
|
||||
confirmDelete: 'Really delete?',
|
||||
alertSuccessAutoHide: 5000
|
||||
};
|
||||
|
||||
@@ -16,17 +16,13 @@ $(document).ready(function () {
|
||||
$('.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#project').val('');
|
||||
if ($('.toolbar form select#activity').find(':selected').attr('data-project')) {
|
||||
$('.toolbar form select#activity').val('');
|
||||
}
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
break;
|
||||
case 'project':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
} else {
|
||||
if ($('.toolbar form select#activity').find(':selected').attr('data-project')) {
|
||||
$('.toolbar form select#activity').val('');
|
||||
}
|
||||
break;
|
||||
|
||||
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';
|
||||
@@ -28,3 +28,14 @@
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&.timesheet-description {
|
||||
ul {
|
||||
padding-left: 15px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
float: right;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
|
||||
.control-sidebar {
|
||||
select {
|
||||
@@ -23,7 +24,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The filter form available on most pages above the datatable */
|
||||
.toolbar form {
|
||||
font-size: $font-size-base;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
"php": "^7.1.3",
|
||||
"ext-intl": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_sqlite": "*",
|
||||
"ext-zip": "*",
|
||||
"beberlei/DoctrineExtensions": "^1.0",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^2.3",
|
||||
"friendsofsymfony/user-bundle": "~2.0",
|
||||
"jms/serializer-bundle": "^2.4",
|
||||
"kevinpapst/adminlte-bundle": "^2.0",
|
||||
"kevinpapst/adminlte-bundle": "~2.1",
|
||||
"nelmio/api-doc-bundle": "^3.2",
|
||||
"ocramius/proxy-manager": "2.1.1",
|
||||
"phpoffice/phpspreadsheet": "^1.4",
|
||||
"phpoffice/phpword": "^0.15.0",
|
||||
"sensio/framework-extra-bundle": "^5.2",
|
||||
"symfony/asset": "^4.0",
|
||||
"symfony/console": "^4.0",
|
||||
@@ -24,7 +26,6 @@
|
||||
"symfony/flex": "^1.0",
|
||||
"symfony/form": "^4.0",
|
||||
"symfony/framework-bundle": "^4.0",
|
||||
"symfony/lts": "^4@dev",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/orm-pack": "^1.0",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
@@ -95,7 +96,8 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": true
|
||||
"allow-contrib": true,
|
||||
"require": "~4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1044
composer.lock
generated
1044
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
fos_rest:
|
||||
# disable_csrf_role: ROLE_API
|
||||
# disable_csrf_role: IS_AUTHENTICATED_ANONYMOUSLY
|
||||
# access_denied_listener:
|
||||
# enabled: false
|
||||
# service: null
|
||||
# formats:
|
||||
# name: ~
|
||||
# unauthorized_challenge: null
|
||||
# param_fetcher_listener:
|
||||
# enabled: false
|
||||
param_fetcher_listener:
|
||||
enabled: true
|
||||
# force: false
|
||||
# service: null
|
||||
# cache_dir: '%kernel.cache_dir%/fos_rest'
|
||||
cache_dir: '%kernel.cache_dir%/fos_rest'
|
||||
# allowed_methods_listener:
|
||||
# enabled: false
|
||||
# service: null
|
||||
@@ -18,8 +18,8 @@ fos_rest:
|
||||
default_format: json
|
||||
include_format: true
|
||||
# prefix_methods: true
|
||||
# body_converter:
|
||||
# enabled: false
|
||||
body_converter:
|
||||
enabled: true
|
||||
# validate: false
|
||||
# validation_errors_argument: validationErrors
|
||||
# service:
|
||||
@@ -46,8 +46,8 @@ fos_rest:
|
||||
# name: ~
|
||||
# templating_formats:
|
||||
# name: ~
|
||||
# view_response_listener:
|
||||
# enabled: true
|
||||
view_response_listener:
|
||||
enabled: true
|
||||
# force: true
|
||||
# service: null
|
||||
failed_validation: 400
|
||||
@@ -67,16 +67,16 @@ fos_rest:
|
||||
messages:
|
||||
'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
|
||||
'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': true
|
||||
# body_listener:
|
||||
# enabled: true
|
||||
body_listener:
|
||||
enabled: true
|
||||
# service: null
|
||||
# default_format: null
|
||||
# throw_exception_on_unsupported_content_type: false
|
||||
# decoders:
|
||||
# name: ~
|
||||
# array_normalizer:
|
||||
# service: null
|
||||
# forms: false
|
||||
array_normalizer:
|
||||
service: null
|
||||
forms: true
|
||||
format_listener:
|
||||
enabled: true
|
||||
# service: null
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
jms_serializer:
|
||||
handlers:
|
||||
datetime:
|
||||
default_format: "Y-m-d H:i"
|
||||
visitors:
|
||||
xml:
|
||||
format_output: '%kernel.debug%'
|
||||
|
||||
@@ -8,6 +8,9 @@ kimai:
|
||||
# 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
|
||||
@@ -30,16 +33,12 @@ 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'
|
||||
# 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/'
|
||||
|
||||
# Language specific settings, like the date formats
|
||||
languages:
|
||||
@@ -58,7 +57,7 @@ kimai:
|
||||
fr:
|
||||
date_short: 'd/m/Y'
|
||||
duration: '%%h h %%m min'
|
||||
duration_short: '%%h:%%m h'
|
||||
duration_short: '%%h h %%m'
|
||||
es:
|
||||
date_short: 'd.m.Y'
|
||||
duration: '%%h:%%m h'
|
||||
@@ -67,6 +66,14 @@ kimai:
|
||||
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'
|
||||
|
||||
# timesheet calendar configuration
|
||||
calendar:
|
||||
@@ -89,6 +96,9 @@ kimai:
|
||||
# theme related settings, will be available as twig globals at "kimai_context.*"
|
||||
# please see documentation at var/docs/theme.md
|
||||
theme:
|
||||
# BETA test: If you set this to 'selectpicker' the customer/project/activity select boxes will be transformed
|
||||
# into a searchable and javascript enhanced input type
|
||||
select_type: ~
|
||||
# display a warning color if the user has at least X active recordings
|
||||
active_warning: 3
|
||||
# fallback color for all widgets that don't have a dedicated color
|
||||
@@ -151,3 +161,10 @@ kimai:
|
||||
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
|
||||
@@ -1,16 +1,35 @@
|
||||
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}}'
|
||||
# host: '{{hostname}}'
|
||||
schemes: [http, https]
|
||||
info:
|
||||
title: Kimai 2 - API Docs
|
||||
description: REST API description for the Kimai 2 time-tracking software
|
||||
version: 0.1
|
||||
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
|
||||
|
||||
@@ -4,10 +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
|
||||
|
||||
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%'
|
||||
@@ -1,18 +1,32 @@
|
||||
App\Entity\Activity:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, comment, visible, project_id]
|
||||
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:
|
||||
getProjectId:
|
||||
name: project_id
|
||||
serialized_name: project_id
|
||||
getProject:
|
||||
serialized_name: project
|
||||
exp: "object.getProject() === null ? null : object.getProject().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
|
||||
@@ -1,35 +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]
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
App\Entity\Project:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, comment, visible, budget, orderNumber, customer_id]
|
||||
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:
|
||||
getCustomerId:
|
||||
name: customer_id
|
||||
serialized_name: customer_id
|
||||
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]
|
||||
@@ -3,9 +3,13 @@ App\Entity\User:
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
alias:
|
||||
include: true
|
||||
groups: [Default]
|
||||
title:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
avatar:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
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:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: en|de|it|fr|es|ru
|
||||
app_locales: en|de|it|fr|es|ru|ar|hu
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@@ -32,15 +32,8 @@ services:
|
||||
# APPLICATION CORE
|
||||
# ================================================================================
|
||||
|
||||
App\Invoice\ServiceInvoice:
|
||||
class: App\Invoice\ServiceInvoice
|
||||
arguments: ['%kimai.invoice%']
|
||||
|
||||
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:
|
||||
@@ -54,6 +47,14 @@ services:
|
||||
arguments:
|
||||
$dashboard: "%kimai.dashboard%"
|
||||
|
||||
App\Twig\MarkdownExtension:
|
||||
arguments:
|
||||
$timesheetAsMarkdown: "%kimai.timesheet.markdown%"
|
||||
|
||||
App\Controller\Admin\CustomerController:
|
||||
arguments:
|
||||
$defaults: "%kimai.defaults%"
|
||||
|
||||
# ================================================================================
|
||||
# DATABASE
|
||||
# ================================================================================
|
||||
@@ -77,17 +78,21 @@ 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 }
|
||||
|
||||
# ================================================================================
|
||||
# THEME
|
||||
# ================================================================================
|
||||
@@ -141,3 +146,13 @@ services:
|
||||
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%']
|
||||
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<env name="DATABASE_PREFIX" value="kimai2_" force="true"/>
|
||||
<!-- define your env variables for the test env here -->
|
||||
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak_vendors"/>
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
||||
|
||||
<!-- ###+ doctrine/doctrine-bundle ### -->
|
||||
<env name="DATABASE_URL" value="sqlite:///var/data/kimai_test.sqlite" force="true"/>
|
||||
@@ -39,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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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,8 @@
|
||||
{
|
||||
"build/app.js": "/build/app.js?a67489c4fc07f30e6f86",
|
||||
"build/app.css": "/build/app.css?e2bd74ce6119917ab0c8fd4de8d4b9c6",
|
||||
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
|
||||
"build/app.js": "/build/app.js?d823754f6c3293656052",
|
||||
"build/app.css": "/build/app.css?979d88ed847886068fcf3eb0bc3de545",
|
||||
"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",
|
||||
"build/images/glyphicons-halflings-regular.svg": "/build/images/glyphicons-halflings-regular.svg?89889688",
|
||||
@@ -13,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.png": "/build/images/blue.png?96f8a905",
|
||||
"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",
|
||||
@@ -23,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"
|
||||
}
|
||||
@@ -13,13 +13,14 @@ namespace App\API;
|
||||
|
||||
use App\Entity\Activity;
|
||||
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 Nelmio\ApiDocBundle\Annotation as API;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class ActivityController extends Controller
|
||||
class ActivityController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
@@ -51,26 +52,46 @@ class ActivityController extends Controller
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing activities",
|
||||
* @SWG\Schema(ref=@API\Model(type=Activity::class)),
|
||||
* 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")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction()
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
$query = new ActivityQuery();
|
||||
$query->setOrderGlobalsFirst(true)
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS);
|
||||
|
||||
if (null !== ($globals = $paramFetcher->get('globals'))) {
|
||||
$query->setGlobalsOnly(true);
|
||||
}
|
||||
|
||||
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=@API\Model(type=Activity::class)),
|
||||
* response=200,
|
||||
* description="Returns one activity entity",
|
||||
* @SWG\Schema(ref="#/definitions/ActivityEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
@@ -83,6 +104,7 @@ class ActivityController extends Controller
|
||||
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
|
||||
{
|
||||
}
|
||||
@@ -13,13 +13,14 @@ namespace App\API;
|
||||
|
||||
use App\Entity\Customer;
|
||||
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 Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class CustomerController extends Controller
|
||||
class CustomerController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
@@ -51,26 +52,35 @@ class CustomerController extends Controller
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing customer",
|
||||
* @SWG\Schema(ref=@Model(type=Customer::class)),
|
||||
* 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)")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction()
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
$query = new CustomerQuery();
|
||||
$query->setResultType(CustomerQuery::RESULT_TYPE_OBJECTS);
|
||||
|
||||
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=@Model(type=Customer::class)),
|
||||
* response=200,
|
||||
* description="Returns one customer entity",
|
||||
* @SWG\Schema(ref="#/definitions/CustomerEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
@@ -83,6 +93,7 @@ class CustomerController extends Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Constants;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
@@ -36,6 +37,7 @@ class HealthcheckController extends Controller
|
||||
* @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")
|
||||
@@ -46,4 +48,25 @@ class HealthcheckController extends Controller
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,14 @@ namespace App\API;
|
||||
|
||||
use App\Entity\Project;
|
||||
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 Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class ProjectController extends Controller
|
||||
class ProjectController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
@@ -51,26 +52,41 @@ class ProjectController extends Controller
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing projects",
|
||||
* @SWG\Schema(ref=@Model(type=Project::class)),
|
||||
* 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)")
|
||||
*
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction()
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
$query = new ProjectQuery();
|
||||
$query->setResultType(ProjectQuery::RESULT_TYPE_OBJECTS);
|
||||
|
||||
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=@Model(type=Project::class)),
|
||||
* response=200,
|
||||
* description="Returns one project entity",
|
||||
* @SWG\Schema(ref="#/definitions/ProjectEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
@@ -83,6 +99,7 @@ class ProjectController extends Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
191
src/API/TimesheetController.php
Normal file
191
src/API/TimesheetController.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?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")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class TimesheetController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param TimesheetRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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')")
|
||||
*
|
||||
* @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")
|
||||
* )
|
||||
*
|
||||
* @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"),
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @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,
|
||||
'method' => 'POST',
|
||||
'duration_only' => false,
|
||||
]);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,8 @@ use App\Repository\UserRepository;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
@@ -28,7 +26,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
*/
|
||||
class UserController extends Controller
|
||||
class UserController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var UserRepository
|
||||
@@ -52,9 +50,9 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all registered users",
|
||||
* @SWG\Schema(ref=@Model(type=User::class)),
|
||||
* response=200,
|
||||
* description="Returns the collection of all registered users",
|
||||
* @SWG\Schema(ref="#/definitions/UserCollection"),
|
||||
* )
|
||||
*
|
||||
* @return Response
|
||||
@@ -63,15 +61,16 @@ class UserController extends Controller
|
||||
{
|
||||
$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=@Model(type=User::class)),
|
||||
* response=200,
|
||||
* description="Return one user entity",
|
||||
* @SWG\Schema(ref="#/definitions/UserEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
@@ -84,6 +83,7 @@ class UserController extends Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'User']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ class TimesheetEntity
|
||||
$this->start = $entry->getBegin();
|
||||
$this->title = $entry->getActivity()->getName();
|
||||
$this->description = $entry->getDescription();
|
||||
$this->customer = $entry->getActivity()->getProject()->getCustomer()->getName();
|
||||
$this->project = $entry->getActivity()->getProject()->getName();
|
||||
$this->customer = $entry->getProject()->getCustomer()->getName();
|
||||
$this->project = $entry->getProject()->getName();
|
||||
$this->activity = $entry->getActivity()->getName();
|
||||
|
||||
if (null === $entry->getEnd()) {
|
||||
|
||||
@@ -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 ...');
|
||||
@@ -246,7 +263,7 @@ class KimaiImporterCommand extends Command
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importProjects($io, $projects);
|
||||
$counter = $this->importProjects($io, $projects, $fixedRates, $rates);
|
||||
$allImports += $counter;
|
||||
$io->success('Imported projects: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
@@ -256,7 +273,7 @@ class KimaiImporterCommand extends Command
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -266,7 +283,7 @@ class KimaiImporterCommand extends Command
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importTimesheetRecords($io, $records);
|
||||
$counter = $this->importTimesheetRecords($io, $records, $fixedRates, $rates);
|
||||
$allImports += $counter;
|
||||
$io->success('Imported timesheet records: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
@@ -275,10 +292,6 @@ class KimaiImporterCommand extends Command
|
||||
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,32 @@ 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"';
|
||||
$version = $this->connection->createQueryBuilder()
|
||||
->select('value')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration'))
|
||||
->where('option = :option')
|
||||
->setParameter(':option', 'version')
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
$version = $this->getImportConnection()->query($versionQuery)->fetchColumn();
|
||||
$revision = $this->getImportConnection()->query($revisionQuery)->fetchColumn();
|
||||
$revision = $this->connection->createQueryBuilder()
|
||||
->select('value')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration'))
|
||||
->where('option = :option')
|
||||
->setParameter(':option', 'revision')
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
if (1 == version_compare($requiredVersion, $version)) {
|
||||
$io->error(
|
||||
@@ -364,21 +387,16 @@ class KimaiImporterCommand extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -508,8 +526,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 +556,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 +606,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 +633,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 +694,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 +737,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 +748,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 +785,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 +829,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 +859,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 +886,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,7 +934,7 @@ class KimaiImporterCommand extends Command
|
||||
->setEnd(new \DateTime('@' . $oldRecord['end']))
|
||||
->setDuration($duration)
|
||||
->setActivity($activity)
|
||||
->setRate($rate)
|
||||
->setProject($project)
|
||||
;
|
||||
|
||||
if (!$this->validateImport($io, $timesheet)) {
|
||||
@@ -845,13 +953,20 @@ class KimaiImporterCommand extends Command
|
||||
$io->error('Reason: ' . $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.6;
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -72,7 +56,7 @@ abstract class AbstractController extends Controller
|
||||
/**
|
||||
* Adds a "warning" flash message to the stack.
|
||||
*
|
||||
* @param $translationKey
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashWarning($translationKey, $parameter = [])
|
||||
@@ -91,7 +75,7 @@ abstract class AbstractController extends Controller
|
||||
/**
|
||||
* Adds a "error" flash message to the stack.
|
||||
*
|
||||
* @param $translationKey
|
||||
* @param string $translationKey
|
||||
* @param array $parameter
|
||||
*/
|
||||
protected function flashError($translationKey, $parameter = [])
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
148
src/Controller/Admin/AboutController.php
Normal file
148
src/Controller/Admin/AboutController.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?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',
|
||||
];
|
||||
|
||||
$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,7 +13,10 @@ 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\Security;
|
||||
@@ -61,6 +64,7 @@ class ActivityController extends AbstractController
|
||||
return $this->render('admin/activity.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
@@ -104,6 +108,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();
|
||||
@@ -111,11 +131,12 @@ class ActivityController extends AbstractController
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordAmount() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($activity);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
try {
|
||||
$this->getRepository()->deleteActivity($activity, $deleteForm->get('activity')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_activity');
|
||||
}
|
||||
@@ -146,7 +167,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();
|
||||
|
||||
@@ -13,7 +13,10 @@ 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\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -28,6 +31,19 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
*/
|
||||
class CustomerController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $defaults;
|
||||
|
||||
/**
|
||||
* @param array $defaults
|
||||
*/
|
||||
public function __construct(array $defaults)
|
||||
{
|
||||
$this->defaults = $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \App\Repository\CustomerRepository
|
||||
*/
|
||||
@@ -58,6 +74,7 @@ class CustomerController extends AbstractController
|
||||
return $this->render('admin/customer.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
@@ -67,7 +84,12 @@ class CustomerController extends AbstractController
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +117,7 @@ class CustomerController extends AbstractController
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
@@ -121,6 +143,18 @@ class CustomerController extends AbstractController
|
||||
$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();
|
||||
@@ -128,11 +162,12 @@ class CustomerController extends AbstractController
|
||||
$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');
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ 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\Security;
|
||||
@@ -62,6 +65,7 @@ class ProjectController extends AbstractController
|
||||
return $this->render('admin/project.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
@@ -98,6 +102,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();
|
||||
@@ -105,11 +122,12 @@ class ProjectController extends AbstractController
|
||||
$deleteForm->handleRequest($request);
|
||||
|
||||
if (0 == $stats->getRecordAmount() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($project);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
try {
|
||||
$this->getRepository()->deleteProject($project, $deleteForm->get('project')->getData());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (ORMException $ex) {
|
||||
$this->flashError('action.delete.error');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_project');
|
||||
}
|
||||
@@ -137,7 +155,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();
|
||||
|
||||
@@ -60,6 +60,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,6 +75,7 @@ class TimesheetController extends AbstractController
|
||||
'entries' => $entries,
|
||||
'page' => $page,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
@@ -118,7 +125,7 @@ class TimesheetController extends AbstractController
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="admin_timesheet_delete", methods={"GET", "POST"})
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="admin_timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
@@ -132,9 +139,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')]);
|
||||
|
||||
@@ -77,6 +77,7 @@ class UserController extends AbstractController
|
||||
return $this->render('admin/user.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
@@ -102,7 +103,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()]);
|
||||
@@ -149,7 +150,7 @@ class UserController extends AbstractController
|
||||
$entityManager->remove($userToDelete);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.deleted_successfully');
|
||||
$this->flashSuccess('action.delete.success');
|
||||
|
||||
return $this->redirectToRoute('admin_user');
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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;
|
||||
|
||||
@@ -23,7 +22,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* @Route(path="/dashboard")
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class DashboardController extends Controller
|
||||
class DashboardController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace App\Controller;
|
||||
|
||||
use App\Constants;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -20,7 +19,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
* @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/';
|
||||
|
||||
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,6 +15,7 @@ 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;
|
||||
@@ -35,13 +36,25 @@ class InvoiceController extends AbstractController
|
||||
* @var ServiceInvoice
|
||||
*/
|
||||
protected $service;
|
||||
/**
|
||||
* @var InvoiceTemplateRepository
|
||||
*/
|
||||
protected $invoiceRepository;
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $timesheetRepository;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,16 +75,9 @@ class InvoiceController extends AbstractController
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \App\Repository\InvoiceTemplateRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->getDoctrine()->getRepository(InvoiceTemplate::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="invoice", methods={"GET", "POST"})
|
||||
* @Security("is_granted('view', 'invoice')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -79,7 +85,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');
|
||||
}
|
||||
|
||||
@@ -87,31 +93,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) {
|
||||
@@ -126,28 +204,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(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"})
|
||||
*
|
||||
* TODO permission
|
||||
* @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,
|
||||
@@ -156,10 +228,10 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/edit", name="admin_invoice_template_edit", methods={"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
|
||||
@@ -170,21 +242,60 @@ class InvoiceController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/create", name="admin_invoice_template_create", methods={"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')]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,13 +310,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', [
|
||||
@@ -214,28 +326,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
|
||||
@@ -244,7 +334,10 @@ class InvoiceController extends AbstractController
|
||||
{
|
||||
return $this->createForm(InvoiceToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('invoice', []),
|
||||
'method' => 'GET',
|
||||
'method' => 'POST',
|
||||
'attr' => [
|
||||
'id' => 'invoice-print-form'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,14 @@ class ProfileController extends AbstractController
|
||||
$this->encoder = $encoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)")
|
||||
@@ -68,9 +76,9 @@ 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);
|
||||
@@ -93,9 +101,9 @@ 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_password', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'password', null, $form);
|
||||
@@ -118,9 +126,9 @@ 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_api_token', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'api-token', null, null, null, null, $form);
|
||||
@@ -140,9 +148,9 @@ 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);
|
||||
@@ -184,9 +192,15 @@ 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()]);
|
||||
// switch locale if neccessary
|
||||
$locale = $profile->getPreferenceValue('language', $request->getLocale());
|
||||
|
||||
return $this->redirectToRoute('user_profile_preferences', [
|
||||
'_locale' => $locale,
|
||||
'username' => $profile->getUsername()
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, 'preferences', null, null, null, $form);
|
||||
|
||||
@@ -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,7 +9,6 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
@@ -19,6 +18,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
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 timesheets.
|
||||
@@ -42,6 +42,10 @@ class TimesheetController extends AbstractController
|
||||
* @Route(path="/", defaults={"page": 1}, name="timesheet", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated", methods={"GET"})
|
||||
* @Cache(smaxage="10")
|
||||
*
|
||||
* @param int $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -53,6 +57,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());
|
||||
@@ -64,12 +74,47 @@ class TimesheetController extends AbstractController
|
||||
'entries' => $entries,
|
||||
'page' => $page,
|
||||
'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"})
|
||||
*
|
||||
* @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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "main button and fly-out" for displaying (and stopping) active entries.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
@@ -99,20 +144,36 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to stop a running entry.
|
||||
* The route to re-start a timesheet entry.
|
||||
*
|
||||
* @Route(path="/start/{id}", name="timesheet_start", requirements={"id" = "\d+"}, methods={"GET", "POST"})
|
||||
* @Security("is_granted('start', activity)")
|
||||
* @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);
|
||||
$entityManager->flush();
|
||||
$this->flashSuccess('timesheet.start.success');
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('timesheet.start.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
@@ -155,7 +216,7 @@ class TimesheetController extends AbstractController
|
||||
/**
|
||||
* The route to delete an existing entry.
|
||||
*
|
||||
* @Route(path="/{id}/delete", name="timesheet_delete", methods={"GET", "POST"})
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
@@ -169,9 +230,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')]);
|
||||
|
||||
@@ -92,13 +92,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')]);
|
||||
}
|
||||
@@ -170,14 +168,12 @@ trait TimesheetControllerTrait
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ class CustomerFixtures extends Fixture
|
||||
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;
|
||||
@@ -66,6 +68,16 @@ class CustomerFixtures extends Fixture
|
||||
$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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,11 +125,11 @@ class CustomerFixtures extends Fixture
|
||||
|
||||
/**
|
||||
* @param Generator $faker
|
||||
* @param Project $project
|
||||
* @param Project|null $project
|
||||
* @param bool $visible
|
||||
* @return Activity
|
||||
*/
|
||||
private function createActivity(Generator $faker, Project $project, $visible)
|
||||
private function createActivity(Generator $faker, ?Project $project, $visible)
|
||||
{
|
||||
$entry = new Activity();
|
||||
$entry
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
@@ -28,38 +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()
|
||||
{
|
||||
$faker = Factory::create();
|
||||
|
||||
$template = new InvoiceTemplate();
|
||||
$template
|
||||
->setName('Invoice')
|
||||
->setTitle('Your company name')
|
||||
->setCompany($faker->company)
|
||||
->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(
|
||||
$faker->streetAddress . PHP_EOL .
|
||||
$faker->city . ', ' . $faker->stateAbbr . ' ' . $faker->postcode . PHP_EOL .
|
||||
'Phone: ' . $faker->phoneNumber . PHP_EOL .
|
||||
'Email: ' . $faker->safeEmail
|
||||
)
|
||||
$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,6 +10,7 @@
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
@@ -56,6 +57,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
$allUser = $this->getAllUsers($manager);
|
||||
$activities = $this->getAllActivities($manager);
|
||||
$projects = $this->getAllProjects($manager);
|
||||
|
||||
$faker = Factory::create();
|
||||
|
||||
@@ -80,6 +82,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
$description
|
||||
);
|
||||
|
||||
@@ -98,6 +101,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
null,
|
||||
false
|
||||
);
|
||||
@@ -126,6 +130,22 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Project[]
|
||||
*/
|
||||
protected function getAllProjects(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var Project[] $entries */
|
||||
$entries = $manager->getRepository(Project::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Activity[]
|
||||
@@ -133,7 +153,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
protected function getAllActivities(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var User[] $entries */
|
||||
/* @var Activity[] $entries */
|
||||
$entries = $manager->getRepository(Activity::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
@@ -142,7 +162,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
return $all;
|
||||
}
|
||||
|
||||
private function createTimesheetEntry(User $user, Activity $activity, $description, $setEndDate = true)
|
||||
private function createTimesheetEntry(User $user, Activity $activity, Project $project, $description, $setEndDate = true)
|
||||
{
|
||||
$start = new \DateTime();
|
||||
$start = $start->modify('- ' . (rand(1, self::TIMERANGE_DAYS)) . ' days');
|
||||
@@ -151,6 +171,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
->setActivity($activity)
|
||||
->setProject($activity->getProject() ?? $project)
|
||||
->setDescription($description)
|
||||
->setUser($user)
|
||||
->setBegin($start);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
@@ -36,64 +35,57 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
||||
|
||||
$container->setParameter('kimai.languages', $config['languages']);
|
||||
$container->setParameter('kimai.calendar', $config['calendar']);
|
||||
$container->setParameter('kimai.theme', $config['theme']);
|
||||
$container->setParameter('kimai.dashboard', $config['dashboard']);
|
||||
$container->setParameter('kimai.widgets', $config['widgets']);
|
||||
$container->setParameter('kimai.invoice.documents', $config['invoice']['documents']);
|
||||
$container->setParameter('kimai.defaults', $config['defaults']);
|
||||
|
||||
$this->createUserParameter($config, $container);
|
||||
$this->createTimesheetParameter($config, $container);
|
||||
$this->createInvoiceParameter($config, $container);
|
||||
$this->createThemeParameter($config['theme'], $container);
|
||||
$this->createUserParameter($config['user'], $container);
|
||||
$this->createTimesheetParameter($config['timesheet'], $container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
public function createUserParameter(array $config, ContainerBuilder $container)
|
||||
protected function createThemeParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
if (!$config['user']['registration']) {
|
||||
$container->setParameter('kimai.theme', $config);
|
||||
$container->setParameter('kimai.theme.select_type', $config['select_type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
protected function createUserParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
if (!$config['registration']) {
|
||||
$routes = $container->getParameter('admin_lte_theme.routes');
|
||||
$routes['adminlte_registration'] = null;
|
||||
$container->setParameter('admin_lte_theme.routes', $routes);
|
||||
}
|
||||
|
||||
if (!$config['user']['password_reset']) {
|
||||
if (!$config['password_reset']) {
|
||||
$routes = $container->getParameter('admin_lte_theme.routes');
|
||||
$routes['adminlte_password_reset'] = null;
|
||||
$container->setParameter('admin_lte_theme.routes', $routes);
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.fosuser', $config['user']);
|
||||
$container->setParameter('kimai.fosuser', $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
public function createTimesheetParameter(array $config, ContainerBuilder $container)
|
||||
protected function createTimesheetParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
|
||||
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
||||
$container->setParameter('kimai.timesheet.duration_only', $config['timesheet']['duration_only']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
private function createInvoiceParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
$keys = ['renderer', 'calculator', 'number_generator'];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (!isset($config['invoice'][$key]) || 0 === count($config['invoice'][$key])) {
|
||||
throw new InvalidDefinitionException('Missing invoice configuration: kimai.invoice.' . $key);
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.invoice.' . $key, $config['invoice'][$key]);
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.invoice', $config['invoice']);
|
||||
$container->setParameter('kimai.timesheet.rates', $config['rates']);
|
||||
$container->setParameter('kimai.timesheet.rounding', $config['rounding']);
|
||||
$container->setParameter('kimai.timesheet.duration_only', $config['duration_only']);
|
||||
$container->setParameter('kimai.timesheet.markdown', $config['markdown_content']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Kernel;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Dynamically adds all dependencies to the InvoiceService.
|
||||
*/
|
||||
class InvoiceServiceCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
// always first check if the primary service is defined
|
||||
if (!$container->has(ServiceInvoice::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $container->findDefinition(ServiceInvoice::class);
|
||||
|
||||
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_RENDERER);
|
||||
foreach ($taggedRenderer as $id => $tags) {
|
||||
$definition->addMethodCall('addRenderer', [new Reference($id)]);
|
||||
}
|
||||
|
||||
$taggedGenerator = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_NUMBER_GENERATOR);
|
||||
foreach ($taggedGenerator as $id => $tags) {
|
||||
$definition->addMethodCall('addNumberGenerator', [new Reference($id)]);
|
||||
}
|
||||
|
||||
$taggedCalculator = $container->findTaggedServiceIds(Kernel::TAG_INVOICE_CALCULATOR);
|
||||
foreach ($taggedCalculator as $id => $tags) {
|
||||
$definition->addMethodCall('addCalculator', [new Reference($id)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,5 +29,17 @@ class TwigContextCompilerPass implements CompilerPassInterface
|
||||
|
||||
$twig->addMethodCall('addGlobal', ['kimai_context', $theme]);
|
||||
$twig->addMethodCall('addGlobal', ['duration_only', $durationOnly]);
|
||||
|
||||
if ($container->hasDefinition('twig.loader.native_filesystem')) {
|
||||
$definition = $container->getDefinition('twig.loader.native_filesystem');
|
||||
|
||||
$path = dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR;
|
||||
foreach ($container->getParameter('kimai.invoice.documents') as $invoicePath) {
|
||||
if (!is_dir($path . $invoicePath)) {
|
||||
continue;
|
||||
}
|
||||
$definition->addMethodCall('addPath', [$path . $invoicePath, 'invoice']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class Configuration implements ConfigurationInterface
|
||||
->append($this->getThemeNode())
|
||||
->append($this->getDashboardNode())
|
||||
->append($this->getWidgetsNode())
|
||||
->append($this->getDefaultsNode())
|
||||
->end()
|
||||
->end();
|
||||
|
||||
@@ -55,6 +56,9 @@ class Configuration implements ConfigurationInterface
|
||||
->booleanNode('duration_only')
|
||||
->defaultValue(false)
|
||||
->end()
|
||||
->booleanNode('markdown_content')
|
||||
->defaultValue(false)
|
||||
->end()
|
||||
->arrayNode('rounding')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
@@ -96,6 +100,12 @@ class Configuration implements ConfigurationInterface
|
||||
->floatNode('factor')
|
||||
->isRequired()
|
||||
->defaultValue(1)
|
||||
->validate()
|
||||
->ifTrue(function ($value) {
|
||||
return $value <= 0;
|
||||
})
|
||||
->thenInvalid('A rate factor smaller or equals 0 is not allowed')
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
@@ -113,32 +123,15 @@ class Configuration implements ConfigurationInterface
|
||||
$node = $builder->root('invoice');
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->arrayNode('renderer')
|
||||
->arrayNode('documents')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->scalarPrototype()->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Controller\InvoiceController::invoiceAction',
|
||||
])
|
||||
->end()
|
||||
->arrayNode('calculator')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Invoice\DefaultCalculator',
|
||||
])
|
||||
->end()
|
||||
->arrayNode('number_generator')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Invoice\DateNumberGenerator',
|
||||
'var/invoices/',
|
||||
'templates/invoice/renderer/'
|
||||
])
|
||||
->end()
|
||||
->end()
|
||||
@@ -222,6 +215,9 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('box_color')
|
||||
->defaultValue('green')
|
||||
->end()
|
||||
->scalarNode('select_type')
|
||||
->defaultNull()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
@@ -311,4 +307,26 @@ class Configuration implements ConfigurationInterface
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getDefaultsNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('defaults');
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->arrayNode('customer')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('timezone')->defaultValue('Europe/Berlin')->end()
|
||||
->scalarNode('country')->defaultValue('DE')->end()
|
||||
->scalarNode('currency')->defaultValue('EUR')->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
namespace App\Doctrine;
|
||||
|
||||
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\Migrations\AbstractMigration as BaseAbstractMigration;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
@@ -50,7 +52,7 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws \Doctrine\DBAL\DBALException
|
||||
* @throws DBALException
|
||||
*/
|
||||
protected function getPlatform()
|
||||
{
|
||||
@@ -62,7 +64,7 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
* $schema = $this->getClassMetaData(User::class);
|
||||
*
|
||||
* @param string $entityName
|
||||
* @return \Doctrine\Common\Persistence\Mapping\ClassMetadata
|
||||
* @return ClassMetadata
|
||||
*/
|
||||
protected function getClassMetaData($entityName)
|
||||
{
|
||||
@@ -77,7 +79,7 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
*
|
||||
* @param string $indexName
|
||||
* @param string $tableName
|
||||
* @throws \Doctrine\DBAL\DBALException
|
||||
* @throws DBALException
|
||||
*/
|
||||
protected function addSqlDropIndex($indexName, $tableName)
|
||||
{
|
||||
|
||||
@@ -34,7 +34,6 @@ class Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="activities")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
|
||||
@@ -69,6 +68,30 @@ class Activity
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
@@ -97,8 +120,6 @@ class Activity
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name
|
||||
*
|
||||
* @param string $name
|
||||
* @return Activity
|
||||
*/
|
||||
@@ -110,8 +131,6 @@ class Activity
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
@@ -120,8 +139,6 @@ class Activity
|
||||
}
|
||||
|
||||
/**
|
||||
* Set comment
|
||||
*
|
||||
* @param string $comment
|
||||
* @return Activity
|
||||
*/
|
||||
@@ -133,8 +150,6 @@ class Activity
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
@@ -143,10 +158,7 @@ class Activity
|
||||
}
|
||||
|
||||
/**
|
||||
* Set visible
|
||||
*
|
||||
* @param bool $visible
|
||||
*
|
||||
* @return Activity
|
||||
*/
|
||||
public function setVisible($visible)
|
||||
@@ -157,8 +169,6 @@ class Activity
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getVisible()
|
||||
@@ -167,13 +177,41 @@ class Activity
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activity id
|
||||
*
|
||||
* @return int
|
||||
* @return float
|
||||
*/
|
||||
public function getId()
|
||||
public function getFixedRate(): ?float
|
||||
{
|
||||
return $this->id;
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $fixedRate
|
||||
* @return Activity
|
||||
*/
|
||||
public function setFixedRate(?float $fixedRate)
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getHourlyRate(): ?float
|
||||
{
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $hourlyRate
|
||||
* @return Activity
|
||||
*/
|
||||
public function setHourlyRate(?float $hourlyRate)
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -149,6 +149,22 @@ class Customer
|
||||
*/
|
||||
private $timezone;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
@@ -509,6 +525,44 @@ class Customer
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getFixedRate(): ?float
|
||||
{
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $fixedRate
|
||||
* @return Customer
|
||||
*/
|
||||
public function setFixedRate(?float $fixedRate)
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getHourlyRate(): ?float
|
||||
{
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $hourlyRate
|
||||
* @return Customer
|
||||
*/
|
||||
public function setHourlyRate(?float $hourlyRate)
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
52
src/Entity/InvoiceDocument.php
Normal file
52
src/Entity/InvoiceDocument.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
class InvoiceDocument
|
||||
{
|
||||
/**
|
||||
* @var \SplFileInfo
|
||||
*/
|
||||
private $file;
|
||||
|
||||
/**
|
||||
* @param \SplFileInfo $file
|
||||
*/
|
||||
public function __construct(\SplFileInfo $file)
|
||||
{
|
||||
$this->file = $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
$file = $this->file->getFilename();
|
||||
|
||||
return substr($file, 0, strpos($file, '.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return basename($this->getFilename());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFilename(): string
|
||||
{
|
||||
return $this->file->getRealPath();
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,9 @@ class InvoiceTemplate
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=255, nullable=false)
|
||||
* @ORM\Column(name="name", type="string", length=60, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=1, max=60)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
@@ -76,8 +77,8 @@ class InvoiceTemplate
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="vat", type="integer", length=2, nullable=true)
|
||||
* @Assert\Range(min = 0, max = 99)
|
||||
* @ORM\Column(name="vat", type="float", nullable=false)
|
||||
* @Assert\Range(min = 0.0, max = 99.99)
|
||||
*/
|
||||
private $vat = 0.00;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class Project
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="projects")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
@@ -86,8 +86,29 @@ class Project
|
||||
private $activities;
|
||||
|
||||
/**
|
||||
* Get projectid
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Timesheet", mappedBy="project")
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
@@ -104,8 +125,8 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $customer
|
||||
* @return $this
|
||||
* @param Customer $customer
|
||||
* @return Project
|
||||
*/
|
||||
public function setCustomer($customer)
|
||||
{
|
||||
@@ -115,8 +136,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name
|
||||
*
|
||||
* @param string $name
|
||||
* @return Project
|
||||
*/
|
||||
@@ -128,8 +147,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
@@ -138,8 +155,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Set comment
|
||||
*
|
||||
* @param string $comment
|
||||
* @return Project
|
||||
*/
|
||||
@@ -151,8 +166,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
@@ -161,8 +174,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Set visible
|
||||
*
|
||||
* @param bool $visible
|
||||
* @return Project
|
||||
*/
|
||||
@@ -174,8 +185,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getVisible()
|
||||
@@ -184,8 +193,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Set budget
|
||||
*
|
||||
* @param float $budget
|
||||
* @return Project
|
||||
*/
|
||||
@@ -197,8 +204,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Get budget
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getBudget()
|
||||
@@ -206,6 +211,25 @@ class Project
|
||||
return $this->budget;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @return Project
|
||||
*/
|
||||
public function setTimesheets($timesheets)
|
||||
{
|
||||
$this->timesheets = $timesheets;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getTimesheets()
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity[] $activities
|
||||
* @return Project
|
||||
@@ -226,7 +250,7 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOrderNumber(): ?string
|
||||
{
|
||||
@@ -244,6 +268,44 @@ class Project
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getFixedRate(): ?float
|
||||
{
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $fixedRate
|
||||
* @return Project
|
||||
*/
|
||||
public function setFixedRate(?float $fixedRate)
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getHourlyRate(): ?float
|
||||
{
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $hourlyRate
|
||||
* @return Project
|
||||
*/
|
||||
public function setHourlyRate(?float $hourlyRate)
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -64,7 +64,7 @@ class Timesheet
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(name="user", referencedColumnName="id", onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(name="user", referencedColumnName="id", onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
@@ -73,11 +73,20 @@ class Timesheet
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity", inversedBy="timesheets")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="timesheets")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
@@ -89,9 +98,26 @@ class Timesheet
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="rate", type="decimal", precision=10, scale=2, nullable=false)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* Get entry id
|
||||
*
|
||||
@@ -136,6 +162,8 @@ class Timesheet
|
||||
public function setEnd($end)
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
// FIXME test and then remove it, this should not be neccessary
|
||||
if (null === $end) {
|
||||
$this->duration = 0;
|
||||
}
|
||||
@@ -213,6 +241,25 @@ class Timesheet
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
*/
|
||||
public function getProject()
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setProject(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description
|
||||
*
|
||||
@@ -260,6 +307,46 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getFixedRate(): ?float
|
||||
{
|
||||
return $this->fixedRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $fixedRate
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setFixedRate(?float $fixedRate)
|
||||
{
|
||||
$this->fixedRate = $fixedRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getHourlyRate(): ?float
|
||||
{
|
||||
return $this->hourlyRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $hourlyRate
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setHourlyRate(?float $hourlyRate)
|
||||
{
|
||||
$this->hourlyRate = $hourlyRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* These validations are used in places, where we don't use a form yet (like the API).
|
||||
*
|
||||
* @param ExecutionContextInterface $context
|
||||
* @param mixed $payload
|
||||
*
|
||||
@@ -267,7 +354,58 @@ class Timesheet
|
||||
*/
|
||||
public function validate(ExecutionContextInterface $context, $payload)
|
||||
{
|
||||
if (null !== $this->getEnd() && $this->getEnd()->getTimestamp() < $this->getBegin()->getTimestamp()) {
|
||||
if (null === ($activity = $this->getActivity())) {
|
||||
$context->buildViolation('A timesheet must have an activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === ($project = $this->getProject())) {
|
||||
$context->buildViolation('A timesheet must have a project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null !== $activity && null !== $project) {
|
||||
if (null !== $activity->getProject() && $activity->getProject() !== $project) {
|
||||
$context->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($activity->getVisible() === false) {
|
||||
$context->buildViolation('Cannot start a disabled activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($project->getVisible() === false) {
|
||||
$context->buildViolation('Cannot start a disabled project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if ($project->getCustomer()->getVisible() === false) {
|
||||
$context->buildViolation('Cannot start a disabled customer.')
|
||||
->atPath('customer')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $this->getBegin() && null !== $this->getEnd()) {
|
||||
$context->buildViolation('You must submit a begin date before an end date is added.')
|
||||
->atPath('begin')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null !== $this->getBegin() && null !== $this->getEnd() && $this->getEnd()->getTimestamp() < $this->getBegin()->getTimestamp()) {
|
||||
$context->buildViolation('End date must not be earlier then start date.')
|
||||
->atPath('end')
|
||||
->setTranslationDomain('validators')
|
||||
|
||||
@@ -89,10 +89,6 @@ class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
);
|
||||
}
|
||||
|
||||
$event->addItem(
|
||||
new MenuItemModel('logout', 'menu.logout', 'fos_user_security_logout', [], 'fas fa-sign-out-alt')
|
||||
);
|
||||
|
||||
$this->activateByRoute(
|
||||
$event->getRequest()->get('_route'),
|
||||
$event->getItems()
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
@@ -21,7 +23,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
*
|
||||
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||
*/
|
||||
class RedirectToLocaleSubscriber
|
||||
class RedirectToLocaleSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var UrlGeneratorInterface
|
||||
@@ -70,6 +72,16 @@ class RedirectToLocaleSubscriber
|
||||
$this->locales = array_unique($this->locales);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => ['onKernelRequest']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GetResponseEvent $event
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
/**
|
||||
* Class ThemeOptionsSubscriber
|
||||
* Allows dynamic injection of theme related options.
|
||||
*/
|
||||
class ThemeOptionsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
@@ -33,7 +33,6 @@ class ThemeOptionsSubscriber implements EventSubscriberInterface
|
||||
protected $helper;
|
||||
|
||||
/**
|
||||
* ThemeOptionsSubscriber constructor.
|
||||
* @param TokenStorageInterface $storage
|
||||
* @param ContextHelper $helper
|
||||
*/
|
||||
|
||||
@@ -12,11 +12,14 @@ namespace App\EventSubscriber;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\UserPreferenceEvent;
|
||||
use App\Form\Type\CalendarViewType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\SkinType;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
|
||||
use Symfony\Component\HttpKernel\Event\KernelEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
@@ -69,6 +72,16 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
->setValue(0)
|
||||
->setType(IntegerType::class)
|
||||
->addConstraint(new Range(['min' => 0])),
|
||||
/*
|
||||
(new UserPreference())
|
||||
->setName('timezone')
|
||||
->setValue(date_default_timezone_get())
|
||||
->setType(TimezoneType::class),
|
||||
*/
|
||||
(new UserPreference())
|
||||
->setName('language')
|
||||
->setValue('en') // TODO fetch from services.yaml
|
||||
->setType(LanguageType::class),
|
||||
|
||||
(new UserPreference())
|
||||
->setName(UserPreference::SKIN)
|
||||
@@ -95,12 +108,10 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
->setValue(true)
|
||||
->setType(CheckboxType::class),
|
||||
|
||||
/*
|
||||
(new UserPreference())
|
||||
->setName('language')
|
||||
->setValue('de')
|
||||
->setType(LanguageType::class),
|
||||
*/
|
||||
->setName('calendar.initial_view')
|
||||
->setValue(CalendarViewType::DEFAULT_VIEW)
|
||||
->setType(CalendarViewType::class),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -34,8 +37,11 @@ class ActivityEditForm extends AbstractType
|
||||
$entry = $options['data'];
|
||||
|
||||
$project = null;
|
||||
if ($entry->getId() !== null) {
|
||||
$customer = null;
|
||||
|
||||
if (null !== $entry->getProject()) {
|
||||
$project = $entry->getProject();
|
||||
$customer = $project->getCustomer();
|
||||
}
|
||||
|
||||
$builder
|
||||
@@ -48,13 +54,33 @@ class ActivityEditForm extends AbstractType
|
||||
'label' => 'label.comment',
|
||||
'required' => false,
|
||||
])
|
||||
// entity type: project
|
||||
->add('customer', CustomerType::class, [
|
||||
'label' => 'label.customer',
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->builderForEntityType($customer);
|
||||
},
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_project',
|
||||
'data-api-url' => ['get_projects', ['customer' => '-s-']],
|
||||
],
|
||||
])
|
||||
->add('project', ProjectType::class, [
|
||||
'label' => 'label.project',
|
||||
'required' => false,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project) {
|
||||
return $repo->builderForEntityType($project);
|
||||
},
|
||||
])
|
||||
->add('fixedRate', NumberType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
])
|
||||
// boolean
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'label.visible',
|
||||
|
||||
@@ -15,6 +15,7 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -45,9 +46,6 @@ class CustomerEditForm extends AbstractType
|
||||
'label' => 'label.comment',
|
||||
'required' => false,
|
||||
])
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'label.visible',
|
||||
])
|
||||
->add('company', TextType::class, [
|
||||
'label' => 'label.company',
|
||||
'required' => false,
|
||||
@@ -89,6 +87,17 @@ class CustomerEditForm extends AbstractType
|
||||
->add('timezone', TimezoneType::class, [
|
||||
'label' => 'label.timezone',
|
||||
])
|
||||
->add('fixedRate', NumberType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
])
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'label.visible',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
65
src/Form/Extension/EnhancedChoiceTypeExtension.php
Normal file
65
src/Form/Extension/EnhancedChoiceTypeExtension.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Extension;
|
||||
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
/**
|
||||
* Converts normal select boxes into javascript enhanced versions.
|
||||
*/
|
||||
class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
{
|
||||
public const TYPE_SELECTPICKER = 'selectpicker';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $type = null;
|
||||
|
||||
/**
|
||||
* @param null|string $type
|
||||
*/
|
||||
public function __construct(?string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getExtendedType()
|
||||
{
|
||||
return EntityType::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormView $view
|
||||
* @param FormInterface $form
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
if ($this->type !== self::TYPE_SELECTPICKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($view->vars['attr'])) {
|
||||
$view->vars['attr'] = [];
|
||||
}
|
||||
|
||||
$view->vars['attr'] = array_merge(
|
||||
$view->vars['attr'],
|
||||
['class' => 'selectpicker', 'data-live-search' => true, 'data-width' => '100%']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use App\Form\Type\InvoiceCalculatorType;
|
||||
use App\Form\Type\InvoiceNumberGeneratorType;
|
||||
use App\Form\Type\InvoiceRendererType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PercentType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -51,9 +51,9 @@ class InvoiceTemplateForm extends AbstractType
|
||||
->add('dueDays', TextType::class, [
|
||||
'label' => 'label.due_days',
|
||||
])
|
||||
->add('vat', PercentType::class, [
|
||||
->add('vat', NumberType::class, [
|
||||
'label' => 'label.vat',
|
||||
'type' => 'integer',
|
||||
'scale' => 2,
|
||||
])
|
||||
->add('renderer', InvoiceRendererType::class, [])
|
||||
->add('calculator', InvoiceCalculatorType::class, [])
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Repository\CustomerRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -58,14 +59,22 @@ class ProjectEditForm extends AbstractType
|
||||
return $repo->builderForEntityType($customer);
|
||||
},
|
||||
])
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'label.visible',
|
||||
->add('fixedRate', NumberType::class, [
|
||||
'label' => 'label.fixed_rate',
|
||||
'required' => false,
|
||||
])
|
||||
->add('hourlyRate', NumberType::class, [
|
||||
'label' => 'label.hourly_rate',
|
||||
'required' => false,
|
||||
])
|
||||
->add('budget', MoneyType::class, [
|
||||
'label' => 'label.budget',
|
||||
'currency' => $customer ? $customer->getCurrency() : $builder->getOption('currency'),
|
||||
'required' => false,
|
||||
])
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'label.visible',
|
||||
])
|
||||
;
|
||||
|
||||
if ($entry->getId() === null) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user