Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc4fa0811 | ||
|
|
fb17af1afd | ||
|
|
b8811924c3 | ||
|
|
1b0b45649d | ||
|
|
e49bd05ae6 | ||
|
|
32f0d80729 | ||
|
|
f0f757cf75 | ||
|
|
ccf4e32523 | ||
|
|
56fe14c451 | ||
|
|
28801bf1ea | ||
|
|
460391136f | ||
|
|
215d4fc8bf | ||
|
|
46cb021260 | ||
|
|
f9c8028ea1 | ||
|
|
32b36f42f0 | ||
|
|
48f3ad842d | ||
|
|
3251b26d9e | ||
|
|
136df3724f | ||
|
|
0d6b8486c4 | ||
|
|
3a4aa5a001 | ||
|
|
5bea9915f5 | ||
|
|
26de367bc5 | ||
|
|
3c361c9414 | ||
|
|
0d4fdc38e4 | ||
|
|
6f7035e4cf | ||
|
|
38ae013e03 | ||
|
|
13d9f8f3f7 | ||
|
|
e990ae4800 | ||
|
|
6a68dc6e4f | ||
|
|
cfc19dca25 | ||
|
|
50318a8fc5 | ||
|
|
bde791fa55 | ||
|
|
2e6f3ed864 | ||
|
|
fb6bf4319f | ||
|
|
19bfff9899 | ||
|
|
f4d53e9006 | ||
|
|
46655ad462 | ||
|
|
1b6b957b23 | ||
|
|
0f6334314e | ||
|
|
3ac72a5c89 | ||
|
|
519be2d5a2 | ||
|
|
e8bc45f7dd | ||
|
|
76c3c5f2f6 | ||
|
|
54cd30c564 | ||
|
|
e260dd84ad | ||
|
|
bce85b04d6 | ||
|
|
642db480ce | ||
|
|
bea4be881b | ||
|
|
86832fb703 | ||
|
|
03aa4c9ddc | ||
|
|
36a158a805 | ||
|
|
32fbebd469 | ||
|
|
3d4b0c3fd6 | ||
|
|
1f7ec6769a | ||
|
|
239be527b8 | ||
|
|
fd4a87e3a6 | ||
|
|
3f535cc506 | ||
|
|
0b57bd51ca | ||
|
|
49c78ddd9d | ||
|
|
47b7046743 | ||
|
|
b4f1cae323 | ||
|
|
17856e0b96 | ||
|
|
8005679e60 | ||
|
|
ef33233624 | ||
|
|
2c6f57c7ce | ||
|
|
fd60d5bb17 | ||
|
|
742e6eb3a3 | ||
|
|
331ea586f1 | ||
|
|
afb0eabaa4 | ||
|
|
afa0aa5b3a | ||
|
|
1f99f69ecd | ||
|
|
d00596d297 | ||
|
|
8dcc18dde3 | ||
|
|
616d514d5a | ||
|
|
b2587c71b8 | ||
|
|
816866549c | ||
|
|
062735c9e3 | ||
|
|
aae9183de6 | ||
|
|
7f3e2d453f | ||
|
|
1b42aed34c | ||
|
|
974806d145 | ||
|
|
fe4c3c10d6 | ||
|
|
67dab35b57 | ||
|
|
55d8bc9ba5 | ||
|
|
572f01ed74 | ||
|
|
ee32d6f926 | ||
|
|
89abaa9dda | ||
|
|
a9ece209ae | ||
|
|
47ac50b4d0 | ||
|
|
5539f68a54 | ||
|
|
5b1ec1eb8f | ||
|
|
a609cbf84a | ||
|
|
1ad440f488 | ||
|
|
64330e6cc4 | ||
|
|
03421c099a | ||
|
|
b5c4084452 | ||
|
|
5d5b971f57 | ||
|
|
825d31af20 | ||
|
|
7880f980a8 | ||
|
|
137fd69f47 | ||
|
|
ed8efcf139 | ||
|
|
7fa3b67cc4 | ||
|
|
93c500fbf2 | ||
|
|
c58c12ea3d | ||
|
|
35305ad107 | ||
|
|
589d750e59 | ||
|
|
43b488f21f | ||
|
|
54d06462fc | ||
|
|
2d6109d587 | ||
|
|
8fddf627bf | ||
|
|
0334f6ce86 | ||
|
|
025558e58e | ||
|
|
f74547008d | ||
|
|
67dc1c7640 | ||
|
|
40322c6de4 | ||
|
|
417675b3df | ||
|
|
321ac74b6a | ||
|
|
b1c06eed7b | ||
|
|
8cb52e22b1 | ||
|
|
a073064900 | ||
|
|
07cceca67e | ||
|
|
540318fcd0 | ||
|
|
e9cbb872d0 | ||
|
|
f9dc42d1f9 | ||
|
|
1dabeedaa0 | ||
|
|
dc70ff128c | ||
|
|
b012b7170b | ||
|
|
0a1c2d8440 | ||
|
|
d72596f000 | ||
|
|
33029b2a11 | ||
|
|
bae24dd0ec | ||
|
|
db7de3aac1 | ||
|
|
c66dfbc653 | ||
|
|
51c207e025 | ||
|
|
e44e2442af | ||
|
|
86f40083ef | ||
|
|
f45fbbce60 | ||
|
|
c648866cb4 | ||
|
|
f8adafdabe | ||
|
|
ab4a1a0a1d | ||
|
|
34a3f4e275 | ||
|
|
55aedcd4a8 | ||
|
|
6633c550f5 | ||
|
|
0165658659 | ||
|
|
3075276a52 | ||
|
|
b46e752d0a | ||
|
|
ad171e7706 | ||
|
|
92c665436a | ||
|
|
2dbf433474 | ||
|
|
56ac642446 | ||
|
|
2f6c0d2208 | ||
|
|
a32614fff8 | ||
|
|
53053c127e | ||
|
|
a89ef41e81 | ||
|
|
eee7cb9bf4 | ||
|
|
98dc38ed99 | ||
|
|
7bcd6a6382 | ||
|
|
73e0a75585 | ||
|
|
71aa899f77 | ||
|
|
53f82a808b | ||
|
|
b035fc9ebd | ||
|
|
8cae5e8b3f | ||
|
|
6163f33abf | ||
|
|
a636faa4ab | ||
|
|
ee4df36735 | ||
|
|
4ede5463f9 | ||
|
|
4c277fe1b5 | ||
|
|
9f6079ad8a | ||
|
|
6fb6a16246 | ||
|
|
3af8070a57 | ||
|
|
ebe007eb78 | ||
|
|
55b7e192aa | ||
|
|
62f91f4d38 | ||
|
|
b1546c0568 | ||
|
|
af2d1c3a6e | ||
|
|
ca893b303a | ||
|
|
142369d651 | ||
|
|
3241c73e7c | ||
|
|
d6d9abd05e | ||
|
|
85ff2ff9de | ||
|
|
dbbb434723 | ||
|
|
7804ef83ce | ||
|
|
fb2d898e4c | ||
|
|
c6e99b35d5 | ||
|
|
3dcd38eca7 | ||
|
|
39ea44fdc3 | ||
|
|
2f84f3751a | ||
|
|
a7780dac9e | ||
|
|
e549a0824f | ||
|
|
8b8583d48b | ||
|
|
a8a8424ced | ||
|
|
0996eca3c5 | ||
|
|
e6f8bc8ae2 | ||
|
|
5ccc1c991b | ||
|
|
1dd5f2b72f |
@@ -2,8 +2,6 @@
|
||||
# Copy this file to .env file for development, create environment variables when deploying to production
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
|
||||
|
||||
# KIMAI DEFAULT ENV VARS
|
||||
DATABASE_PREFIX=kimai2_
|
||||
MAILER_FROM=kimai@example.com
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
@@ -27,3 +25,7 @@ DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/kimai.sqlite
|
||||
# Delivery is disabled by default via "null://localhost"
|
||||
MAILER_URL=null://localhost
|
||||
###< symfony/swiftmailer-bundle ###
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
22
.github/ISSUE_TEMPLATE.md
vendored
22
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,22 +0,0 @@
|
||||
## Description
|
||||
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
|
||||
## Possible Fix
|
||||
|
||||
|
||||
## Steps to Reproduce
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Your Environment
|
||||
* Version:
|
||||
* Browser Name and version:
|
||||
* Operating System and version (desktop or mobile):
|
||||
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
about: Create a report to help us improve Kimai
|
||||
|
||||
---
|
||||
|
||||
@@ -11,25 +11,24 @@ A clear and concise description of what the bug is.
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
**Logfile**
|
||||
```
|
||||
Add the last lines from your logfile at var/log/prod.log, around the time when the problem happened.
|
||||
```
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
**Desktop/Smartphone**
|
||||
Please complete the following information if this seems to be a frontend issue:
|
||||
- Device: [e.g. iPhone6s]
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
- Kimai version
|
||||
- PHP version
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
about: Suggest an idea to make Kimai better
|
||||
|
||||
---
|
||||
|
||||
|
||||
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,5 +1,5 @@
|
||||
## Description
|
||||
|
||||
A clear and concise description of what this pull request adds or changes.
|
||||
|
||||
## Types of changes
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
@@ -7,8 +7,7 @@
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
|
||||
## Checklist
|
||||
- [ ] My code follows the code style
|
||||
- [ ] All files have a license header
|
||||
- [ ] All methods have a doc header with type declarations
|
||||
- [ ] I have updated the documentation accordingly
|
||||
- [ ] I ran `bin/console kimai:codestyle --fix` to verify the correct code style
|
||||
- [ ] I have updated the [documentation](https://github.com/kimai/www.kimai.org/tree/master/_documentation) accordingly
|
||||
- [ ] I have added tests to cover my changes
|
||||
- [ ] I agree that this code is used in Kimai and will be published under the [MIT license](https://github.com/kevinpapst/kimai2/blob/master/LICENSE)
|
||||
|
||||
2
.github/lock.yaml → .github/lock.yml
vendored
2
.github/lock.yaml → .github/lock.yml
vendored
@@ -14,6 +14,8 @@ lockComment: >
|
||||
This thread has been automatically locked since there has not been
|
||||
any recent activity after it was closed. Please open a new issue for
|
||||
related bugs.
|
||||
If you use Kimai on a daily basis, please [consider donating](https://www.kimai.org/donate/) to
|
||||
support further development of Kimai.
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: false
|
||||
@@ -1,2 +1,4 @@
|
||||
unreleased=true
|
||||
future-release=0.3
|
||||
future-release=0.9
|
||||
exclude-labels=duplicate,question,invalid,wontfix,release
|
||||
enhancement_labels=>enhancement,Enhancement,feature request
|
||||
25
.gitignore
vendored
25
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
|
||||
# ========== KIMAI ==========
|
||||
# IDE
|
||||
.idea/
|
||||
@@ -6,12 +5,22 @@
|
||||
/bin/*
|
||||
!bin/console
|
||||
/config/packages/local.yaml
|
||||
/config/packages/*/local.yaml
|
||||
|
||||
/var/data/kimai.sqlite
|
||||
templates/invoice/renderer/.~lock*
|
||||
|
||||
/var/data/*
|
||||
!/var/data/.gitkeep
|
||||
!/var/data/kimai_test.sqlite
|
||||
/var/coverage/
|
||||
/var/cache/*
|
||||
!var/cache/.gitkeep
|
||||
|
||||
/var/invoices/*
|
||||
|
||||
/var/plugins/*
|
||||
!var/plugins/.gitkeep
|
||||
|
||||
/var/log/*
|
||||
!var/log/.gitkeep
|
||||
|
||||
@@ -31,18 +40,20 @@
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> symfony/webpack-encore-pack ###
|
||||
/node_modules/
|
||||
###< symfony/webpack-encore-pack ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
||||
###> symfony/webpack-encore-pack ###
|
||||
/node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
###< symfony/webpack-encore-pack ###
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
build:
|
||||
environment:
|
||||
node: v8.11.2
|
||||
php:
|
||||
version: 7.2
|
||||
ini:
|
||||
date.timezone: "UTC"
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
@@ -12,11 +16,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'
|
||||
command: 'bin/console kimai:codestyle --checkstyle=checkstyle'
|
||||
stop_on_failure: true
|
||||
analysis:
|
||||
file: 'checkstyle'
|
||||
format: 'php-cs-checkstyle'
|
||||
@@ -33,3 +39,8 @@ filter:
|
||||
dependency_paths:
|
||||
- 'node_modules/'
|
||||
- 'vendor/'
|
||||
|
||||
build_failure_conditions:
|
||||
- 'project.metric("scrutinizer.quality", < 9.30)'
|
||||
- 'project.metric("scrutinizer.test_coverage", < 0.9)'
|
||||
- 'project.metric_change("scrutinizer.test_coverage", < -0.01)'
|
||||
@@ -14,21 +14,25 @@ services:
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- php: 7.1
|
||||
- php: 7.2
|
||||
- php: 7.3
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer self-update
|
||||
- php -i
|
||||
|
||||
install:
|
||||
- composer install
|
||||
|
||||
script:
|
||||
- php -r "echo date(DATE_RFC850);"
|
||||
- vendor/bin/phpunit tests/
|
||||
- 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
|
||||
|
||||
523
CHANGELOG.md
523
CHANGELOG.md
@@ -1,20 +1,459 @@
|
||||
# Change Log
|
||||
|
||||
## [0.9](https://github.com/kevinpapst/kimai2/tree/0.9) (2019-04-29)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8.1...0.9)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Translation to Swedish [\#731](https://github.com/kevinpapst/kimai2/issues/731)
|
||||
- Remove active flag in user preferences form for non admin user [\#694](https://github.com/kevinpapst/kimai2/issues/694)
|
||||
- Add floor and ceil rounding [\#692](https://github.com/kevinpapst/kimai2/issues/692)
|
||||
- POST/PUT API endpoints for projects and activities [\#683](https://github.com/kevinpapst/kimai2/issues/683)
|
||||
- API is not able to handle CORS [\#646](https://github.com/kevinpapst/kimai2/issues/646)
|
||||
- Recompile the frontend assets for usage in a sub-directory [\#635](https://github.com/kevinpapst/kimai2/issues/635)
|
||||
- Remove database table prefix [\#633](https://github.com/kevinpapst/kimai2/issues/633)
|
||||
- Autofocus on first entry of forms [\#620](https://github.com/kevinpapst/kimai2/issues/620)
|
||||
- Activity admin - filter for globals [\#598](https://github.com/kevinpapst/kimai2/issues/598)
|
||||
- Non visible customer/projects etc are shown in dropdowns [\#597](https://github.com/kevinpapst/kimai2/issues/597)
|
||||
- remove duplicate confirm dialog of deletion [\#591](https://github.com/kevinpapst/kimai2/issues/591)
|
||||
- remove mobile navbar header [\#588](https://github.com/kevinpapst/kimai2/issues/588)
|
||||
- Support 12h time format [\#578](https://github.com/kevinpapst/kimai2/issues/578)
|
||||
- add timerange filter to timesheet export api [\#577](https://github.com/kevinpapst/kimai2/issues/577)
|
||||
- Extend rounding options with "to closest" mode [\#576](https://github.com/kevinpapst/kimai2/issues/576)
|
||||
- Custom CSS [\#516](https://github.com/kevinpapst/kimai2/issues/516)
|
||||
- reduce config complexity by converting php format to js format [\#734](https://github.com/kevinpapst/kimai2/pull/734) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated authorization screens [\#727](https://github.com/kevinpapst/kimai2/pull/727) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added delete timesheet API endpoint [\#726](https://github.com/kevinpapst/kimai2/pull/726) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Design updates [\#723](https://github.com/kevinpapst/kimai2/pull/723) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added api/activities POST and PATCH [\#717](https://github.com/kevinpapst/kimai2/pull/717) ([horlabs](https://github.com/horlabs))
|
||||
- allow to overwrite config via prepend [\#715](https://github.com/kevinpapst/kimai2/pull/715) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Load export type ids dynamically [\#703](https://github.com/kevinpapst/kimai2/pull/703) ([simone-gasparini](https://github.com/simone-gasparini))
|
||||
- fixing default env var for cors header [\#656](https://github.com/kevinpapst/kimai2/pull/656) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added cors bundle to allow external api access [\#655](https://github.com/kevinpapst/kimai2/pull/655) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added route to redirect locale to configured homepage [\#654](https://github.com/kevinpapst/kimai2/pull/654) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added database driven system configurations with admin screen [\#647](https://github.com/kevinpapst/kimai2/pull/647) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added new rounding mode: closest [\#611](https://github.com/kevinpapst/kimai2/pull/611) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- order datatable items by name [\#594](https://github.com/kevinpapst/kimai2/pull/594) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- UI improvements for datatables [\#593](https://github.com/kevinpapst/kimai2/pull/593) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Plugins support [\#592](https://github.com/kevinpapst/kimai2/pull/592) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- 500 \(Internal Server Error\) create customer [\#711](https://github.com/kevinpapst/kimai2/issues/711)
|
||||
- Export date parsing does not care about timezone [\#707](https://github.com/kevinpapst/kimai2/issues/707)
|
||||
- API docs are wrong for GET entity \(ignores serialization group\) [\#701](https://github.com/kevinpapst/kimai2/issues/701)
|
||||
- API date and datetime format [\#700](https://github.com/kevinpapst/kimai2/issues/700)
|
||||
- totals do not match [\#691](https://github.com/kevinpapst/kimai2/issues/691)
|
||||
- Validate email when creating user via cmd-line [\#663](https://github.com/kevinpapst/kimai2/issues/663)
|
||||
- Datepicker "From" not working [\#651](https://github.com/kevinpapst/kimai2/issues/651)
|
||||
- Error export PDF [\#648](https://github.com/kevinpapst/kimai2/issues/648)
|
||||
- Error on importing v1 timeSheets [\#645](https://github.com/kevinpapst/kimai2/issues/645)
|
||||
- Time selection dropdown not working in Firefox [\#618](https://github.com/kevinpapst/kimai2/issues/618)
|
||||
- Revoke of start\_own\_timesheet permission doesn't prevent start active entry [\#606](https://github.com/kevinpapst/kimai2/issues/606)
|
||||
- remove invalid cache header [\#722](https://github.com/kevinpapst/kimai2/pull/722) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- API: changed date-format, camelCase instead of snake\_case, null values, update and create for customer and project [\#718](https://github.com/kevinpapst/kimai2/pull/718) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix modal - autofocus problem, datepicker not shown on begin date [\#653](https://github.com/kevinpapst/kimai2/pull/653) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix pdf export cache directory [\#650](https://github.com/kevinpapst/kimai2/pull/650) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- 403 error after installation [\#725](https://github.com/kevinpapst/kimai2/issues/725)
|
||||
- AppExtension Error after bin/console cache:clear --env=prod on Line 43 [\#721](https://github.com/kevinpapst/kimai2/issues/721)
|
||||
- Kimai-v1 Import Failure [\#719](https://github.com/kevinpapst/kimai2/issues/719)
|
||||
- Review export lifecycle for plugin injection [\#702](https://github.com/kevinpapst/kimai2/issues/702)
|
||||
- Missing database-table kimai2\_configuration while trying to run import/migration script [\#698](https://github.com/kevinpapst/kimai2/issues/698)
|
||||
- Import from v1 fails if two users have the same email or blank email [\#688](https://github.com/kevinpapst/kimai2/issues/688)
|
||||
- mysql limit of max amount of activities ? [\#681](https://github.com/kevinpapst/kimai2/issues/681)
|
||||
- Filter by project broken [\#670](https://github.com/kevinpapst/kimai2/issues/670)
|
||||
- Group of user [\#669](https://github.com/kevinpapst/kimai2/issues/669)
|
||||
- Demo not loading [\#665](https://github.com/kevinpapst/kimai2/issues/665)
|
||||
- Admin Panel [\#662](https://github.com/kevinpapst/kimai2/issues/662)
|
||||
- mixed date formats in the api for the timesheet [\#658](https://github.com/kevinpapst/kimai2/issues/658)
|
||||
- Some activites don't show up in last activities [\#609](https://github.com/kevinpapst/kimai2/issues/609)
|
||||
- Wrong link in the CSS [\#605](https://github.com/kevinpapst/kimai2/issues/605)
|
||||
- Failed to import users: failed to validate user [\#600](https://github.com/kevinpapst/kimai2/issues/600)
|
||||
- Edit timesheet , delete user edit own timesheet [\#498](https://github.com/kevinpapst/kimai2/issues/498)
|
||||
- Add demo bundle [\#442](https://github.com/kevinpapst/kimai2/issues/442)
|
||||
- Add audit logs [\#62](https://github.com/kevinpapst/kimai2/issues/62)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- added svenska [\#732](https://github.com/kevinpapst/kimai2/pull/732) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- apply users timezone to toolbar queries [\#716](https://github.com/kevinpapst/kimai2/pull/716) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- remove unused role ROLE\_CUSTOMER [\#712](https://github.com/kevinpapst/kimai2/pull/712) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Update to Symfony 4.2 [\#710](https://github.com/kevinpapst/kimai2/pull/710) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix validation for create user command [\#709](https://github.com/kevinpapst/kimai2/pull/709) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- remove the total counter from all admin pages [\#708](https://github.com/kevinpapst/kimai2/pull/708) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- removed active flag in user preferences form for non admin user. \(\#694\) [\#696](https://github.com/kevinpapst/kimai2/pull/696) ([simone-gasparini](https://github.com/simone-gasparini))
|
||||
- added floor and ceil rounding with tests \(\#692\) [\#695](https://github.com/kevinpapst/kimai2/pull/695) ([simone-gasparini](https://github.com/simone-gasparini))
|
||||
- fixed activity-project assignment for kimai v1 importer [\#679](https://github.com/kevinpapst/kimai2/pull/679) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added content-type to allowed headers for api [\#672](https://github.com/kevinpapst/kimai2/pull/672) ([hmr-it-jr](https://github.com/hmr-it-jr))
|
||||
- added plugin screen [\#671](https://github.com/kevinpapst/kimai2/pull/671) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated hungarian translation [\#661](https://github.com/kevinpapst/kimai2/pull/661) ([infeeeee](https://github.com/infeeeee))
|
||||
- remove mailer url from about debug info [\#657](https://github.com/kevinpapst/kimai2/pull/657) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- delete directory with invalid case [\#643](https://github.com/kevinpapst/kimai2/pull/643) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added command to create release packages \(for FTP user\) [\#642](https://github.com/kevinpapst/kimai2/pull/642) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- removed dynamic database prefix [\#641](https://github.com/kevinpapst/kimai2/pull/641) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- set autofocus attribute on form fields for better keyboard support [\#640](https://github.com/kevinpapst/kimai2/pull/640) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- API - added begin end and export filter for timesheets [\#639](https://github.com/kevinpapst/kimai2/pull/639) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- replaced delete confirm dialog with modal [\#638](https://github.com/kevinpapst/kimai2/pull/638) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- convert-timezone: add a default value 'y' to the asked question [\#637](https://github.com/kevinpapst/kimai2/pull/637) ([blueowl04](https://github.com/blueowl04))
|
||||
- support subdirectories and subdomains for assets [\#636](https://github.com/kevinpapst/kimai2/pull/636) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- improved plugin support [\#634](https://github.com/kevinpapst/kimai2/pull/634) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix random order in recent activities [\#631](https://github.com/kevinpapst/kimai2/pull/631) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix for firefox datepicker focus problem in modals [\#630](https://github.com/kevinpapst/kimai2/pull/630) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- remove invisible entities from toolbars [\#628](https://github.com/kevinpapst/kimai2/pull/628) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- removed inline documentation [\#624](https://github.com/kevinpapst/kimai2/pull/624) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added support for Ante meridiem and Post meridiem format [\#615](https://github.com/kevinpapst/kimai2/pull/615) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added filter for global activities [\#614](https://github.com/kevinpapst/kimai2/pull/614) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improve create and start permission handling [\#613](https://github.com/kevinpapst/kimai2/pull/613) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Edit and create timesheet records in modal [\#603](https://github.com/kevinpapst/kimai2/pull/603) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code cleanup [\#602](https://github.com/kevinpapst/kimai2/pull/602) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- simplify theme settings in user profile [\#586](https://github.com/kevinpapst/kimai2/pull/586) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.8.1](https://github.com/kevinpapst/kimai2/tree/0.8.1) (2019-02-22)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8...0.8.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- resize mobile navbar buttons [\#582](https://github.com/kevinpapst/kimai2/issues/582)
|
||||
- deletion of customers leads to internal server error 500 [\#589](https://github.com/kevinpapst/kimai2/issues/589)
|
||||
- Entry editor label wrong \(from/to\) [\#587](https://github.com/kevinpapst/kimai2/issues/587)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Support foreign keys with SQLite [\#590](https://github.com/kevinpapst/kimai2/pull/590) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- post release fixes [\#584](https://github.com/kevinpapst/kimai2/pull/584) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Mobile navbar size [\#583](https://github.com/kevinpapst/kimai2/pull/583) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.8](https://github.com/kevinpapst/kimai2/tree/0.8) (2019-02-19)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.7...0.8)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Export other users' timesheets using the API [\#562](https://github.com/kevinpapst/kimai2/issues/562)
|
||||
- Some importer bugs/improvements [\#553](https://github.com/kevinpapst/kimai2/issues/553)
|
||||
- Export timesheet only exports entries on the current page [\#534](https://github.com/kevinpapst/kimai2/issues/534)
|
||||
- On the last activities dropdown show tha last ten DIFFERENT activities [\#533](https://github.com/kevinpapst/kimai2/issues/533)
|
||||
- Better user expierence on "My Times" [\#526](https://github.com/kevinpapst/kimai2/issues/526)
|
||||
- Highlight current day in datepickers and not only the selected day [\#522](https://github.com/kevinpapst/kimai2/issues/522)
|
||||
- Configuration of first page after login [\#501](https://github.com/kevinpapst/kimai2/issues/501)
|
||||
- Visual Grouping of entry from same day or same week with some stats of that group [\#495](https://github.com/kevinpapst/kimai2/issues/495)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Error 404 on 'My times' page 2+ if the time range is changed to something smaller [\#558](https://github.com/kevinpapst/kimai2/issues/558)
|
||||
- Timezone is not working correctly [\#554](https://github.com/kevinpapst/kimai2/issues/554)
|
||||
- New time picker not working on languages without updated translation [\#546](https://github.com/kevinpapst/kimai2/issues/546)
|
||||
- Export timesheet only exports entries on the current page [\#534](https://github.com/kevinpapst/kimai2/issues/534)
|
||||
- Kimai2 will not allow me to enter times in the future [\#531](https://github.com/kevinpapst/kimai2/issues/531)
|
||||
- No projects in filter section [\#525](https://github.com/kevinpapst/kimai2/issues/525)
|
||||
- This value should be greater than or equal to zero [\#511](https://github.com/kevinpapst/kimai2/issues/511)
|
||||
- Wrong time after migration kimai v1 data [\#507](https://github.com/kevinpapst/kimai2/issues/507)
|
||||
- SQL-Error for getRecentActivities\(\) caused by GROUP BY [\#488](https://github.com/kevinpapst/kimai2/issues/488)
|
||||
- fix sql for only\_full\_group\_by [\#581](https://github.com/kevinpapst/kimai2/pull/581) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Feedback for improving installation setup [\#541](https://github.com/kevinpapst/kimai2/issues/541)
|
||||
- template entry.end\_time shows empty field [\#535](https://github.com/kevinpapst/kimai2/issues/535)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- handle deleted user during import from v1 [\#569](https://github.com/kevinpapst/kimai2/pull/569) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix pagination in combination with daterange picker [\#568](https://github.com/kevinpapst/kimai2/pull/568) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow to query other users timesheets via api [\#563](https://github.com/kevinpapst/kimai2/pull/563) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated HTML invoice templates [\#560](https://github.com/kevinpapst/kimai2/pull/560) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated composer packages [\#559](https://github.com/kevinpapst/kimai2/pull/559) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix timezone problems in timesheet forms [\#555](https://github.com/kevinpapst/kimai2/pull/555) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Daily stats in timesheet [\#552](https://github.com/kevinpapst/kimai2/pull/552) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added more php requirements to documentation [\#551](https://github.com/kevinpapst/kimai2/pull/551) ([infeeeee](https://github.com/infeeeee))
|
||||
- improve recent activities [\#550](https://github.com/kevinpapst/kimai2/pull/550) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- scss fixes: year selector width, dropdown menu width, navbar refactoring [\#549](https://github.com/kevinpapst/kimai2/pull/549) ([infeeeee](https://github.com/infeeeee))
|
||||
- improved installation docs [\#548](https://github.com/kevinpapst/kimai2/pull/548) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix daterange-picker for fr, hu and ar [\#547](https://github.com/kevinpapst/kimai2/pull/547) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- do not limit users timesheet export page size [\#545](https://github.com/kevinpapst/kimai2/pull/545) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Use Symfony formatter for currency symbol placement [\#542](https://github.com/kevinpapst/kimai2/pull/542) ([sanjitlpatel](https://github.com/sanjitlpatel))
|
||||
- Localized date-inputs and daterange-picker [\#540](https://github.com/kevinpapst/kimai2/pull/540) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added export module [\#538](https://github.com/kevinpapst/kimai2/pull/538) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated documentation [\#536](https://github.com/kevinpapst/kimai2/pull/536) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- support remote data in beta-test selectpicker [\#529](https://github.com/kevinpapst/kimai2/pull/529) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improve allowed running records [\#528](https://github.com/kevinpapst/kimai2/pull/528) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improve daterangepicker [\#527](https://github.com/kevinpapst/kimai2/pull/527) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable view after login [\#523](https://github.com/kevinpapst/kimai2/pull/523) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- convert timesheets to UTC with support for user timezone [\#372](https://github.com/kevinpapst/kimai2/pull/372) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.7](https://github.com/kevinpapst/kimai2/tree/0.7) (2019-01-28)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6.1...0.7)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Editing from calendar view will return to my times table instead of calendar [\#515](https://github.com/kevinpapst/kimai2/issues/515)
|
||||
- Timesheet Export for Admins [\#503](https://github.com/kevinpapst/kimai2/issues/503)
|
||||
- Customer List Not Alphabetic [\#499](https://github.com/kevinpapst/kimai2/issues/499)
|
||||
- sorting of ${entry.X} values [\#487](https://github.com/kevinpapst/kimai2/issues/487)
|
||||
- ${entry.description} needed in Word-Docx [\#485](https://github.com/kevinpapst/kimai2/issues/485)
|
||||
- Automatic sort for activities, customers, etc. [\#477](https://github.com/kevinpapst/kimai2/issues/477)
|
||||
- User title in the timesheet invoice [\#461](https://github.com/kevinpapst/kimai2/issues/461)
|
||||
- global variables for reports/invoices [\#438](https://github.com/kevinpapst/kimai2/issues/438)
|
||||
- Configuration option: Only one active record for each user [\#427](https://github.com/kevinpapst/kimai2/issues/427)
|
||||
- User creating activity and projects [\#393](https://github.com/kevinpapst/kimai2/issues/393)
|
||||
- Configuration option to disable fixed rate and hourly rate from "edit timesheet" [\#330](https://github.com/kevinpapst/kimai2/issues/330)
|
||||
- Set other users hourly rate [\#303](https://github.com/kevinpapst/kimai2/issues/303)
|
||||
- Feature request - Make "Rate" hideable [\#217](https://github.com/kevinpapst/kimai2/issues/217)
|
||||
- fixed null project for advanced invoice calculator [\#462](https://github.com/kevinpapst/kimai2/pull/462) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Timesheet Export for Admins [\#503](https://github.com/kevinpapst/kimai2/issues/503)
|
||||
- admin activity: visibility "none" \(no filter\) causes sql-error [\#491](https://github.com/kevinpapst/kimai2/issues/491)
|
||||
- login-screen optimizations [\#483](https://github.com/kevinpapst/kimai2/issues/483)
|
||||
- Configuration of roles to add/edit customers, projects, activities... [\#479](https://github.com/kevinpapst/kimai2/issues/479)
|
||||
- Line breaks for address and payment information fields [\#464](https://github.com/kevinpapst/kimai2/issues/464)
|
||||
- Possible to use a decimal in hourly rate field? [\#458](https://github.com/kevinpapst/kimai2/issues/458)
|
||||
- Invoice Number Generator possibly not compatible [\#454](https://github.com/kevinpapst/kimai2/issues/454)
|
||||
- users can change their role to system-admin [\#440](https://github.com/kevinpapst/kimai2/issues/440)
|
||||
- fix wrong include filename in user registration [\#520](https://github.com/kevinpapst/kimai2/pull/520) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix segmentation fault - rollback composer dependencies [\#463](https://github.com/kevinpapst/kimai2/pull/463) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed null project for advanced invoice calculator [\#462](https://github.com/kevinpapst/kimai2/pull/462) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix the restart timesheet button [\#436](https://github.com/kevinpapst/kimai2/pull/436) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Redirecting when using Kimai on a subdirectory + reverse proxy [\#492](https://github.com/kevinpapst/kimai2/issues/492)
|
||||
- Installation \(Cannot declare.... in use\) [\#455](https://github.com/kevinpapst/kimai2/issues/455)
|
||||
- New Number Generator not recognized [\#453](https://github.com/kevinpapst/kimai2/issues/453)
|
||||
- de/help/invoices returns 404 error [\#452](https://github.com/kevinpapst/kimai2/issues/452)
|
||||
- Can you Add Brazilian Portuguese Translation? I can help it.. [\#444](https://github.com/kevinpapst/kimai2/issues/444)
|
||||
- replace all selects with smart-selects and live-search [\#441](https://github.com/kevinpapst/kimai2/issues/441)
|
||||
- use parsedown-extra for rendering markdown [\#388](https://github.com/kevinpapst/kimai2/issues/388)
|
||||
- Think about a cooler name [\#133](https://github.com/kevinpapst/kimai2/issues/133)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- pagination without reload while keeping filters applied [\#521](https://github.com/kevinpapst/kimai2/pull/521) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- go back to calendar after editing and creation of time-records [\#519](https://github.com/kevinpapst/kimai2/pull/519) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fetch toolbar results without page reload [\#518](https://github.com/kevinpapst/kimai2/pull/518) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Form and theme improvements [\#513](https://github.com/kevinpapst/kimai2/pull/513) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- validation for future and negative times [\#512](https://github.com/kevinpapst/kimai2/pull/512) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- alphabetical order for selectboxes [\#510](https://github.com/kevinpapst/kimai2/pull/510) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Export team timesheets [\#508](https://github.com/kevinpapst/kimai2/pull/508) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix broken sql in activity admin [\#506](https://github.com/kevinpapst/kimai2/pull/506) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- display invoice entries in ascending order [\#505](https://github.com/kevinpapst/kimai2/pull/505) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improve DOCX template row [\#504](https://github.com/kevinpapst/kimai2/pull/504) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- do not display admin menu if it has no children [\#500](https://github.com/kevinpapst/kimai2/pull/500) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- login-screen optimizations [\#493](https://github.com/kevinpapst/kimai2/pull/493) ([lduer](https://github.com/lduer))
|
||||
- preg\_replace for md-file-extensions when rendering correct link… [\#482](https://github.com/kevinpapst/kimai2/pull/482) ([lduer](https://github.com/lduer))
|
||||
- Better composer install in the docker [\#481](https://github.com/kevinpapst/kimai2/pull/481) ([tobybatch](https://github.com/tobybatch))
|
||||
- Update dockerfile for https in composer installer [\#478](https://github.com/kevinpapst/kimai2/pull/478) ([scolson](https://github.com/scolson))
|
||||
- support line breaks in docx [\#466](https://github.com/kevinpapst/kimai2/pull/466) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated README and docu [\#465](https://github.com/kevinpapst/kimai2/pull/465) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow decimals in users hourly rate [\#460](https://github.com/kevinpapst/kimai2/pull/460) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated all composer packages [\#459](https://github.com/kevinpapst/kimai2/pull/459) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- moved some packages to dev requirements [\#456](https://github.com/kevinpapst/kimai2/pull/456) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added portuguese translations [\#446](https://github.com/kevinpapst/kimai2/pull/446) ([marquesmatheus](https://github.com/marquesmatheus))
|
||||
- Simplify timesheet edit form if only one customer is existing [\#443](https://github.com/kevinpapst/kimai2/pull/443) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added project variables for invoice templates [\#439](https://github.com/kevinpapst/kimai2/pull/439) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable permission system [\#424](https://github.com/kevinpapst/kimai2/pull/424) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Only one running timesheet: automatically stop others [\#386](https://github.com/kevinpapst/kimai2/pull/386) ([lduer](https://github.com/lduer))
|
||||
|
||||
## [0.6.1](https://github.com/kevinpapst/kimai2/tree/0.6.1) (2018-11-19)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6...0.6.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Error importing v1 Database [\#428](https://github.com/kevinpapst/kimai2/issues/428)
|
||||
- Redo button missing? [\#426](https://github.com/kevinpapst/kimai2/issues/426)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Error importing v1 Database \(could not convert to string\) [\#431](https://github.com/kevinpapst/kimai2/issues/431)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- fix migration for MySQL [\#430](https://github.com/kevinpapst/kimai2/pull/430) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix importer for MySQL [\#429](https://github.com/kevinpapst/kimai2/pull/429) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.6](https://github.com/kevinpapst/kimai2/tree/0.6) (2018-11-18)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.5...0.6)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Language setting does not work immediately [\#418](https://github.com/kevinpapst/kimai2/issues/418)
|
||||
- Merge projects and/or activities [\#406](https://github.com/kevinpapst/kimai2/issues/406)
|
||||
- Global activities support for Kimai 1 importer [\#400](https://github.com/kevinpapst/kimai2/issues/400)
|
||||
- Invoice Grouped by Activity [\#379](https://github.com/kevinpapst/kimai2/issues/379)
|
||||
- Set the order of columns \(in my times page\) [\#371](https://github.com/kevinpapst/kimai2/issues/371)
|
||||
- Columns on small/narrow displays \(mobile\) on „my times“ page [\#370](https://github.com/kevinpapst/kimai2/issues/370)
|
||||
- Make settings more accessible [\#365](https://github.com/kevinpapst/kimai2/issues/365)
|
||||
- Data validation for register page username field. [\#360](https://github.com/kevinpapst/kimai2/issues/360)
|
||||
- Option for setting default calendar view [\#359](https://github.com/kevinpapst/kimai2/issues/359)
|
||||
- Activities just Duration [\#334](https://github.com/kevinpapst/kimai2/issues/334)
|
||||
- API: Add filtering/sorting/limits [\#333](https://github.com/kevinpapst/kimai2/issues/333)
|
||||
- API: Timesheets [\#315](https://github.com/kevinpapst/kimai2/issues/315)
|
||||
- Feature request: Quick search box for activities when creating new timesheet [\#307](https://github.com/kevinpapst/kimai2/issues/307)
|
||||
- Separate Customer / Project / Activity input when logging time [\#250](https://github.com/kevinpapst/kimai2/issues/250)
|
||||
- Interactive help after fresh installation [\#94](https://github.com/kevinpapst/kimai2/issues/94)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Invoicing export - 500: Internal Server Error [\#390](https://github.com/kevinpapst/kimai2/issues/390)
|
||||
- invalid CSRF-Token [\#344](https://github.com/kevinpapst/kimai2/issues/344)
|
||||
- fix column visibility if unconfigured \(no cookie existing\) [\#423](https://github.com/kevinpapst/kimai2/pull/423) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix duration and rate for html invoices [\#414](https://github.com/kevinpapst/kimai2/pull/414) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed broken toolbar form with empty date [\#391](https://github.com/kevinpapst/kimai2/pull/391) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- After installation no graphic/background/icons shown [\#417](https://github.com/kevinpapst/kimai2/issues/417)
|
||||
- Form fields should be always visible [\#408](https://github.com/kevinpapst/kimai2/issues/408)
|
||||
- Active filter should stay open [\#405](https://github.com/kevinpapst/kimai2/issues/405)
|
||||
- Make project field optional [\#401](https://github.com/kevinpapst/kimai2/issues/401)
|
||||
- Unrecognized Options "renderer, number\_generator" under "kimai.invoice" [\#381](https://github.com/kevinpapst/kimai2/issues/381)
|
||||
- 500: Internal Server Error after login \(new install\) [\#342](https://github.com/kevinpapst/kimai2/issues/342)
|
||||
- Invoices export error [\#341](https://github.com/kevinpapst/kimai2/issues/341)
|
||||
- add the same activity for multiple projects [\#325](https://github.com/kevinpapst/kimai2/issues/325)
|
||||
- API: Endpoint for API docs needs session cookie [\#319](https://github.com/kevinpapst/kimai2/issues/319)
|
||||
- docker image for kimai2 [\#284](https://github.com/kevinpapst/kimai2/issues/284)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- redirect to user language after profile update [\#421](https://github.com/kevinpapst/kimai2/pull/421) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added more timesheet related invoice fields [\#411](https://github.com/kevinpapst/kimai2/pull/411) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- toolbar improvements [\#410](https://github.com/kevinpapst/kimai2/pull/410) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- move entries from one entity to another upon deletion [\#409](https://github.com/kevinpapst/kimai2/pull/409) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- show toolbar filter if they were submitted [\#407](https://github.com/kevinpapst/kimai2/pull/407) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- full configurable data columns in all screen sizes [\#404](https://github.com/kevinpapst/kimai2/pull/404) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added global activity support in importer [\#402](https://github.com/kevinpapst/kimai2/pull/402) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added parsedown extension for header ids \(\#388\) [\#399](https://github.com/kevinpapst/kimai2/pull/399) ([lduer](https://github.com/lduer))
|
||||
- Fix for \#397 [\#398](https://github.com/kevinpapst/kimai2/pull/398) ([tobybatch](https://github.com/tobybatch))
|
||||
- Added links to docker hub and my docker repo [\#396](https://github.com/kevinpapst/kimai2/pull/396) ([tobybatch](https://github.com/tobybatch))
|
||||
- fixed issues- & milestone links [\#395](https://github.com/kevinpapst/kimai2/pull/395) ([lduer](https://github.com/lduer))
|
||||
- added links to further docker images by the community [\#394](https://github.com/kevinpapst/kimai2/pull/394) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- beta test: added quick search box for customers, projects and activities [\#392](https://github.com/kevinpapst/kimai2/pull/392) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated the documentation, reflecting the changes in \#306 [\#387](https://github.com/kevinpapst/kimai2/pull/387) ([lduer](https://github.com/lduer))
|
||||
- Added Hungarian translation and fixed the path of kimai.yaml in translation documentation [\#385](https://github.com/kevinpapst/kimai2/pull/385) ([infeeeee](https://github.com/infeeeee))
|
||||
- improved docs and cross-linked docker [\#383](https://github.com/kevinpapst/kimai2/pull/383) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Doc update of install/update instructions re: username [\#382](https://github.com/kevinpapst/kimai2/pull/382) ([srdco](https://github.com/srdco))
|
||||
- invoice calculator to group entries by activity [\#380](https://github.com/kevinpapst/kimai2/pull/380) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- basic about screen with server and debug infos [\#378](https://github.com/kevinpapst/kimai2/pull/378) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Dev/demo dockerfile [\#377](https://github.com/kevinpapst/kimai2/pull/377) ([tobybatch](https://github.com/tobybatch))
|
||||
- Sidebar - user-profile links and language settings [\#369](https://github.com/kevinpapst/kimai2/pull/369) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable initial calendar view [\#363](https://github.com/kevinpapst/kimai2/pull/363) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- decrease minimum username to 3 character [\#362](https://github.com/kevinpapst/kimai2/pull/362) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Default country \(\#2\) [\#358](https://github.com/kevinpapst/kimai2/pull/358) ([tobybatch](https://github.com/tobybatch))
|
||||
- composer: removed deprecated symfony/lt, updated others [\#354](https://github.com/kevinpapst/kimai2/pull/354) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated admin-lte theme bundle to 2.1.1 [\#350](https://github.com/kevinpapst/kimai2/pull/350) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated timesheet and email documentation [\#349](https://github.com/kevinpapst/kimai2/pull/349) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated required dependencies [\#346](https://github.com/kevinpapst/kimai2/pull/346) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fix password reset [\#345](https://github.com/kevinpapst/kimai2/pull/345) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added arabic translations and locale support in datepicker [\#337](https://github.com/kevinpapst/kimai2/pull/337) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- API endpoint for Timesheet entries [\#332](https://github.com/kevinpapst/kimai2/pull/332) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Support global activities [\#259](https://github.com/kevinpapst/kimai2/pull/259) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.5](https://github.com/kevinpapst/kimai2/tree/0.5) (2018-09-27)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.4...0.5)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- API: Kimai metadata endpoint [\#320](https://github.com/kevinpapst/kimai2/issues/320)
|
||||
- Support Markdown in timesheet description [\#295](https://github.com/kevinpapst/kimai2/issues/295)
|
||||
- Add support for MS Office templates [\#283](https://github.com/kevinpapst/kimai2/issues/283)
|
||||
- Adding rates to customers, projects and tasks [\#271](https://github.com/kevinpapst/kimai2/issues/271)
|
||||
- Delete invoice templates [\#267](https://github.com/kevinpapst/kimai2/issues/267)
|
||||
- Sum of hours in timesheet invoice [\#262](https://github.com/kevinpapst/kimai2/issues/262)
|
||||
- Feature request - API [\#251](https://github.com/kevinpapst/kimai2/issues/251)
|
||||
- Feature request - PDF Download for monthly report [\#244](https://github.com/kevinpapst/kimai2/issues/244)
|
||||
- Add support for OpenOffice templates [\#223](https://github.com/kevinpapst/kimai2/issues/223)
|
||||
- Feature Request - Excel Output of times [\#221](https://github.com/kevinpapst/kimai2/issues/221)
|
||||
- Add hourly rate options [\#111](https://github.com/kevinpapst/kimai2/issues/111)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Excel Output not containing data [\#327](https://github.com/kevinpapst/kimai2/issues/327)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Creating database schema throws exceptions [\#322](https://github.com/kevinpapst/kimai2/issues/322)
|
||||
- Add permission voter for invoices and templates [\#293](https://github.com/kevinpapst/kimai2/issues/293)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- added copy invoice template action [\#331](https://github.com/kevinpapst/kimai2/pull/331) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fix spreadsheet renderer for invoices with one entry [\#328](https://github.com/kevinpapst/kimai2/pull/328) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- invoice VAT as float and database key length [\#323](https://github.com/kevinpapst/kimai2/pull/323) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added version command + api endpoint [\#321](https://github.com/kevinpapst/kimai2/pull/321) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Timesheet export [\#317](https://github.com/kevinpapst/kimai2/pull/317) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added invoice voter [\#316](https://github.com/kevinpapst/kimai2/pull/316) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Refactored invoice system [\#306](https://github.com/kevinpapst/kimai2/pull/306) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- add stable controller tests [\#305](https://github.com/kevinpapst/kimai2/pull/305) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fixed rate and Hourly rates [\#304](https://github.com/kevinpapst/kimai2/pull/304) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- add hourly\_rate and fixed\_rate to timesheet entries [\#302](https://github.com/kevinpapst/kimai2/pull/302) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- allow to switch language on any page [\#299](https://github.com/kevinpapst/kimai2/pull/299) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Fix duration\_short format for french translation [\#297](https://github.com/kevinpapst/kimai2/pull/297) ([jeau](https://github.com/jeau))
|
||||
- allow markdown in timesheet descriptions [\#296](https://github.com/kevinpapst/kimai2/pull/296) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added option to delete invoice template [\#294](https://github.com/kevinpapst/kimai2/pull/294) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved invoice preview [\#254](https://github.com/kevinpapst/kimai2/pull/254) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.4](https://github.com/kevinpapst/kimai2/tree/0.4) (2018-09-01)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.3...0.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Delete user [\#225](https://github.com/kevinpapst/kimai2/issues/225)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Don't allow to stop an already stopped entry [\#282](https://github.com/kevinpapst/kimai2/issues/282)
|
||||
- installed it, and then what??? [\#255](https://github.com/kevinpapst/kimai2/issues/255)
|
||||
- fixed calendar for empty timesheet descriptions [\#265](https://github.com/kevinpapst/kimai2/pull/265) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixed .htaccess for apache users [\#260](https://github.com/kevinpapst/kimai2/pull/260) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Spanish Translation Contrib [\#248](https://github.com/kevinpapst/kimai2/issues/248)
|
||||
- Administration of projects throws error if locale is "de" [\#247](https://github.com/kevinpapst/kimai2/issues/247)
|
||||
- Web Installer Package for near-automated installs from browser [\#235](https://github.com/kevinpapst/kimai2/issues/235)
|
||||
- Improve dashboard display [\#280](https://github.com/kevinpapst/kimai2/issues/280)
|
||||
- Feature request - Monthly overview / report [\#207](https://github.com/kevinpapst/kimai2/issues/207)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- fixed money display in dashboard widgets [\#291](https://github.com/kevinpapst/kimai2/pull/291) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Symfony update 4.1.4 [\#290](https://github.com/kevinpapst/kimai2/pull/290) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- do not allow to stop already stopped timesheets [\#289](https://github.com/kevinpapst/kimai2/pull/289) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added configurable formats for duration [\#287](https://github.com/kevinpapst/kimai2/pull/287) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated russian translations [\#286](https://github.com/kevinpapst/kimai2/pull/286) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Update french translation [\#279](https://github.com/kevinpapst/kimai2/pull/279) ([jeau](https://github.com/jeau))
|
||||
- Fix standard date format in french [\#278](https://github.com/kevinpapst/kimai2/pull/278) ([jeau](https://github.com/jeau))
|
||||
- new translations: spanish, french and italian [\#276](https://github.com/kevinpapst/kimai2/pull/276) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- French translation for Kimai [\#275](https://github.com/kevinpapst/kimai2/pull/275) ([jeau](https://github.com/jeau))
|
||||
- updated to AdminLTE 2.4.8 [\#274](https://github.com/kevinpapst/kimai2/pull/274) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- fixing migration\_v1.md url [\#270](https://github.com/kevinpapst/kimai2/pull/270) ([ltsavar](https://github.com/ltsavar))
|
||||
- dashboard widgets are configurable via config [\#269](https://github.com/kevinpapst/kimai2/pull/269) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Spanish translation for Kimai [\#266](https://github.com/kevinpapst/kimai2/pull/266) ([cobaltos](https://github.com/cobaltos))
|
||||
- moved theme settings to dynamic compiler pass [\#264](https://github.com/kevinpapst/kimai2/pull/264) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added config setting to disable password-reset and user-registration [\#261](https://github.com/kevinpapst/kimai2/pull/261) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added basic API endpoints [\#258](https://github.com/kevinpapst/kimai2/pull/258) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Hotfix for Symfony Flex incompatibility with latest composer changes [\#257](https://github.com/kevinpapst/kimai2/pull/257) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added italian translation [\#253](https://github.com/kevinpapst/kimai2/pull/253) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Delete user [\#249](https://github.com/kevinpapst/kimai2/pull/249) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- dashboard widgets from event [\#246](https://github.com/kevinpapst/kimai2/pull/246) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Added timesheet-calendar view [\#236](https://github.com/kevinpapst/kimai2/pull/236) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved data fixtures [\#234](https://github.com/kevinpapst/kimai2/pull/234) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.3](https://github.com/kevinpapst/kimai2/tree/0.3) (2018-07-22)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.2...0.3)
|
||||
|
||||
**Update from 0.2:**
|
||||
**Implemented enhancements:**
|
||||
|
||||
You need to adjust your `.env` file and add your `from` address for [all emails](https://github.com/kevinpapst/kimai2/blob/master/var/docs/emails.md) generated by Kimai 2:
|
||||
```
|
||||
MAILER_FROM=kimai@example.com
|
||||
```
|
||||
|
||||
Users upgrading from 0.2 need to execute:
|
||||
```
|
||||
bin/console doctrine:migrations:version --add 20180701120000
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
- Install Kimai as WebApp [\#203](https://github.com/kevinpapst/kimai2/issues/203)
|
||||
- Replace AvanzuAdminTheme with AdminLTE bundle [\#201](https://github.com/kevinpapst/kimai2/issues/201)
|
||||
- Add dynamic column filter [\#174](https://github.com/kevinpapst/kimai2/issues/174)
|
||||
- Add register user function [\#164](https://github.com/kevinpapst/kimai2/issues/164)
|
||||
- Add forgot password function - login screen [\#163](https://github.com/kevinpapst/kimai2/issues/163)
|
||||
- Integrate FOSUserBundle \(for registration, password-reset, etc.\) [\#144](https://github.com/kevinpapst/kimai2/issues/144)
|
||||
- added dynamic column filter \#174 [\#184](https://github.com/kevinpapst/kimai2/pull/184) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
@@ -25,20 +464,14 @@ bin/console doctrine:migrations:migrate
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Verifying email configuration [\#226](https://github.com/kevinpapst/kimai2/issues/226)
|
||||
- Installation Issue of version1.3.1 [\#208](https://github.com/kevinpapst/kimai2/issues/208)
|
||||
- Error on bin/console doctrine:schema:create [\#191](https://github.com/kevinpapst/kimai2/issues/191)
|
||||
- Verifying email configuration [\#226](https://github.com/kevinpapst/kimai2/issues/226)
|
||||
- Add favicon [\#205](https://github.com/kevinpapst/kimai2/issues/205)
|
||||
- Install Kimai as WebApp [\#203](https://github.com/kevinpapst/kimai2/issues/203)
|
||||
- Replace AvanzuAdminTheme with AdminLTE bundle [\#201](https://github.com/kevinpapst/kimai2/issues/201)
|
||||
- Document DB requirements in install docu [\#196](https://github.com/kevinpapst/kimai2/issues/196)
|
||||
- easier creation of tasks [\#195](https://github.com/kevinpapst/kimai2/issues/195)
|
||||
- Error on bin/console doctrine:schema:create [\#191](https://github.com/kevinpapst/kimai2/issues/191)
|
||||
- Integrate lock bot [\#183](https://github.com/kevinpapst/kimai2/issues/183)
|
||||
- Upgrade to FontAwesome 5 [\#179](https://github.com/kevinpapst/kimai2/issues/179)
|
||||
- Add dynamic column filter [\#174](https://github.com/kevinpapst/kimai2/issues/174)
|
||||
- Add register user function [\#164](https://github.com/kevinpapst/kimai2/issues/164)
|
||||
- Add forgot password function - login screen [\#163](https://github.com/kevinpapst/kimai2/issues/163)
|
||||
- Integrate FOSUserBundle \(for registration, password-reset, etc.\) [\#144](https://github.com/kevinpapst/kimai2/issues/144)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
@@ -57,42 +490,24 @@ bin/console doctrine:migrations:migrate
|
||||
- improved install docs and composer [\#187](https://github.com/kevinpapst/kimai2/pull/187) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added lock-bot config \#183 [\#186](https://github.com/kevinpapst/kimai2/pull/186) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Composer package name [\#185](https://github.com/kevinpapst/kimai2/pull/185) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added dynamic column filter \#174 [\#184](https://github.com/kevinpapst/kimai2/pull/184) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- updated to font-awesome 5 \#179 [\#181](https://github.com/kevinpapst/kimai2/pull/181) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- language specific money and date display [\#180](https://github.com/kevinpapst/kimai2/pull/180) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.2](https://github.com/kevinpapst/kimai2/tree/0.2) (2018-06-23)
|
||||
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.1...0.2)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Change to users language after login [\#7](https://github.com/kevinpapst/kimai2/issues/7)
|
||||
- Prevent end time before start time [\#152](https://github.com/kevinpapst/kimai2/issues/152)
|
||||
- Importing old data fails [\#145](https://github.com/kevinpapst/kimai2/issues/145)
|
||||
- Translate LanguageType choices [\#83](https://github.com/kevinpapst/kimai2/issues/83)
|
||||
- Mobile alignments and toolbar \#151 [\#157](https://github.com/kevinpapst/kimai2/pull/157) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved mobile view \#151 [\#150](https://github.com/kevinpapst/kimai2/pull/150) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add english translation [\#8](https://github.com/kevinpapst/kimai2/issues/8)
|
||||
- Add configurable system configuration [\#6](https://github.com/kevinpapst/kimai2/issues/6)
|
||||
- Add CHANGELOG.md [\#177](https://github.com/kevinpapst/kimai2/issues/177)
|
||||
- Hide notifications after some seconds [\#169](https://github.com/kevinpapst/kimai2/issues/169)
|
||||
- Add preview blog message and info text on old website [\#162](https://github.com/kevinpapst/kimai2/issues/162)
|
||||
- Setup Scrutinizer to run code sniffer [\#158](https://github.com/kevinpapst/kimai2/issues/158)
|
||||
- Local config to overwrite settings [\#153](https://github.com/kevinpapst/kimai2/issues/153)
|
||||
- Improve mobile view [\#151](https://github.com/kevinpapst/kimai2/issues/151)
|
||||
- Upgrade to Symfony 4.1 [\#149](https://github.com/kevinpapst/kimai2/issues/149)
|
||||
- Replace has\_role\(\) with is\_granted\(\) [\#148](https://github.com/kevinpapst/kimai2/issues/148)
|
||||
- Add help text to form elements [\#138](https://github.com/kevinpapst/kimai2/issues/138)
|
||||
- Duration mode: creating new timesheet records prefills duration with some seconds [\#137](https://github.com/kevinpapst/kimai2/issues/137)
|
||||
- Add duration-only mode [\#131](https://github.com/kevinpapst/kimai2/issues/131)
|
||||
- Add help controller [\#129](https://github.com/kevinpapst/kimai2/issues/129)
|
||||
- \[RFC\] Encore: configureFilenames with hash after filename instead of in the filename [\#125](https://github.com/kevinpapst/kimai2/issues/125)
|
||||
- \[RFC\] Kimai license header [\#124](https://github.com/kevinpapst/kimai2/issues/124)
|
||||
- Secure create-user command [\#123](https://github.com/kevinpapst/kimai2/issues/123)
|
||||
- Setup travis to run phpunit tests [\#120](https://github.com/kevinpapst/kimai2/issues/120)
|
||||
- Add command for building a production release [\#115](https://github.com/kevinpapst/kimai2/issues/115)
|
||||
- Add webpack and webpack-encore for frontend assets [\#113](https://github.com/kevinpapst/kimai2/issues/113)
|
||||
- Add time rounding option [\#112](https://github.com/kevinpapst/kimai2/issues/112)
|
||||
@@ -102,7 +517,6 @@ bin/console doctrine:migrations:migrate
|
||||
- Add importer from Kimai v1 [\#102](https://github.com/kevinpapst/kimai2/issues/102)
|
||||
- Setup error pages [\#100](https://github.com/kevinpapst/kimai2/issues/100)
|
||||
- Add basic HTML invoice rendering [\#97](https://github.com/kevinpapst/kimai2/issues/97)
|
||||
- Add russian translation [\#95](https://github.com/kevinpapst/kimai2/issues/95)
|
||||
- Filter user in timesheet admin [\#93](https://github.com/kevinpapst/kimai2/issues/93)
|
||||
- Support to switch theme settings [\#89](https://github.com/kevinpapst/kimai2/issues/89)
|
||||
- Roles should become a real table [\#86](https://github.com/kevinpapst/kimai2/issues/86)
|
||||
@@ -111,10 +525,35 @@ bin/console doctrine:migrations:migrate
|
||||
- Add remember me login [\#53](https://github.com/kevinpapst/kimai2/issues/53)
|
||||
- Add preferences sidebar [\#10](https://github.com/kevinpapst/kimai2/issues/10)
|
||||
- Add user settings [\#9](https://github.com/kevinpapst/kimai2/issues/9)
|
||||
- Mobile alignments and toolbar \#151 [\#157](https://github.com/kevinpapst/kimai2/pull/157) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Local config \#153 [\#156](https://github.com/kevinpapst/kimai2/pull/156) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Filter in hidden toolbar - improved mobile \#151 [\#155](https://github.com/kevinpapst/kimai2/pull/155) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved mobile view \#151 [\#150](https://github.com/kevinpapst/kimai2/pull/150) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Change to users language after login [\#7](https://github.com/kevinpapst/kimai2/issues/7)
|
||||
- Prevent end time before start time [\#152](https://github.com/kevinpapst/kimai2/issues/152)
|
||||
- Importing old data fails [\#145](https://github.com/kevinpapst/kimai2/issues/145)
|
||||
- Translate LanguageType choices [\#83](https://github.com/kevinpapst/kimai2/issues/83)
|
||||
- Mobile alignments and toolbar \\#151 [\#157](https://github.com/kevinpapst/kimai2/pull/157) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Improved mobile view \\#151 [\#150](https://github.com/kevinpapst/kimai2/pull/150) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Add CHANGELOG.md [\#177](https://github.com/kevinpapst/kimai2/issues/177)
|
||||
- Add preview blog message and info text on old website [\#162](https://github.com/kevinpapst/kimai2/issues/162)
|
||||
- Upgrade to Symfony 4.1 [\#149](https://github.com/kevinpapst/kimai2/issues/149)
|
||||
- Replace has\_role\(\) with is\_granted\(\) [\#148](https://github.com/kevinpapst/kimai2/issues/148)
|
||||
- Duration mode: creating new timesheet records prefills duration with some seconds [\#137](https://github.com/kevinpapst/kimai2/issues/137)
|
||||
- \[RFC\] Encore: configureFilenames with hash after filename instead of in the filename [\#125](https://github.com/kevinpapst/kimai2/issues/125)
|
||||
- \[RFC\] Kimai license header [\#124](https://github.com/kevinpapst/kimai2/issues/124)
|
||||
- Setup travis to run phpunit tests [\#120](https://github.com/kevinpapst/kimai2/issues/120)
|
||||
- Add russian translation [\#95](https://github.com/kevinpapst/kimai2/issues/95)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- prepare v0.2 release \#177 [\#178](https://github.com/kevinpapst/kimai2/pull/178) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- \[WIP\] Translations [\#173](https://github.com/kevinpapst/kimai2/pull/173) ([simonschaufi](https://github.com/simonschaufi))
|
||||
- various documentation updates [\#176](https://github.com/kevinpapst/kimai2/pull/176) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- code styles: header\_comment and ordered\_imports [\#175](https://github.com/kevinpapst/kimai2/pull/175) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Code sniffer rules \#158 [\#172](https://github.com/kevinpapst/kimai2/pull/172) ([kevinpapst](https://github.com/kevinpapst))
|
||||
@@ -126,8 +565,6 @@ bin/console doctrine:migrations:migrate
|
||||
- added scrutinizer config \#158 [\#161](https://github.com/kevinpapst/kimai2/pull/161) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Update to Symfony 4.1 \#149 [\#160](https://github.com/kevinpapst/kimai2/pull/160) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Updated README [\#159](https://github.com/kevinpapst/kimai2/pull/159) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Local config \#153 [\#156](https://github.com/kevinpapst/kimai2/pull/156) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Filter in hidden toolbar - improved mobile \#151 [\#155](https://github.com/kevinpapst/kimai2/pull/155) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- added russian translation \#95 [\#147](https://github.com/kevinpapst/kimai2/pull/147) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- Bugfix importer \(\#145\) [\#146](https://github.com/kevinpapst/kimai2/pull/146) ([kevinpapst](https://github.com/kevinpapst))
|
||||
- help text and link for form labels \#138 [\#143](https://github.com/kevinpapst/kimai2/pull/143) ([kevinpapst](https://github.com/kevinpapst))
|
||||
@@ -159,7 +596,7 @@ bin/console doctrine:migrations:migrate
|
||||
- upgraded to symfony 4 \#74 [\#81](https://github.com/kevinpapst/kimai2/pull/81) ([kevinpapst](https://github.com/kevinpapst))
|
||||
|
||||
## [0.1](https://github.com/kevinpapst/kimai2/tree/0.1) (2018-01-10)
|
||||
**Closed issues:**
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Hide inactive delete user button [\#78](https://github.com/kevinpapst/kimai2/issues/78)
|
||||
- Delete activity [\#71](https://github.com/kevinpapst/kimai2/issues/71)
|
||||
|
||||
@@ -5,13 +5,13 @@ Send us your ideas, code reviews, pull requests and feature requests to help us
|
||||
|
||||
## Pull request rules
|
||||
|
||||
- We use PSR-2 with some additional code-style checks (see our [php-cs-fixer config](.php_cs.dist)). You can run `bin/console kimai:phpcs` to check and `bin/console kimai:phpcs --fix` to fix violations.
|
||||
- We use PSR-2 with some additional code-style checks (see our [php-cs-fixer config](.php_cs.dist)). You can run `bin/console kimai:codestyle` to check and `bin/console kimai:codestyle --fix` to fix violations.
|
||||
- Add PHPUnit tests for your changes, verify everything still works and execute our test-suites `bin/console kimai:test-unit` and `bin/console kimai:test-integration`.
|
||||
- If you contribute new files, please add them with the file-header template from below (our chode-style fixer can do that for you).
|
||||
- With sending in a PR, you accept that your contributions/code will be published under MIT license (see the [LICENSE](LICENSE) file as well).
|
||||
- If one of the PR checks fails, please fix them before asking us for a review.
|
||||
|
||||
Further documentation can be found in the [developer documentation](var/docs/developers.md).
|
||||
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).
|
||||
|
||||
### File-header template
|
||||
```
|
||||
|
||||
49
Dockerfile
Normal file
49
Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
||||
# This file is part of the Kimai time-tracking app.
|
||||
#
|
||||
# For the full copyright and license information, please view the LICENSE
|
||||
# file that was distributed with this source code.
|
||||
|
||||
FROM php:7.2.9-apache-stretch
|
||||
|
||||
RUN apt update && \
|
||||
apt install -y \
|
||||
git \
|
||||
libicu-dev \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
libldb-dev \
|
||||
libpng-dev \
|
||||
unzip \
|
||||
wget \
|
||||
zip \
|
||||
&& \
|
||||
EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)" && \
|
||||
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
|
||||
ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" && \
|
||||
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]; then >&2 echo 'ERROR: Invalid installer signature'; rm composer-setup.php; exit 1; fi && \
|
||||
php composer-setup.php --quiet && \
|
||||
rm composer-setup.php && \
|
||||
mv /var/www/html/composer.phar /usr/bin/composer && \
|
||||
docker-php-ext-install \
|
||||
gd \
|
||||
intl \
|
||||
ldap \
|
||||
pdo_mysql \
|
||||
zip && \
|
||||
apt remove -y wget && \
|
||||
apt -y autoremove && \
|
||||
apt clean && \
|
||||
git clone https://github.com/kevinpapst/kimai2.git /opt/kimai && \
|
||||
sed "s/prod/dev/g" /opt/kimai/.env.dist > /opt/kimai/.env && \
|
||||
composer install --working-dir=/opt/kimai --dev --optimize-autoloader && \
|
||||
/opt/kimai/bin/console doctrine:database:create && \
|
||||
/opt/kimai/bin/console doctrine:schema:create && \
|
||||
/opt/kimai/bin/console doctrine:migrations:version --add --all && \
|
||||
/opt/kimai/bin/console cache:warmup && \
|
||||
chown -R www-data:www-data /opt/kimai/var
|
||||
|
||||
WORKDIR /opt/kimai
|
||||
|
||||
EXPOSE 8001
|
||||
USER www-data
|
||||
CMD /opt/kimai/bin/console server:run 0.0.0.0:8001
|
||||
12
LICENSE
12
LICENSE
@@ -1,11 +1,13 @@
|
||||
Copyright (c) 2017-2018 Kevin Papst
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2019 Kevin Papst @ https://www.kevinpapst.de
|
||||
|
||||
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.
|
||||
175
README.md
175
README.md
@@ -1,6 +1,6 @@
|
||||
# Kimai v2 - Time Tracking
|
||||
# Kimai 2 - Time-tracking made easy
|
||||
|
||||
Kimai v2 - the open source time-tracking application with a mobile-first approach, read more at the [official website](http://v2.kimai.org).
|
||||
Kimai - the open source time-tracker application with a mobile-first approach (read more at the [official website](https://www.kimai.org)).
|
||||
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
@@ -11,152 +11,69 @@ Kimai v2 - the open source time-tracking application with a mobile-first approac
|
||||
|
||||
## Introduction
|
||||
|
||||
This is the reloaded version of the open source timetracker Kimai.
|
||||
The new version has not much in common with its predecessor [Kimai v1](http://www.kimai.org) besides the basic ideas of time-tracking and the current development team.
|
||||
This is the _reloaded_ version of the open source timetracker Kimai.
|
||||
It is built from scratch and doesn't share any source code with its [predecessor](http://www.kimai.org).
|
||||
But it adapts the same ideas and a clean & simple UI for your time-tracking experience.
|
||||
|
||||
Right now its in an early development phase, its usable but some advanced features from Kimai v1 are missing by now (like export and ODT invoices).
|
||||
But we already support to [import your timesheets](migration_v1.md) from Kimai v1.
|
||||
By now it is in an almost-stable development phase, usable and with most advanced features from Kimai 1.
|
||||
You can even [import your data](https://www.kimai.org/documentation/migration-v1.html) and start testing and using it today.
|
||||
|
||||
It is developed with modern frameworks like [Symfony v4](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/),
|
||||
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json)...
|
||||
Kimai is a [multi-language application](https://www.kimai.org/documentation/translations.html) and already translated to english, german, italian, french, spanish, russian, arabic, hungarian, portuguese (brazilian) and swedish.
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 7.1.3 or higher
|
||||
- One PHP extension of [PDO-SQLite](https://php.net/manual/en/ref.pdo-sqlite.php) or [PDO-MySQL](https://php.net/manual/en/ref.pdo-mysql.php) enabled (it might work with PostgreSQL and Oracle as well, but that wasn't tested and is not officially supported)
|
||||
- The PHP extension [intl](https://php.net/manual/en/book.intl.php)
|
||||
- The [usual Symfony application requirements](http://symfony.com/doc/current/reference/requirements.html)
|
||||
- If you use MariaDB, make sure its at least v10.7.2 (see [FAQ](var/docs/faq.md))
|
||||
- Kimai needs to be installed in the root directory of a domain or you need to [recompile the frontend assets](var/docs/developers.md)
|
||||
- A modern browser, Kimai v2 might be broken on old browsers like IE 9
|
||||
- PHP 7.2 or higher (test your system compatibility with the [requirements-checker](http://symfony.com/doc/current/reference/requirements.html))
|
||||
- The PHP extensions [xml](http://php.net/manual/en/book.xml.php), [mbstring](http://php.net/manual/en/book.mbstring.php), [gd](http://php.net/manual/en/book.image.php), [intl](https://php.net/manual/en/book.intl.php), [zip](https://php.net/manual/en/book.zip.php) and [PDO](https://php.net/manual/en/book.pdo.php) with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled
|
||||
- If you use MariaDB, make sure its at least v10.2.7 (see [FAQ](https://www.kimai.org/documentation/faq.html))
|
||||
- A modern browser, Kimai v2 might be broken on old browsers like IE 10
|
||||
|
||||
## Documentation & Roadmap
|
||||
## Documentation
|
||||
|
||||
Looking for more information about using Kimai? Check out our more detailed [documentation](var/docs/).
|
||||
Looking for more information about Kimai 2? Check out our detailed [documentation](https://www.kimai.org/documentation/).
|
||||
|
||||
You can see our development roadmap for the future in the [Milestones](milestones/) sections,
|
||||
current work is organized in the [Project](projects/) planning boards.
|
||||
Our roadmap is open for changes and input from the community, please [sent us](issues/) your ideas and questions.
|
||||
### Installation
|
||||
|
||||
## Installation
|
||||
There are multiple ways to install Kimai, all of them described in the [installation docu](https://www.kimai.org/documentation/installation.html):
|
||||
|
||||
There are [further infos about installation](var/docs/installation.md) if you have to use FTP or want to develop with Kimai.
|
||||
- [Recommended installation with GIT and Composer](https://www.kimai.org/documentation/installation.html#recommended-setup)
|
||||
- [Development setup](https://www.kimai.org/documentation/installation.html#development-installation)
|
||||
- [Docker](https://www.kimai.org/documentation/docker.html)
|
||||
- [1-click installations](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations)
|
||||
- [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation)
|
||||
|
||||
If you want to install Kimai v2 in your production environment, then SSH into your server and change to your webserevr root.
|
||||
You need to install Git and [Composer](https://getcomposer.org/doc/00-intro.md) if you haven't already.
|
||||
### Updating Kimai
|
||||
|
||||
First clone this repo:
|
||||
Read the following documentations before you start your upgrade:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kevinpapst/kimai2.git
|
||||
cd kimai2/
|
||||
```
|
||||
- The [update documentation](https://www.kimai.org/documentation/updates.html)
|
||||
- The version specific [UPGRADING guide](UPGRADING.md)
|
||||
- The [release information](https://github.com/kevinpapst/kimai2/releases)
|
||||
|
||||
Make sure the [file permissions are correct](https://symfony.com/doc/current/setup/file_permissions.html):
|
||||
```bash
|
||||
chown -R www-data var/
|
||||
chmod -R 777 var/
|
||||
```
|
||||
## Roadmap and releases
|
||||
|
||||
It's up to you which database server you want to use, Kimai v2 supports MySQL/MariaDB and SQLite.
|
||||
Configure the database connection string in your the `.env` file:
|
||||
```
|
||||
# adjust all settings in .env to your needs
|
||||
APP_ENV=prod
|
||||
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
|
||||
```
|
||||
You can see a rough development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
|
||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome!
|
||||
|
||||
Now install all dependencies for Kimai 2:
|
||||
> Kimai 2 uses a rolling release concept for delivering updates.
|
||||
> You can upgrade Kimai at any time, you don't need to wait for the next official release. Read the [upgrade docs](UPGRADING.md) first!
|
||||
|
||||
```bash
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
Release versions will be created on a regular base (approx. 1 per month) and you can use these tags if you are familiar with Git,
|
||||
but we cannot provide support for any specific version (especially older ones).
|
||||
Every code change, whether it's a new feature or a bug fix, will be done on the master branch.
|
||||
I have to do it this way, as I develop Kimai in my free time and want to put most
|
||||
effort into the software instead of backporting changes for old versions.
|
||||
|
||||
Optionally create the database:
|
||||
```bash
|
||||
bin/console doctrine:database:create
|
||||
```
|
||||
## Plugins for Kimai 2
|
||||
|
||||
Create all schema tables:
|
||||
```bash
|
||||
bin/console doctrine:schema:create
|
||||
```
|
||||
Kimai 2 was built on top of the Symfony framework and can be extended in various ways:
|
||||
|
||||
Make sure that upcoming updates can be correctly applied by setting the initial database version:
|
||||
```bash
|
||||
bin/console doctrine:migrations:version --add --all
|
||||
```
|
||||
- Users looking for existing plugins go to our [plugin marketplace](https://www.kimai.org/store/)
|
||||
- Developer start with the [developer documentation](https://www.kimai.org/documentation/developers.html)
|
||||
|
||||
Warm up the cache (as webserver user):
|
||||
```bash
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
```
|
||||
## Credits
|
||||
|
||||
Create your first user with the following command. You will be asked to enter a password afterwards:
|
||||
```bash
|
||||
bin/console kimai:create-user username admin@example.com ROLE_SUPER_ADMIN
|
||||
```
|
||||
_Tip: You can skip the "create user" step, if you are going to [import data from Kimai v1](migration_v1.md)._
|
||||
|
||||
For available roles, please refer to the [user documentation](var/docs/users.md).
|
||||
|
||||
> **NOTE**
|
||||
>
|
||||
> If you want to use a fully-featured web server (like Nginx or Apache) to run
|
||||
> Kimai, configure it to point at the `public/` directory of the project.
|
||||
> For more details, see:
|
||||
> http://symfony.com/doc/current/cookbook/configuration/web_server_configuration.html
|
||||
|
||||
Installation complete: enjoy time-tracking :-)
|
||||
|
||||
## Updating Kimai
|
||||
|
||||
**STOP**
|
||||
|
||||
1. It's important that you don't execute the Installation steps before or after your upgrade
|
||||
2. Make sure that you have a working database backup before you start the update
|
||||
3. Read the [UPGRADING](UPGRADING.md) guide and [release information](https://github.com/kevinpapst/kimai2/releases) to check if there a further steps required
|
||||
|
||||
Get the latest code and install dependencies:
|
||||
```bash
|
||||
cd kimai2/
|
||||
git pull origin master
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
|
||||
Refresh your cache:
|
||||
```bash
|
||||
sudo -u www-data bin/console cache:clear --env=prod
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
```
|
||||
|
||||
And upgrade your database:
|
||||
```bash
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
Done! You can use the latest version of Kimai 2.
|
||||
|
||||
## Rolling releases & Git
|
||||
|
||||
Please note: Kimai 2 uses a rolling release concept for delivering updates.
|
||||
Release versions will be created on a regular base and you can use these tags if you are familiar with Git, but we
|
||||
will not provide support for any specific version (whether its bugs or installation/update docu).
|
||||
|
||||
Every code change, whether it's a new features or bug fixes, will be targeted against the master branch and
|
||||
intensively tested before merging. We have to go this way, as we develop Kimai in our free time and want to put our
|
||||
effort into the software instead of installation scripts and complicated upgrade processes.
|
||||
|
||||
## Extensions for Kimai 2
|
||||
|
||||
As Kimai 2 was built on top of Symfony, it can be extended like every other Symfony application.
|
||||
We call these extensions bundles, but you might also know them as add-ons, extensions or plugins.
|
||||
|
||||
All available Kimai 2 bundles can be found at the [Kimai recipes](https://github.com/kimai/recipes) repository.
|
||||
|
||||
## Developer
|
||||
|
||||
If you want to develop with and for Kimai 2 please read the following documentation:
|
||||
|
||||
- an example on how to extend Kimai 2 can be found in this [GitHub repository](https://github.com/kevinpapst/kimai2-invoice)
|
||||
- the [developer documentation](var/docs/developers.md) is available both on GitHub and your local installation
|
||||
Kimai 2 is developed with modern frameworks like
|
||||
[Symfony v4](https://github.com/symfony/symfony),
|
||||
[Doctrine](https://github.com/doctrine/),
|
||||
[AdminLTEBundle](https://github.com/kevinpapst/AdminLTEBundle/) (based on [AdminLTE theme](https://github.com/almasaeed2010/AdminLTE)) and
|
||||
[many](composer.json) [more](package.json).
|
||||
|
||||
123
UPGRADING.md
123
UPGRADING.md
@@ -1,10 +1,127 @@
|
||||
# Upgrading Kimai 2
|
||||
|
||||
## [0.3](https://github.com/kevinpapst/kimai2/releases/tag/0.3) (2018-07-22)
|
||||
_Database upgrades are currently ONLY provided for MySQL/MariaDB and SQLite._
|
||||
|
||||
**Update from 0.2:**
|
||||
Upgrading to the latest available version can be achieved with these commands:
|
||||
|
||||
You need to adjust your `.env` file and add your `from` address for [all emails](https://github.com/kevinpapst/kimai2/blob/master/var/docs/emails.md) generated by Kimai 2:
|
||||
```bash
|
||||
git fetch --tags
|
||||
git checkout 0.8
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
sudo -u www-data bin/console cache:clear --env=prod
|
||||
sudo -u www-data bin/console cache:warmup --env=prod
|
||||
bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
There might be more steps required which are version specific and need to be executed after following the above list of commands.
|
||||
Read and follow each version info below, otherwise you risk data inconsistency or a broken installation!
|
||||
|
||||
And make sure to **create a backup before you start**.
|
||||
|
||||
## [0.9](https://github.com/kevinpapst/kimai2/releases/tag/0.9)
|
||||
|
||||
Follow the normal update and database migration process (see above).
|
||||
|
||||
Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||
|
||||
### BC BREAKS
|
||||
|
||||
This release contains some BC breaks which were necessary before 1.0 will be released (_now or never_), to prevent those BC breaks after 1.0.
|
||||
|
||||
- **Kimai requires PHP 7.2 now => [PHP 7.1 expired 4 month ago](https://www.php.net/supported-versions.php)**
|
||||
- The `.env` variable `DATABASE_PREFIX` was removed and the table prefix is now hardcoded to `kimai2_`. If you used another prefix,
|
||||
you have to rename your tables manually before starting the update process. You can delete the row `DATABASE_PREFIX` from your `.env` file.
|
||||
- API: Format for DateTime objects changed, now including timezone identifier (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00+00:00`), see [#718](https://github.com/kevinpapst/kimai2/pull/718)
|
||||
- API: changed from snake_case to camelCase (affected fields: hourlyRate vs hourly_rate / fixedRate vs fixed_rate / orderNumber vs order_number / i18n config object)
|
||||
- Plugin mechanism changed: existing Plugins have to be deleted or updated
|
||||
|
||||
### Apply necessary changes to your `local.yaml`:
|
||||
|
||||
New permissions are available:
|
||||
- `system_configuration` - for accessing the new system configuration screen
|
||||
- `system_actions` - for the experimental feature to flush your cache from the about screen
|
||||
- `plugins` - for accessing the new plugins screen
|
||||
|
||||
The setting `kimai.timesheet.mode` replaces the setting `kimai.timesheet.duration_only`. If you used the duration_only mode, you need to change your config:
|
||||
```yaml
|
||||
# Before
|
||||
kimai:
|
||||
timesheet:
|
||||
duration_only: true
|
||||
|
||||
# After
|
||||
kimai:
|
||||
timesheet:
|
||||
mode: duration_only
|
||||
```
|
||||
Or switch the mode directly in the new System configuration screen within Kimai.
|
||||
|
||||
## [0.8.1](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1)
|
||||
|
||||
A bug fixing release. Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||
|
||||
## [0.8](https://github.com/kevinpapst/kimai2/releases/tag/0.8)
|
||||
|
||||
After you followed the normal update and database migration process (see above), you need to execute a bash command to convert your timesheet data for timezone support:
|
||||
|
||||
- Read this [pull request](https://github.com/kevinpapst/kimai2/pull/372) BEFORE you follow the instructions to convert the
|
||||
timezones in your existing time records with `bin/console kimai:convert-timezone`. Without that, you will end up with wrong times in your database.
|
||||
|
||||
### Apply necessary changes to your `local.yaml`:
|
||||
|
||||
- A new boolean setting `kimai.timesheet.rules.allow_future_times` was introduced
|
||||
- New permissions are available:
|
||||
- `view_export` - for the new export feature
|
||||
- `create_export` - for the new export feature
|
||||
- `edit_export_own_timesheet` - for the new export feature
|
||||
- `edit_export_other_timesheet` - for the new export feature
|
||||
- `system_information` - to see the new about screen
|
||||
|
||||
## [0.7](https://github.com/kevinpapst/kimai2/releases/tag/0.7)
|
||||
|
||||
The configuration `kimai.theme.active_warning` was deprecated and should be replaced in your local.yaml,
|
||||
[read config docs for more information](https://www.kimai.org/documentation/timesheet.html#limit-active-entries).
|
||||
|
||||
## [0.6.1](https://github.com/kevinpapst/kimai2/releases/tag/0.6.1)
|
||||
|
||||
A bugfix release to address database compatibility issues with older MySQL/MariaDB versions.
|
||||
|
||||
## [0.6](https://github.com/kevinpapst/kimai2/releases/tag/0.6)
|
||||
|
||||
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)
|
||||
|
||||
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://www.kimai.org/documentation/invoices.html)).
|
||||
|
||||
**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)
|
||||
|
||||
In the time between 0.3 and 0.4 there was a release of composer that introduced a BC break,
|
||||
which leads to problems between Composer and Symfony Flex, resulting in an error like this when running it:
|
||||
|
||||
```
|
||||
[ErrorException]
|
||||
Declaration of Symfony\Flex\ParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with Composer\Util\RemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array &$responseHeaders = NULL)
|
||||
```
|
||||
|
||||
This can be fixed by updating Composer and Flex before executing the Kimai update:
|
||||
```
|
||||
sudo composer self-update
|
||||
sudo -u www-data composer update symfony/flex --no-plugins --no-scripts
|
||||
```
|
||||
|
||||
## [0.3](https://github.com/kevinpapst/kimai2/releases/tag/0.3)
|
||||
|
||||
You need to adjust your `.env` file and add your `from` address for [all emails](https://www.kimai.org/documentation/emails.html) generated by Kimai 2:
|
||||
```
|
||||
MAILER_FROM=kimai@example.com
|
||||
```
|
||||
|
||||
67
assets/app.js
Normal file
67
assets/app.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// ------------------- INLINED ADMIN-LTE DEFINITIONS -------------------
|
||||
// this was done to save around 300kb by:
|
||||
// - removing moment locales which are not used
|
||||
// - removing fullcalendar locales which are not used
|
||||
// - removing icheck which is not used
|
||||
|
||||
//require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
|
||||
|
||||
const $ = require('jquery');
|
||||
global.$ = global.jQuery = $;
|
||||
|
||||
require('jquery-ui');
|
||||
require('bootstrap-sass');
|
||||
require('jquery-slimscroll');
|
||||
require('bootstrap-select');
|
||||
|
||||
const Moment = require('moment');
|
||||
global.moment = Moment;
|
||||
require('moment/locale/de');
|
||||
require('moment/locale/it');
|
||||
require('moment/locale/fr');
|
||||
require('moment/locale/es');
|
||||
require('moment/locale/ru');
|
||||
require('moment/locale/ar');
|
||||
require('moment/locale/hu');
|
||||
require('moment/locale/pt-br');
|
||||
|
||||
require('daterangepicker');
|
||||
|
||||
// ------ AdminLTE framework ------
|
||||
require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte.scss');
|
||||
require('admin-lte/dist/css/AdminLTE.min.css');
|
||||
require('admin-lte/dist/css/skins/_all-skins.css');
|
||||
require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte-extensions.scss');
|
||||
|
||||
global.$.AdminLTE = {};
|
||||
global.$.AdminLTE.options = {};
|
||||
require('admin-lte/dist/js/adminlte.min');
|
||||
|
||||
require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/default_avatar.png');
|
||||
|
||||
// ------------------- INLINED ADMIN-LTE DEFINITIONS -------------------
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
require('./sass/app.scss');
|
||||
|
||||
require('fullcalendar');
|
||||
require('fullcalendar/dist/gcal.min');
|
||||
require('fullcalendar/dist/locale/de');
|
||||
require('fullcalendar/dist/locale/it');
|
||||
require('fullcalendar/dist/locale/fr');
|
||||
require('fullcalendar/dist/locale/es');
|
||||
require('fullcalendar/dist/locale/ru');
|
||||
require('fullcalendar/dist/locale/ar');
|
||||
require('fullcalendar/dist/locale/hu');
|
||||
require('fullcalendar/dist/locale/pt-br');
|
||||
require('fullcalendar/dist/fullcalendar.min.css');
|
||||
|
||||
// ------ for charts ------
|
||||
require('chart.js/dist/Chart.min');
|
||||
|
||||
// ------ Kimai itself ------
|
||||
require('./js/kimai.js');
|
||||
require('./js/datatable.js');
|
||||
require('./js/toolbar.js');
|
||||
require('./images/default_avatar.png');
|
||||
require('./images/signature.png');
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
|
||||
|
||||
.error-page {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* ================================ CONTENT ================================ */
|
||||
|
||||
.content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================ NAVBAR ================================ */
|
||||
|
||||
.navbar-nav > li > a.ddt-large {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
li.messages-menu ul.menu li .pull-left i {
|
||||
color: #dd4b39;
|
||||
}
|
||||
|
||||
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.navbar-nav .start_record {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
.ticktac:hover i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
li.open .ticktac i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
.ticktac i.stopped{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ticktac:hover i.stopped{
|
||||
color: #4ff131;
|
||||
}
|
||||
.user-panel>.info {
|
||||
left: 40px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* ================================ TOOLBAR ================================ */
|
||||
|
||||
/* Page based action buttons in the upper right corner of the content area */
|
||||
.content-header > .breadcrumb {
|
||||
position: absolute;
|
||||
float: right;
|
||||
background: transparent;
|
||||
}
|
||||
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
|
||||
.control-sidebar {
|
||||
select {
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
div.image {
|
||||
img.img-circle {
|
||||
max-width: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The filter form available on most pages above the datatable */
|
||||
.toolbar form {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
/* ================================ SIDEBAR ================================ */
|
||||
|
||||
/* Used for the Sidebar UserPanel (which is deactivated right now) */
|
||||
.user-panel>.info {
|
||||
left: 80px;
|
||||
}
|
||||
|
||||
/* ================================ FOOTER ================================ */
|
||||
|
||||
footer.main-footer {
|
||||
padding: 5px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* ================================ PRINT ================================ */
|
||||
|
||||
@media print{
|
||||
.sf-toolbar, .control-sidebar {display: none !important;}
|
||||
}
|
||||
|
||||
/* ================================ INVOICES ================================ */
|
||||
|
||||
.invoice {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.invoice {
|
||||
margin: 10px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-small{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div.invoice-address {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
table.invoice-records {}
|
||||
table.invoice-meta th {
|
||||
padding-right: 40px;
|
||||
}
|
||||
table.invoice-sum th {
|
||||
width: 70%;
|
||||
}
|
||||
table.invoice-sum th, table.invoice-sum td {
|
||||
text-align: right;
|
||||
}
|
||||
BIN
assets/images/signature.png
Normal file
BIN
assets/images/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,10 +0,0 @@
|
||||
require('../../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
|
||||
|
||||
require('../css/app.scss');
|
||||
require('../css/datatable.scss');
|
||||
|
||||
// ------ Kimai itself ------
|
||||
require('./kimai.js');
|
||||
require('./datatable.js');
|
||||
require('./toolbar.js');
|
||||
require('../images/default_avatar.png');
|
||||
@@ -22,24 +22,38 @@ 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();
|
||||
$.kimai.reloadDatatableWithToolbarFilter();
|
||||
},
|
||||
resetVisibility: function (modalSelector) {
|
||||
$(modalSelector).find('form').each(
|
||||
function() {
|
||||
var cookieName = $(this).attr('name');
|
||||
Cookies.remove(cookieName);
|
||||
}
|
||||
);
|
||||
$(modalSelector).modal('toggle');
|
||||
$.kimai.reloadDatatableWithToolbarFilter();
|
||||
},
|
||||
changeVisibility: function (column) {
|
||||
var tbl = $('table.dataTable');
|
||||
@@ -49,12 +63,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');
|
||||
});
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
/*
|
||||
/*!
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* Main JS application file for Kimai 2. This file should be included in all pages.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/** global: jQuery */
|
||||
/** global: moment */
|
||||
|
||||
if (typeof jQuery === 'undefined') {
|
||||
throw new Error('Kimai requires jQuery');
|
||||
}
|
||||
|
||||
if (typeof moment === 'undefined') {
|
||||
throw new Error('Kimai requires moment.js');
|
||||
}
|
||||
|
||||
/* kimai
|
||||
*
|
||||
* @type Object
|
||||
@@ -29,59 +38,87 @@ $(function() {
|
||||
$.kimai.settings = $.extend({}, $.kimai.defaults, options);
|
||||
}
|
||||
|
||||
// ask before a delete call is executed
|
||||
$('a.btn-trash').click(function (event) {
|
||||
return confirm($.kimai.settings['confirmDelete']);
|
||||
});
|
||||
// set the current locale for all javascript components
|
||||
moment.locale($.kimai.settings['locale']);
|
||||
|
||||
// activate the dropdown functionality
|
||||
$('.dropdown-toggle').dropdown();
|
||||
|
||||
// auto hide success message after x seconds, as they are just meant as quick feedback and
|
||||
// not as a permanent source of information
|
||||
if ($.kimai.settings['alertSuccessAutoHide'] > 0) {
|
||||
setTimeout(
|
||||
function() {
|
||||
$('div.alert-success').alert('close');
|
||||
setTimeout(
|
||||
function() {
|
||||
$('div.alert-success').alert('close');
|
||||
},
|
||||
5000
|
||||
);
|
||||
|
||||
// compound field in toolbar
|
||||
this.activateDateRangePicker('.content-wrapper');
|
||||
|
||||
// single select boxes in toolbars
|
||||
this.activateDatePicker('.content-wrapper');
|
||||
|
||||
// edit timesheet - date with time
|
||||
this.activateDateTimePicker('.content-wrapper');
|
||||
|
||||
// some actions can be performed in a modal for a better UX
|
||||
this.activateAjaxFormInModal('a.modal-ajax-form');
|
||||
|
||||
$('body').on('change', 'select[data-related-select]', function(event) {
|
||||
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'
|
||||
},
|
||||
$.kimai.settings['alertSuccessAutoHide']
|
||||
);
|
||||
}
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
var selectName = '#' + targetSelect;
|
||||
var $select = $(selectName);
|
||||
var $emptyOption = $(selectName + ' option[value=""]');
|
||||
|
||||
$('input[data-datepicker="on"]').daterangepicker({
|
||||
singleDatePicker: true,
|
||||
showDropdowns: true,
|
||||
locale: {
|
||||
format: "YYYY-MM-DD",
|
||||
firstDay: 1
|
||||
$select.find('option').remove().end().find('optgroup').remove().end();
|
||||
|
||||
if ($emptyOption.length !== 0) {
|
||||
$select.append('<option value="">' + $emptyOption.text() + '</option>');
|
||||
}
|
||||
|
||||
$.each(data, function(i, obj) {
|
||||
$select.append('<option value="' + obj.id + '">' + obj.name + '</option>');
|
||||
});
|
||||
|
||||
// if we don't trigger the change, the other selects won't be resetted
|
||||
$select.trigger('change');
|
||||
|
||||
// if the beta test kimai.theme.select_type is active, this will tell the selects to refresh
|
||||
$('.selectpicker').selectpicker('refresh');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
reloadDatatableWithToolbarFilter: function() {
|
||||
// TODO check if toolbar form is present, if not, reload current URL
|
||||
var $form = $('.toolbar form');
|
||||
var loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
|
||||
$('section.content').append(loading);
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
type: $form.attr('method'),
|
||||
data: $form.serialize(),
|
||||
success: function(html) {
|
||||
$('section.content').replaceWith(
|
||||
$(html).find('section.content')
|
||||
);
|
||||
},
|
||||
error: function(xhr, err) {
|
||||
$form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
$('input[data-datetimepicker="on"]').daterangepicker({
|
||||
singleDatePicker: true,
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
cancelLabel: 'Clear',
|
||||
format: "YYYY-MM-DD HH:mm",
|
||||
firstDay: 1
|
||||
}
|
||||
});
|
||||
$('input[data-datetimepicker="on"]').on('apply.daterangepicker', function(ev, picker) {
|
||||
$(this).val(picker.startDate.format('YYYY-MM-DD HH:mm'));
|
||||
});
|
||||
|
||||
/*
|
||||
$('input').iCheck({
|
||||
checkboxClass: 'icheckbox_square-blue',
|
||||
radioClass: 'iradio_square-blue',
|
||||
increaseArea: '20%' // optional
|
||||
});
|
||||
*/
|
||||
|
||||
// $.AdminLTE.pushMenu.expandOnHover();
|
||||
},
|
||||
pauseRecord: function(selector) {
|
||||
$(selector + ' .pull-left i').hover(function () {
|
||||
@@ -93,15 +130,212 @@ $(function() {
|
||||
link.attr('href', link.attr('href').replace('/pause', '/stop'));
|
||||
$(this).removeClass('fa-pause-circle').removeClass('text-orange').addClass('fa-stop-circle');
|
||||
});
|
||||
},
|
||||
activateDatePicker: function(selector) {
|
||||
$(selector + ' input[data-datepickerenable="on"]').each(function(index) {
|
||||
var localeFormat = $(this).data('format');
|
||||
$(this).daterangepicker({
|
||||
singleDatePicker: true,
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
format: localeFormat,
|
||||
firstDay: 1,
|
||||
applyLabel: $.kimai.settings['apply'],
|
||||
cancelLabel: $.kimai.settings['cancel'],
|
||||
customRangeLabel: $.kimai.settings['customRange']
|
||||
}
|
||||
});
|
||||
|
||||
$(this).on('apply.daterangepicker', function(ev, picker) {
|
||||
$(this).val(picker.startDate.format(localeFormat));
|
||||
$(this).trigger("change");
|
||||
});
|
||||
});
|
||||
},
|
||||
activateDateTimePicker: function(selector) {
|
||||
$(selector + ' input[data-datetimepicker="on"]').each(function(index) {
|
||||
var localeFormat = $(this).data('format');
|
||||
$(this).daterangepicker({
|
||||
singleDatePicker: true,
|
||||
timePicker: true,
|
||||
timePicker24Hour: $.kimai.settings['twentyFourHours'],
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
format: localeFormat,
|
||||
firstDay: 1,
|
||||
applyLabel: $.kimai.settings['apply'],
|
||||
cancelLabel: $.kimai.settings['cancel'],
|
||||
customRangeLabel: $.kimai.settings['customRange']
|
||||
}
|
||||
});
|
||||
|
||||
$(this).on('apply.daterangepicker', function(ev, picker) {
|
||||
$(this).val(picker.startDate.format(localeFormat));
|
||||
$(this).trigger("change");
|
||||
});
|
||||
});
|
||||
},
|
||||
activateDateRangePicker: function(selector) {
|
||||
$(selector + ' input[data-daterangepickerenable="on"]').each(function(index) {
|
||||
var localeFormat = $(this).data('format');
|
||||
var separator = $(this).data('separator');
|
||||
var rangesList = {};
|
||||
rangesList[$.kimai.settings['today']] = [moment(), moment()];
|
||||
rangesList[$.kimai.settings['yesterday']] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
|
||||
rangesList[$.kimai.settings['thisWeek']] = [moment().startOf('week'), moment().endOf('week')];
|
||||
rangesList[$.kimai.settings['lastWeek']] = [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')];
|
||||
rangesList[$.kimai.settings['thisMonth']] = [moment().startOf('month'), moment().endOf('month')];
|
||||
rangesList[$.kimai.settings['lastMonth']] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')];
|
||||
rangesList[$.kimai.settings['thisYear']] = [moment().startOf('year'), moment().endOf('year')];
|
||||
rangesList[$.kimai.settings['lastYear']] = [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')];
|
||||
|
||||
$(this).daterangepicker({
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: false,
|
||||
autoApply: false,
|
||||
linkedCalendars: false,
|
||||
locale: {
|
||||
separator: separator,
|
||||
format: localeFormat,
|
||||
firstDay: 1,
|
||||
applyLabel: $.kimai.settings['apply'],
|
||||
cancelLabel: $.kimai.settings['cancel'],
|
||||
customRangeLabel: $.kimai.settings['customRange']
|
||||
},
|
||||
ranges: rangesList,
|
||||
alwaysShowCalendars: true
|
||||
});
|
||||
|
||||
$(this).on('apply.daterangepicker', function(ev, picker) {
|
||||
$(this).val(picker.startDate.format(localeFormat) + ' - ' + picker.endDate.format(localeFormat));
|
||||
$(this).trigger("change");
|
||||
});
|
||||
});
|
||||
},
|
||||
ajaxFormInModal: function(html) {
|
||||
// the modal that we use to render the form in
|
||||
var formIdentifier = '#remote_form_modal .modal-content form';
|
||||
var flashErrorIdentifier = 'div.alert-error';
|
||||
var $form = $(formIdentifier);
|
||||
var $modal = $('#remote_form_modal');
|
||||
|
||||
// will be (re-)activated later
|
||||
$form.off('submit');
|
||||
|
||||
// load new form from given content
|
||||
if ($(html).find('#form_modal .modal-content').length > 0 ) {
|
||||
// switch classes, in case the modal type changed
|
||||
$modal.on('hidden.bs.modal', function () {
|
||||
if ($modal.hasClass('modal-danger')) {
|
||||
$modal.removeClass('modal-danger');
|
||||
}
|
||||
});
|
||||
|
||||
if ($(html).find('#form_modal').hasClass('modal-danger')) {
|
||||
$modal.addClass('modal-danger');
|
||||
}
|
||||
|
||||
// TODO cleanup widgets before replacing the content?
|
||||
$('#remote_form_modal .modal-content').replaceWith(
|
||||
$(html).find('#form_modal .modal-content')
|
||||
);
|
||||
// activate new loaded widgets
|
||||
$.kimai.activateDateTimePicker(formIdentifier);
|
||||
}
|
||||
|
||||
// show error flash messages
|
||||
if ($(html).find(flashErrorIdentifier).length > 0) {
|
||||
$('#remote_form_modal .modal-body').prepend(
|
||||
$(html).find(flashErrorIdentifier)
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// a fix for firefox focus problems with datepicker in modal
|
||||
// see https://github.com/kevinpapst/kimai2/issues/618
|
||||
var enforceModalFocusFn = $.fn.modal.Constructor.prototype.enforceFocus;
|
||||
$.fn.modal.Constructor.prototype.enforceFocus = function() {};
|
||||
$modal.on('hidden.bs.modal', function () {
|
||||
$.fn.modal.Constructor.prototype.enforceFocus = enforceModalFocusFn;
|
||||
});
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// workaround for autofocus attribute, as the modal "steals" it
|
||||
$modal.on('shown.bs.modal', function () {
|
||||
$(this).find('input[type=text],textarea,select').filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
|
||||
});
|
||||
|
||||
$modal.modal('show');
|
||||
|
||||
// the new form that was loaded via ajax
|
||||
$form = $(formIdentifier);
|
||||
|
||||
// click handler for modal save button, to send forms via ajax
|
||||
$form.on('submit', function(event){
|
||||
var btn = $(formIdentifier + ' button[type=submit]').button('loading');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$.ajax({
|
||||
url: $form.attr('action'),
|
||||
type: $form.attr('method'),
|
||||
data: $form.serialize(),
|
||||
success: function(html) {
|
||||
btn.button('reset');
|
||||
var hasFieldError = $(html).find('#form_modal .modal-content .has-error').length > 0;
|
||||
var hasFormError = $(html).find('#form_modal .modal-content ul.list-unstyled li.text-danger').length > 0;
|
||||
var hasFlashError = $(html).find(flashErrorIdentifier).length > 0;
|
||||
|
||||
if (hasFieldError || hasFormError || hasFlashError) {
|
||||
$.kimai.ajaxFormInModal(html);
|
||||
} else {
|
||||
$.kimai.reloadDatatableWithToolbarFilter();
|
||||
$modal.modal('hide');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
error: function(xhr, err) {
|
||||
// FIXME problem in google and 500 error, keeps on submitting...
|
||||
// what else could we do? submitting again at least gives us the opportunity to see errors,
|
||||
// which maybe would be hidden otherwise... this one is totally up for discussion!
|
||||
$form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
activateAjaxFormInModal: function(selector) {
|
||||
$('body').on('click', selector, function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$.ajax({
|
||||
url: $(this).attr('href'),
|
||||
success: function(html) {
|
||||
$.kimai.ajaxFormInModal(html);
|
||||
},
|
||||
error: function(xhr, err) {
|
||||
window.location = $(this).attr('href');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// default values
|
||||
$.kimai.defaults = {
|
||||
baseUrl: '/',
|
||||
imagePath: '/images',
|
||||
confirmDelete: 'Really delete?',
|
||||
alertSuccessAutoHide: 5000
|
||||
locale: 'en',
|
||||
today: 'Today',
|
||||
yesterday: 'Yesterday',
|
||||
apply: 'Apply',
|
||||
cancel: 'Cancel',
|
||||
thisWeek: 'This week',
|
||||
lastWeek: 'Last week',
|
||||
thisMonth: 'This month',
|
||||
lastMonth: 'Last month',
|
||||
thisYear: 'This year',
|
||||
lastYear: 'Last year',
|
||||
customRange: 'Custom range',
|
||||
twentyFourHours: true
|
||||
};
|
||||
|
||||
// once initialized, here are all values
|
||||
|
||||
@@ -6,32 +6,50 @@
|
||||
*/
|
||||
$(document).ready(function () {
|
||||
|
||||
//$('.navbar-form select').selectpicker({});
|
||||
/* Submit the pagination including the toolbar filters */
|
||||
$('body').on('click', 'div.navigation ul.pagination li a', function(event) {
|
||||
var $pager = $(".toolbar form input[name='page']");
|
||||
if ($pager.length === 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var $urlParts = $(this).attr('href').split('/');
|
||||
var page = $urlParts[$urlParts.length-1];
|
||||
$pager.val(page);
|
||||
$pager.trigger('change');
|
||||
return false;
|
||||
});
|
||||
|
||||
// DateRange - TODO improve me, so users can type without reloading in between
|
||||
$('.toolbar form input').change(function (event) {
|
||||
$('.toolbar form').submit();
|
||||
switch (event.target.id) {
|
||||
case 'page':
|
||||
break;
|
||||
default:
|
||||
$('.toolbar form input#page').val(1);
|
||||
}
|
||||
$.kimai.reloadDatatableWithToolbarFilter();
|
||||
});
|
||||
|
||||
$('.toolbar form select').change(function (event) {
|
||||
var reload = true;
|
||||
switch (event.target.id) {
|
||||
case 'customer':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#project').parent().remove();
|
||||
} else {
|
||||
$('.toolbar form select#project').val('');
|
||||
if ($('.toolbar form select#project').length > 0) {
|
||||
reload = false;
|
||||
}
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
break;
|
||||
|
||||
case 'project':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
} else {
|
||||
$('.toolbar form select#activity').val('');
|
||||
if ($('.toolbar form select#activity').length > 0) {
|
||||
reload = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
$('.toolbar form').submit();
|
||||
$('.toolbar form input#page').val(1);
|
||||
if (reload) {
|
||||
$.kimai.reloadDatatableWithToolbarFilter();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
42
assets/sass/app.scss
Normal file
42
assets/sass/app.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 'daterangepicker';
|
||||
@import 'tooltip';
|
||||
@import 'skins';
|
||||
7
assets/sass/calendar.scss
Normal file
7
assets/sass/calendar.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.popover-content {
|
||||
.calendar-entry {
|
||||
ul {
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
assets/sass/content.scss
Normal file
72
assets/sass/content.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
.content-header {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
|
||||
background-color: #fff !important;
|
||||
padding: 15px 0 15px 10px;
|
||||
|
||||
h1 {
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-header>.breadcrumb {
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
float: none;
|
||||
background: #d2d6de;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-header>.breadcrumb {
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&.timesheet-description {
|
||||
ul {
|
||||
padding-left: 15px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ABOUT SCREEN */
|
||||
.box-body .menu-icon {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 35px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
16
assets/sass/dashboard.scss
Normal file
16
assets/sass/dashboard.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
section {
|
||||
&.dashboard {
|
||||
.info-box {
|
||||
.info-box-text {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,43 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/* Loading indicator: overwritten from AdminLTE for positioning and supporting fas classes */
|
||||
.box .overlay > .fas,
|
||||
section.content .overlay > .fas,
|
||||
.overlay-wrapper .overlay > .fas {
|
||||
position: absolute;
|
||||
top: 50vh;
|
||||
left: 50vw;
|
||||
margin-left: -15px;
|
||||
margin-top: -15px;
|
||||
color: #000;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.box > .overlay,
|
||||
.overlay-wrapper > .overlay,
|
||||
section.content > .overlay,
|
||||
.box > .loading-img,
|
||||
.overlay-wrapper > .loading-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.box .overlay,
|
||||
section.content .overlay,
|
||||
.overlay-wrapper .overlay {
|
||||
z-index: 50;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.box .overlay.dark,
|
||||
section.content .overlay.dark,
|
||||
.overlay-wrapper .overlay.dark {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/*
|
||||
dataTables.bootstrap.css
|
||||
*/
|
||||
@@ -120,9 +157,14 @@ table.dataTable thead .sorting_desc_disabled:after {
|
||||
}
|
||||
|
||||
table.dataTable thead > tr > th {
|
||||
padding-right: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
table.dataTable thead > tr > th {
|
||||
padding-right: 30px;
|
||||
}
|
||||
*/
|
||||
table.dataTable th:active {
|
||||
outline: none;
|
||||
}
|
||||
@@ -222,3 +264,25 @@ div.dataTables_scrollHead table.table-bordered {
|
||||
.table-striped.dataTable tbody tr.active:nth-child(odd) th {
|
||||
background-color: #017ebc;
|
||||
}
|
||||
|
||||
table.dataTable {
|
||||
/* summary row, like the timesheet table provides */
|
||||
tr.summary td {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
td {
|
||||
/*
|
||||
Make sure that the action buttons do not line-break if another column takes all available space
|
||||
See https://github.com/twbs/bootstrap/issues/9939#issuecomment-37682952
|
||||
*/
|
||||
.btn-group {
|
||||
display: flex;
|
||||
}
|
||||
.label {
|
||||
font-size: 85%;
|
||||
font-weight: 500;
|
||||
padding: .3em .6em .3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
assets/sass/daterangepicker.scss
Normal file
12
assets/sass/daterangepicker.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
.drp-calendar {
|
||||
.calendar-table {
|
||||
td.today:not(.active) {
|
||||
background-color: lighten(#357ebd, 40);
|
||||
color: #357ebd;
|
||||
}
|
||||
select.yearselect{
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
305
assets/sass/invoice.scss
Normal file
305
assets/sass/invoice.scss
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* 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;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.invoice {
|
||||
margin: 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: 15px/1 $font-family-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;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
article.address {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
article.footer {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/* article */
|
||||
|
||||
article, table.meta, table.inventory {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
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: 85%;
|
||||
|
||||
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, .wrapper, #freelancer-invoice {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#freelancer-invoice {
|
||||
margin: 1em 2em;
|
||||
|
||||
span:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.add, .cut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
78
assets/sass/navbar.scss
Normal file
78
assets/sass/navbar.scss
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.navbar-nav {
|
||||
li>a.ddt-large {
|
||||
padding: 16px 12px 11px 12px;
|
||||
font-size: 10px;
|
||||
}
|
||||
li.messages-menu ul.menu li .pull-left i {
|
||||
color: #dd4b39;
|
||||
}
|
||||
.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.start_record {
|
||||
text-align: center;
|
||||
}
|
||||
.dropdown-menu {
|
||||
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
max-width: 90vw;
|
||||
width: unset !important;
|
||||
}
|
||||
.user-menu {
|
||||
margin-right: 10px;
|
||||
ul {
|
||||
li {
|
||||
a {
|
||||
color: #444;
|
||||
i {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-min) {
|
||||
.navbar-custom-menu>.navbar-nav>li>.dropdown-menu {
|
||||
right: 0;
|
||||
}
|
||||
.navbar-nav .user-menu {
|
||||
margin-right: 0;
|
||||
}
|
||||
.navbar-nav .user-menu.open .dropdown-menu > li > a {
|
||||
padding: 5px 25px 5px 25px
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
.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;
|
||||
}
|
||||
*/
|
||||
26
assets/sass/skins.scss
Normal file
26
assets/sass/skins.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
$skins: blue, blue, black, green, purple, red, yellow;
|
||||
|
||||
@each $skinColor in $skins {
|
||||
@media (max-width: $screen-sm-min) {
|
||||
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li.divider {
|
||||
background-color: #ccc;
|
||||
}
|
||||
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li a {
|
||||
color: #444;
|
||||
}
|
||||
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li a:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li.divider {
|
||||
background-color: #ccc;
|
||||
}
|
||||
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li a {
|
||||
color: #444;
|
||||
}
|
||||
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li a:hover {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
assets/sass/toolbar.scss
Normal file
54
assets/sass/toolbar.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/* Page based action buttons in the upper right corner of the content area */
|
||||
.content-header > .breadcrumb {
|
||||
position: absolute;
|
||||
float: right;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
|
||||
.control-sidebar {
|
||||
select {
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
div.image {
|
||||
img.img-circle {
|
||||
max-width: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-pad {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* The filter form is available on most pages above the datatable */
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.toolbar {
|
||||
form.navbar-form {
|
||||
font-size: $font-size-base;
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.control-label {
|
||||
width: 100%;
|
||||
}
|
||||
.input-group {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar-pad {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
36
assets/sass/tooltip.scss
Normal file
36
assets/sass/tooltip.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* These styles are not yet used and need some love before actively promoted in the UI.
|
||||
* They could be used in: templates/macros/widgets.html.twig::label
|
||||
*/
|
||||
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
[data-tooltip]:after {
|
||||
transition: all .2s ease;
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
top: -24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: $gray;
|
||||
color: #fff;
|
||||
padding: 2px 12px;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
border-radius: $border-radius-small;
|
||||
z-index: 2;
|
||||
}
|
||||
[data-tooltip]:not(:hover):after {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -3,52 +3,72 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"description": "Kimai Time-Tracking 2",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kevin Papst",
|
||||
"homepage": "https://www.kevinpapst.de"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"php": "^7.2",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_sqlite": "*",
|
||||
"beberlei/DoctrineExtensions": "^1.0",
|
||||
"ext-zip": "*",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.0",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"friendsofsymfony/rest-bundle": "^2.3",
|
||||
"friendsofsymfony/user-bundle": "~2.0",
|
||||
"kevinpapst/adminlte-bundle": "^1.2",
|
||||
"fzaninotto/faker": "^1.8",
|
||||
"gedmo/doctrine-extensions": "^2.4",
|
||||
"jms/metadata": "^2.0",
|
||||
"jms/serializer": "^2.3",
|
||||
"jms/serializer-bundle": "^3.2",
|
||||
"kevinpapst/adminlte-bundle": "~2.6",
|
||||
"kimai/kimai2-composer": "^0.1",
|
||||
"mpdf/mpdf": "^7.1",
|
||||
"nelmio/api-doc-bundle": "^3.2",
|
||||
"nelmio/cors-bundle": "^1.5",
|
||||
"ocramius/proxy-manager": "2.1.1",
|
||||
"sensio/framework-extra-bundle": "^5.1",
|
||||
"phpoffice/phpspreadsheet": "^1.4",
|
||||
"phpoffice/phpword": "^0.15.0",
|
||||
"sensio/framework-extra-bundle": "^5.2",
|
||||
"symfony/asset": "^4.0",
|
||||
"symfony/console": "^4.0",
|
||||
"symfony/debug-pack": "^1.0",
|
||||
"symfony/dotenv": "^4.0",
|
||||
"symfony/expression-language": "^4.0",
|
||||
"symfony/flex": "^1.0",
|
||||
"symfony/form": "^4.0",
|
||||
"symfony/framework-bundle": "^4.0",
|
||||
"symfony/lts": "^4@dev",
|
||||
"symfony/intl": "^4.0",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/orm-pack": "^1.0",
|
||||
"symfony/profiler-pack": "^1.0",
|
||||
"symfony/security-bundle": "^4.0",
|
||||
"symfony/security-csrf": "^4.0",
|
||||
"symfony/serializer": "^4.0",
|
||||
"symfony/swiftmailer-bundle": "^3.1",
|
||||
"symfony/translation": "^4.0",
|
||||
"symfony/twig-bundle": "^4.0",
|
||||
"symfony/validator": "^4.0",
|
||||
"symfony/webpack-encore-pack": "^1.0",
|
||||
"symfony/web-server-bundle": "^4.0",
|
||||
"symfony/yaml": "^4.0",
|
||||
"twig/extensions": "^1.5",
|
||||
"white-october/pagerfanta-bundle": "^1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^5.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^2.10",
|
||||
"phpunit/phpunit": "^7.0",
|
||||
"squizlabs/php_codesniffer": "^3.2",
|
||||
"symfony/browser-kit": "^4.0",
|
||||
"symfony/css-selector": "^4.0",
|
||||
"symfony/debug-pack": "^1.0",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/phpunit-bridge": "^4.0",
|
||||
"symfony/thanks": "^1.0",
|
||||
"symfony/web-server-bundle": "^4.0"
|
||||
"symfony/webpack-encore-pack": "^1.0"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": {
|
||||
@@ -58,7 +78,8 @@
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
"App\\": "src/",
|
||||
"KimaiPlugin\\": "var/plugins/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
@@ -72,6 +93,9 @@
|
||||
"symfony/polyfill-php70": "*",
|
||||
"symfony/polyfill-php56": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
@@ -90,13 +114,8 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
|
||||
"allow-contrib": false
|
||||
"allow-contrib": true,
|
||||
"require": "~4.2.0"
|
||||
}
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/kevinpapst/AdminThemeBundle"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
12474
composer.lock
generated
12474
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,5 +18,9 @@ return [
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
KevinPapst\AdminLTEBundle\AdminLTEBundle::class => ['all' => true],
|
||||
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
|
||||
FOS\UserBundle\FOSUserBundle::class => ['all' => true],
|
||||
FOS\RestBundle\FOSRestBundle::class => ['all' => true],
|
||||
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
|
||||
#
|
||||
# Configuration file for the AdminLTE bundle
|
||||
#
|
||||
# For more information about the settings visit:
|
||||
# https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/docs/configurations.md
|
||||
#
|
||||
admin_lte:
|
||||
options:
|
||||
# default image for missing user avatar images
|
||||
default_avatar: build/images/default_avatar.png
|
||||
# the color skin
|
||||
skin: skin-blue
|
||||
skin: skin-green
|
||||
# if activated, the boxed_layout will be deactivated
|
||||
fixed_layout: false
|
||||
fixed_layout: true
|
||||
# boxed layout (true) or full-screen (false)
|
||||
boxed_layout: false
|
||||
# initial collapsed state of the sidebar
|
||||
collapsed_sidebar: false
|
||||
# whether the logo collapses or not
|
||||
mini_sidebar: false
|
||||
# tabs in the control_sidebar (right screen) can be configured, see:
|
||||
# https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/docs/control_sidebar.md
|
||||
control_sidebar:
|
||||
settings:
|
||||
icon: "fas fa-cogs"
|
||||
controller: 'App\Controller\SidebarController::settingsAction'
|
||||
home:
|
||||
icon: "fas fa-question-circle"
|
||||
template: sidebar/home.html.twig
|
||||
mini_sidebar: true
|
||||
|
||||
theme:
|
||||
widget:
|
||||
use_footer: false
|
||||
|
||||
routes:
|
||||
adminlte_profile: user_profile
|
||||
adminlte_welcome: dashboard
|
||||
adminlte_welcome: homepage
|
||||
adminlte_login: fos_user_security_login
|
||||
adminlte_login_check: fos_user_security_check
|
||||
adminlte_registration: fos_user_registration_register
|
||||
|
||||
7
config/packages/dev/jms_serializer.yaml
Normal file
7
config/packages/dev/jms_serializer.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json_serialization:
|
||||
options:
|
||||
- JSON_PRETTY_PRINT
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
@@ -7,24 +7,30 @@ parameters:
|
||||
|
||||
doctrine:
|
||||
dbal:
|
||||
# configure these for your database server
|
||||
driver: 'pdo_mysql'
|
||||
server_version: '5.7'
|
||||
charset: utf8mb4
|
||||
default_table_options:
|
||||
charset: utf8mb4
|
||||
collate: utf8mb4_unicode_ci
|
||||
default_connection: default
|
||||
connections:
|
||||
default:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
driver: 'pdo_mysql'
|
||||
server_version: '5.7'
|
||||
charset: utf8mb4
|
||||
default_table_options:
|
||||
charset: utf8mb4
|
||||
collate: utf8mb4_unicode_ci
|
||||
|
||||
# With Symfony 3.3, remove the `resolve:` prefix
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
types:
|
||||
datetime: App\Doctrine\UTCDateTimeType
|
||||
orm:
|
||||
auto_generate_proxy_classes: '%kernel.debug%'
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
is_bundle: false
|
||||
type: annotation
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: Kimai
|
||||
default_entity_manager: default
|
||||
entity_managers:
|
||||
default:
|
||||
connection: default
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
type: annotation
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: Kimai
|
||||
|
||||
107
config/packages/fos_rest.yaml
Normal file
107
config/packages/fos_rest.yaml
Normal file
@@ -0,0 +1,107 @@
|
||||
fos_rest:
|
||||
# disable_csrf_role: IS_AUTHENTICATED_ANONYMOUSLY
|
||||
# access_denied_listener:
|
||||
# enabled: false
|
||||
# service: null
|
||||
# formats:
|
||||
# name: ~
|
||||
# unauthorized_challenge: null
|
||||
param_fetcher_listener:
|
||||
enabled: true
|
||||
# force: false
|
||||
# service: null
|
||||
cache_dir: '%kernel.cache_dir%/fos_rest'
|
||||
# allowed_methods_listener:
|
||||
# enabled: false
|
||||
# service: null
|
||||
routing_loader:
|
||||
default_format: json
|
||||
include_format: true
|
||||
# prefix_methods: true
|
||||
body_converter:
|
||||
enabled: true
|
||||
# validate: false
|
||||
# validation_errors_argument: validationErrors
|
||||
# service:
|
||||
# router: router
|
||||
# templating: templating
|
||||
# serializer: null
|
||||
# view_handler: fos_rest.view_handler.default
|
||||
# inflector: fos_rest.inflector.doctrine
|
||||
# validator: validator
|
||||
serializer:
|
||||
# version: null
|
||||
# groups: []
|
||||
serialize_null: true
|
||||
view:
|
||||
default_engine: twig
|
||||
# force_redirects:
|
||||
# name: ~
|
||||
# mime_types:
|
||||
# enabled: false
|
||||
# service: null
|
||||
# formats:
|
||||
# name: ~
|
||||
# formats:
|
||||
# name: ~
|
||||
# templating_formats:
|
||||
# name: ~
|
||||
view_response_listener:
|
||||
enabled: true
|
||||
# force: true
|
||||
# service: null
|
||||
failed_validation: 400
|
||||
# empty_content: 204
|
||||
# serialize_null: false
|
||||
# jsonp_handler:
|
||||
# callback_param: callback
|
||||
# mime_type: application/javascript+jsonp
|
||||
exception:
|
||||
enabled: true
|
||||
exception_controller: 'fos_rest.exception.controller:showAction'
|
||||
# service: null
|
||||
codes:
|
||||
# 'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
|
||||
# 'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
|
||||
# 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': 400
|
||||
messages:
|
||||
'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
|
||||
'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': true
|
||||
body_listener:
|
||||
enabled: true
|
||||
# service: null
|
||||
# default_format: null
|
||||
# throw_exception_on_unsupported_content_type: false
|
||||
# decoders:
|
||||
# name: ~
|
||||
array_normalizer:
|
||||
service: fos_rest.normalizer.camel_keys
|
||||
forms: true
|
||||
format_listener:
|
||||
enabled: true
|
||||
# service: null
|
||||
rules:
|
||||
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json, xml ] }
|
||||
# host: null # URL host name
|
||||
# methods: null # Method for URL
|
||||
# stop: false
|
||||
# attributes: []
|
||||
# versioning:
|
||||
# enabled: false
|
||||
# default_version: ~
|
||||
# resolvers:
|
||||
# query:
|
||||
# enabled: true
|
||||
# parameter_name: version
|
||||
# custom_header:
|
||||
# enabled: true
|
||||
# header_name: X-Accept-Version
|
||||
# media_type:
|
||||
# enabled: true
|
||||
# regex: /(v|version)=(?P<version>[0-9\.]+)/
|
||||
# guessing_order:
|
||||
# - query
|
||||
# - custom_header
|
||||
# - media_type
|
||||
zone:
|
||||
- { path: ^/api/* }
|
||||
17
config/packages/jms_serializer.yaml
Normal file
17
config/packages/jms_serializer.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
jms_serializer:
|
||||
handlers:
|
||||
datetime:
|
||||
default_format: 'Y-m-d\TH:i:sO' # DATE_ISO8601
|
||||
visitors:
|
||||
xml_serialization:
|
||||
format_output: '%kernel.debug%'
|
||||
metadata:
|
||||
directories:
|
||||
FOSUB:
|
||||
namespace_prefix: "FOS\\UserBundle"
|
||||
path: "%kernel.root_dir%/../config/serializer/FOS/UserBundle"
|
||||
App:
|
||||
namespace_prefix: "App"
|
||||
path: "%kernel.root_dir%/../config/serializer/App"
|
||||
property_naming:
|
||||
id: 'jms_serializer.identical_property_naming_strategy'
|
||||
@@ -1,52 +1,273 @@
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# DO NOT EDIT THIS FILE, INSTEAD CREATE THE FILE "local.yaml" AND ADD YOUR SETTINGS IN THERE.
|
||||
# See https://www.kimai.org/documentation/configurations.html
|
||||
#
|
||||
# Be aware that this file is YAML format and the indentation is important.
|
||||
# Each config level needs to be indented with 4 additional spaces.
|
||||
#
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
kimai:
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# You can disable the user management functions in the authentication screens (default: true).
|
||||
#user:
|
||||
# registration: false
|
||||
# password_reset: false
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# All configs related to timesheet and record management
|
||||
timesheet:
|
||||
|
||||
# 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
|
||||
# render timesheet descriptions with markdown
|
||||
markdown_content: false
|
||||
|
||||
# The time-tracking mode that should be used (allowed values: default, duration_only)
|
||||
#
|
||||
# default: display start and end time columns in timesheet view and form
|
||||
# duration_only: display start time and duration, https://www.kimai.org/documentation/timesheet.html#duration-only-mode
|
||||
mode: default
|
||||
|
||||
# Rounding rules are used to round the begin & end dates and the duration for timesheet records.
|
||||
# The "default" rule will round "begin" down and "end" up to the full minute, the "duration" will not be rounded.
|
||||
# Please read var/docs/configurations.md to find out more about rounding rules
|
||||
# Find out more about rounding rules at https://www.kimai.org/documentation/timesheet.html
|
||||
rounding:
|
||||
default:
|
||||
days: ['monday','tuesday','wednesday','thursday','friday','saturday','sunday']
|
||||
begin: 1
|
||||
end: 1
|
||||
duration: 0
|
||||
mode: default
|
||||
|
||||
# If you want to apply different hourly rates for specific weekdays, you can uncomment the "rates" configuration.
|
||||
# The "weekend" rule will add 50% to each timesheet entry that will be recorded on "saturdays" or "sundays".
|
||||
# Please read var/docs/configurations.md to find out more about rates rules
|
||||
# See documentation about rate rules: https://www.kimai.org/documentation/timesheet.html#rate-calculation
|
||||
# rates:
|
||||
# weekend:
|
||||
# days: ['saturday','sunday']
|
||||
# factor: 1.5
|
||||
|
||||
# All configs related to the invoice administration
|
||||
invoice:
|
||||
renderer:
|
||||
default: 'App\Controller\InvoiceController::invoiceAction'
|
||||
timesheet: 'App\Controller\InvoiceController::timesheetAction'
|
||||
calculator:
|
||||
default: 'App\Invoice\DefaultCalculator'
|
||||
short: 'App\Invoice\ShortInvoiceCalculator'
|
||||
number_generator:
|
||||
default: 'App\Invoice\DateNumberGenerator'
|
||||
# If you want to limit the max. active entries per user, you can do it here.
|
||||
#
|
||||
# The soft_limit is used as theme setting and displays a warning color if the user has at least X active recordings
|
||||
#
|
||||
# The hard_limit is used to detect how many active records are allowed per user:
|
||||
# By default a user can only have one active time-record. The first one is stopped when the new one starts.
|
||||
# When hard_limit is > 1: as soon as the limit is reached and warning is shown and the user has to stop one active entry.
|
||||
active_entries:
|
||||
soft_limit: 1
|
||||
hard_limit: 3
|
||||
|
||||
# Rules that define the Timesheet validation and behaviour
|
||||
rules:
|
||||
# whether records in the future can be created
|
||||
allow_future_times: true
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Invoice management
|
||||
#invoice:
|
||||
# # all files in these directories will be used as invoice documents (if supported by a renderer)
|
||||
# documents:
|
||||
# - 'var/invoices/'
|
||||
# - 'templates/invoice/renderer/'
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Default settings used to populate forms
|
||||
#defaults:
|
||||
# customer:
|
||||
# timezone: Europe/Berlin
|
||||
# country: DE
|
||||
# currency: EUR
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Find out more about the configurable permission system at https://www.kimai.org/documentation/permissions.html
|
||||
permissions:
|
||||
sets:
|
||||
# mapping complex rulesets of single permissions to named "sets" ("set name" = [array of "permissions"])
|
||||
ACTIVITIES: [view_activity,create_activity,edit_activity,delete_activity]
|
||||
PROJECTS: [view_project,create_project,edit_project,delete_project]
|
||||
CUSTOMERS: [view_customer,create_customer,edit_customer,delete_customer]
|
||||
INVOICE: [view_invoice,create_invoice]
|
||||
INVOICE_TEMPLATE: [view_invoice_template,create_invoice_template,edit_invoice_template,delete_invoice_template]
|
||||
TIMESHEET: [view_own_timesheet,start_own_timesheet,stop_own_timesheet,create_own_timesheet,edit_own_timesheet,export_own_timesheet,delete_own_timesheet]
|
||||
TIMESHEET_OTHER: [view_other_timesheet,start_other_timesheet,stop_other_timesheet,create_other_timesheet,edit_other_timesheet,export_other_timesheet,delete_other_timesheet]
|
||||
PROFILE: [view_own_profile,edit_own_profile,password_own_profile,preferences_own_profile,api-token_own_profile]
|
||||
PROFILE_OTHER: [view_other_profile,edit_other_profile,delete_other_profile,password_other_profile,roles_other_profile,preferences_other_profile,api-token_other_profile]
|
||||
USER: [view_user,create_user,delete_user]
|
||||
RATE: [view_rate_own_timesheet,edit_rate_own_timesheet]
|
||||
RATE_OTHER: [view_rate_other_timesheet,edit_rate_other_timesheet]
|
||||
EXPORT: [view_export,create_export,edit_export_own_timesheet,edit_export_other_timesheet]
|
||||
maps:
|
||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||
ROLE_USER: [TIMESHEET,PROFILE]
|
||||
ROLE_TEAMLEAD: [INVOICE,TIMESHEET,TIMESHEET_OTHER,PROFILE,EXPORT]
|
||||
ROLE_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,RATE,RATE_OTHER,EXPORT]
|
||||
ROLE_SUPER_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,PROFILE_OTHER,USER,RATE,RATE_OTHER,EXPORT]
|
||||
roles:
|
||||
# adding single permissions to user roles, extending the definition from "sets" ("role name" = [array of "permissions"])
|
||||
ROLE_USER: []
|
||||
ROLE_TEAMLEAD: [view_invoice_template,create_invoice_template,edit_invoice_template,view_rate_own_timesheet,view_rate_other_timesheet,hourly-rate_own_profile]
|
||||
ROLE_ADMIN: [hourly-rate_own_profile]
|
||||
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information,system_actions,system_configuration,plugins]
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Language specific settings, like the date formats
|
||||
languages:
|
||||
de:
|
||||
date_short: "d.m.Y"
|
||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||
date_type: 'dd.MM.yyyy'
|
||||
date: 'd.m.Y'
|
||||
date_time: 'd.m. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
en:
|
||||
date_short: "Y-m-d"
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
date_type: 'yyyy-MM-dd'
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m h'
|
||||
pt_BR:
|
||||
date_time_type: 'dd-MM-yyyy HH:mm'
|
||||
date_type: 'dd-MM-yyyy'
|
||||
date: 'd-m-Y'
|
||||
date_time: 'd-m H:i'
|
||||
duration: '%%h:%%m h'
|
||||
it:
|
||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||
date_type: 'dd.MM.yyyy'
|
||||
date: 'd.m.Y'
|
||||
date_time: 'd.m. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
fr:
|
||||
date_time_type: 'dd/MM/yyyy HH:mm'
|
||||
date_type: 'dd/MM/yyyy'
|
||||
date: 'd/m/Y'
|
||||
date_time: 'd/m H:i'
|
||||
duration: '%%h h %%m'
|
||||
es:
|
||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||
date_type: 'dd.MM.yyyy'
|
||||
date: 'd.m.Y'
|
||||
date_time: 'd.m. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
ru:
|
||||
date_short: "d.m.Y"
|
||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||
date_type: 'dd.MM.yyyy'
|
||||
date: 'd.m.Y'
|
||||
date_time: 'd.m. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
ar:
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
date_type: 'yyyy-MM-dd'
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m h'
|
||||
hu:
|
||||
date_time_type: 'yyyy.MM.dd. HH:mm'
|
||||
date_type: 'yyyy.MM.dd.'
|
||||
date: 'Y.m.d.'
|
||||
date_time: 'm.d. H:i'
|
||||
duration: '%%h:%%m h'
|
||||
sv:
|
||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||
date_type: 'yyyy-MM-dd'
|
||||
date: 'Y-m-d'
|
||||
date_time: 'm-d H:i'
|
||||
duration: '%%h:%%m tim'
|
||||
|
||||
twig:
|
||||
globals:
|
||||
kimai_context:
|
||||
box_color: "green" # a color for ???
|
||||
active_warning: 3 # display a warning color if the user has at least X active recordings
|
||||
# --------------------------------------------------------------------------------
|
||||
# timesheet calendar configuration
|
||||
calendar:
|
||||
week_numbers: true
|
||||
day_limit: 4
|
||||
businessHours:
|
||||
days: [1, 2, 3, 4, 5]
|
||||
begin: '08:00'
|
||||
end: '20:00'
|
||||
|
||||
# You can configure unlimited google calendars to display events for your company (e.g. holidays)
|
||||
# Find out more about calendar integration at https://www.kimai.org/documentation/calendar.html
|
||||
# google:
|
||||
# api_key: 'your-restricted-google-api-key'
|
||||
# sources:
|
||||
# holidays:
|
||||
# id: 'de.german#holiday@group.v.calendar.google.com'
|
||||
# color: '#ccc'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Theme related settings, will be available as twig globals at "kimai_context.*".
|
||||
# See documentation at https://www.kimai.org/documentation/theme.html
|
||||
theme:
|
||||
# BETA test: If you set this to 'selectpicker' the customer/project/activity select boxes will be transformed
|
||||
# into a searchable and javascript enhanced input type
|
||||
select_type: ~
|
||||
# fallback color for all widgets that don't have a dedicated color
|
||||
# possible options: blue, black, purple, yellow, red, green
|
||||
box_color: 'green'
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Dashboard widget sections.
|
||||
# See documentation at https://www.kimai.org/documentation/dashboard.html
|
||||
dashboard:
|
||||
user_duration:
|
||||
title: dashboard.you
|
||||
order: 10
|
||||
permission: ROLE_USER
|
||||
widgets: [userDurationToday, userDurationWeek, userDurationMonth, userDurationYear]
|
||||
user_rates:
|
||||
title: ~
|
||||
order: 20
|
||||
permission: ROLE_USER
|
||||
widgets: [userAmountToday, userAmountWeek, userAmountMonth, userAmountYear]
|
||||
duration:
|
||||
title: dashboard.all
|
||||
order: 30
|
||||
permission: ROLE_TEAMLEAD
|
||||
widgets: [durationToday, durationWeek, durationMonth, durationYear]
|
||||
active_users:
|
||||
title: ~
|
||||
order: 40
|
||||
permission: ROLE_TEAMLEAD
|
||||
widgets: [activeUsersToday, activeUsersWeek, activeUsersMonth, activeUsersYear]
|
||||
rates:
|
||||
title: ~
|
||||
order: 50
|
||||
permission: ROLE_ADMIN
|
||||
widgets: [amountToday, amountWeek, amountMonth, amountYear]
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Find out more about dashboard widgets at https://www.kimai.org/documentation/dashboard.html
|
||||
widgets:
|
||||
userDurationToday: { title: stats.durationToday, query: duration, user: true, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
|
||||
userDurationWeek: { title: stats.durationWeek, query: duration, user: true, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: duration, color: blue }
|
||||
userDurationMonth: { title: stats.durationMonth, query: duration, user: true, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: duration, color: purple }
|
||||
userDurationYear: { title: stats.durationYear, query: duration, user: true, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: duration, color: yellow }
|
||||
userDurationTotal: { title: stats.durationTotal, query: duration, user: true, icon: duration, color: red }
|
||||
userAmountToday: { title: stats.amountToday, query: rate, user: true, begin: '00:00:00', end: '23:59:59', icon: money, color: green }
|
||||
userAmountWeek: { title: stats.amountWeek, query: rate, user: true, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: money, color: blue }
|
||||
userAmountMonth: { title: stats.amountMonth, query: rate, user: true, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: money, color: purple }
|
||||
userAmountYear: { title: stats.amountYear, query: rate, user: true, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: money, color: yellow }
|
||||
userAmountTotal: { title: stats.amountTotal, query: rate, user: true, icon: money, color: red }
|
||||
durationToday: { title: stats.durationToday, query: duration, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
|
||||
durationWeek: { title: stats.durationWeek, query: duration, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: duration, color: blue }
|
||||
durationMonth: { title: stats.durationMonth, query: duration, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: duration, color: purple }
|
||||
durationYear: { title: stats.durationYear, query: duration, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: duration, color: yellow }
|
||||
durationTotal: { title: stats.durationTotal, query: duration, icon: duration, color: red }
|
||||
amountToday: { title: stats.amountToday, query: rate, begin: '00:00:00', end: '23:59:59', icon: money, color: green }
|
||||
amountWeek: { title: stats.amountWeek, query: rate, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: money, color: blue }
|
||||
amountMonth: { title: stats.amountMonth, query: rate, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: money, color: purple }
|
||||
amountYear: { title: stats.amountYear, query: rate, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: money, color: yellow }
|
||||
amountTotal: { title: stats.amountTotal, query: rate, icon: money, color: red }
|
||||
activeUsersToday: { title: stats.userActiveToday, query: users, begin: '00:00:00', end: '23:59:59', icon: user, color: green }
|
||||
activeUsersWeek: { title: stats.userActiveWeek, query: users, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: user, color: blue }
|
||||
activeUsersMonth: { title: stats.userActiveMonth, query: users, begin: 'first day of this month 00:00:00', end: 'last day of this month 23:59:59', icon: user, color: purple }
|
||||
activeUsersYear: { title: stats.userActiveYear, query: users, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: user, color: yellow }
|
||||
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
|
||||
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Storage directories
|
||||
data_dir: '%kernel.project_dir%/var/data'
|
||||
plugin_dir: '%kernel.project_dir%/var/plugins'
|
||||
|
||||
53
config/packages/nelmio_api_doc.yaml
Normal file
53
config/packages/nelmio_api_doc.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
nelmio_api_doc:
|
||||
models:
|
||||
use_jms: true
|
||||
names:
|
||||
- { alias: CustomerEditForm, type: App\Form\CustomerEditForm, groups: [Default, Entity, Customer] }
|
||||
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer] }
|
||||
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
|
||||
- { alias: ProjectEditForm, type: App\Form\ProjectEditForm, groups: [Default, Entity, Project] }
|
||||
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project] }
|
||||
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
|
||||
- { alias: ActivityEditForm, type: App\Form\ActivityEditForm, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
|
||||
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
|
||||
- { alias: TimesheetEditForm, 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] }
|
||||
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
|
||||
areas:
|
||||
path_patterns:
|
||||
- ^/api(?!/doc)
|
||||
documentation:
|
||||
# host: '{{hostname}}'
|
||||
schemes: [http, https]
|
||||
info:
|
||||
title: Kimai 2 - API Docs
|
||||
description: |
|
||||
REST API for the Kimai 2 time-tracking software. It is not yet considered stable and BC breaks might happen, even though I try to avoid them as much as possible.
|
||||
- Collections return less data than explicit entity calls
|
||||
- DateTime formats are explained in detail at https://www.kimai.org/documentation/rest-api.html
|
||||
version: 0.3
|
||||
# parameters:
|
||||
# hostname:
|
||||
# name: hostname
|
||||
# in: url
|
||||
# description: the hostname where your api runs
|
||||
# required: true
|
||||
# type: string
|
||||
securityDefinitions:
|
||||
X-AUTH-USER:
|
||||
type: apiKey
|
||||
description: 'Value: {Username}'
|
||||
name: X-AUTH-USER
|
||||
in: header
|
||||
X-AUTH-TOKEN:
|
||||
type: apiKey
|
||||
description: 'Value: {API Token}'
|
||||
name: X-AUTH-TOKEN
|
||||
in: header
|
||||
security:
|
||||
- X-AUTH-USER: []
|
||||
- X-AUTH-TOKEN: []
|
||||
25
config/packages/nelmio_cors.yaml
Normal file
25
config/packages/nelmio_cors.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
parameters:
|
||||
env(CORS_ALLOW_ORIGIN): 'localhost'
|
||||
|
||||
nelmio_cors:
|
||||
defaults:
|
||||
allow_credentials: false
|
||||
# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
# allow_headers: ['Content-Type', 'Authorization', 'X-AUTH-USER', 'X-AUTH-TOKEN']
|
||||
# allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
# expose_headers: ['Link']
|
||||
# max_age: 3600
|
||||
allow_origin: []
|
||||
allow_headers: []
|
||||
allow_methods: []
|
||||
expose_headers: []
|
||||
max_age: 0
|
||||
hosts: []
|
||||
origin_regex: true
|
||||
forced_allow_origin_value: ~
|
||||
paths:
|
||||
'^/api/':
|
||||
allow_origin: ['*']
|
||||
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
max_age: 3600
|
||||
6
config/packages/prod/jms_serializer.yaml
Normal file
6
config/packages/prod/jms_serializer.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
jms_serializer:
|
||||
visitors:
|
||||
json_serialization:
|
||||
options:
|
||||
- JSON_UNESCAPED_SLASHES
|
||||
- JSON_PRESERVE_ZERO_FRACTION
|
||||
@@ -2,7 +2,6 @@ security:
|
||||
encoders:
|
||||
App\Entity\User: bcrypt
|
||||
|
||||
# https://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
|
||||
providers:
|
||||
fos_userbundle:
|
||||
id: fos_user.user_provider.username_email
|
||||
@@ -13,43 +12,35 @@ security:
|
||||
security: false
|
||||
|
||||
secured_area:
|
||||
# this firewall applies to all URLs
|
||||
pattern: ^/
|
||||
|
||||
# make sure only allowed users have access to their time-tracking
|
||||
user_checker: App\Security\UserChecker
|
||||
anonymous: true
|
||||
stateless: false
|
||||
|
||||
# but the firewall does not require login on every page
|
||||
# denying access is done in access_control or in your controllers
|
||||
anonymous: ~
|
||||
guard:
|
||||
authenticators:
|
||||
- App\Security\TokenAuthenticator
|
||||
|
||||
remember_me:
|
||||
secret: '%kernel.secret%'
|
||||
lifetime: 604800
|
||||
path: /
|
||||
|
||||
# This allows the user to login by submitting a username and password
|
||||
# Reference: http://symfony.com/doc/current/cookbook/security/form_login_setup.html
|
||||
form_login:
|
||||
# fos user bundle handles the form login
|
||||
provider: fos_userbundle
|
||||
# The route name that the login form submits to
|
||||
check_path: fos_user_security_check
|
||||
# The name of the route where the login form lives
|
||||
# When the user tries to access a protected page, they are redirected here
|
||||
login_path: fos_user_security_login
|
||||
# Secure the login form against CSRF
|
||||
# Reference: http://symfony.com/doc/current/cookbook/security/csrf_in_login_form.html
|
||||
csrf_token_generator: security.csrf.token_manager
|
||||
|
||||
logout:
|
||||
# The route name the user can go to in order to logout
|
||||
path: fos_user_security_logout
|
||||
# The name of the route to redirect to after logging out
|
||||
target: homepage
|
||||
|
||||
access_decision_manager:
|
||||
strategy: unanimous
|
||||
allow_if_all_abstain: false
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_USER: ROLE_CUSTOMER
|
||||
ROLE_TEAMLEAD: ROLE_USER
|
||||
ROLE_ADMIN: ROLE_TEAMLEAD
|
||||
ROLE_SUPER_ADMIN: ROLE_ADMIN
|
||||
@@ -59,5 +50,5 @@ security:
|
||||
- { path: '^/(%app_locales%)/login', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: '^/(%app_locales%)/admin', roles: ROLE_ADMIN }
|
||||
|
||||
- { path: '^/(%app_locales%)/', roles: ROLE_USER }
|
||||
- { path: '^/api', roles: IS_AUTHENTICATED_REMEMBERED }
|
||||
|
||||
3
config/packages/sensio_framework_extra.yaml
Normal file
3
config/packages/sensio_framework_extra.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
sensio_framework_extra:
|
||||
router:
|
||||
annotations: false
|
||||
@@ -2,3 +2,5 @@ twig:
|
||||
#paths: ['%kernel.project_dir%/templates']
|
||||
debug: '%kernel.debug%'
|
||||
strict_variables: '%kernel.debug%'
|
||||
form_themes:
|
||||
- 'form/kimai-theme.html.twig'
|
||||
|
||||
@@ -9,4 +9,4 @@ services:
|
||||
Twig\Extensions\IntlExtension: ~
|
||||
#Twig\Extensions\TextExtension: ~
|
||||
Twig\Extensions\StringLoader:
|
||||
class: Twig_Extension_StringLoader
|
||||
class: Twig\Extension\StringLoaderExtension
|
||||
@@ -5,21 +5,9 @@ home:
|
||||
route: homepage
|
||||
permanent: true
|
||||
|
||||
homepage:
|
||||
homeLocale:
|
||||
path: /{_locale}
|
||||
defaults:
|
||||
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
|
||||
route: timesheet
|
||||
route: homepage
|
||||
permanent: true
|
||||
|
||||
fos_user_security:
|
||||
prefix: /{_locale}
|
||||
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
|
||||
|
||||
fos_user_registration:
|
||||
prefix: /{_locale}/register
|
||||
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
|
||||
|
||||
fos_user_resetting:
|
||||
prefix: /{_locale}/resetting
|
||||
resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
|
||||
|
||||
@@ -5,4 +5,9 @@ controllers:
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
_locale: '%locale%'
|
||||
|
||||
app.api:
|
||||
resource: '../src/API/'
|
||||
type: rest
|
||||
prefix: /api
|
||||
|
||||
12
config/routes/fos_user.yaml
Normal file
12
config/routes/fos_user.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# Expose security related features like login and logout
|
||||
fos_user_security:
|
||||
prefix: /{_locale}
|
||||
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
|
||||
|
||||
# The features "user registration" and "password-reset" are enabled by default.
|
||||
#
|
||||
# You can disable them by setting the config keys in file "config/kimai.yaml":
|
||||
# - kimai.user.registration: false
|
||||
# - kimai.user.password_reset: false
|
||||
#
|
||||
# The routes for these functions are added dynamically in src/Kernel.php
|
||||
11
config/routes/nelmio_api_doc.yaml
Normal file
11
config/routes/nelmio_api_doc.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Expose the Kimai API documentation as HTML
|
||||
app.swagger_ui:
|
||||
path: /api/doc
|
||||
methods: GET
|
||||
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
|
||||
|
||||
# Expose the Kimai API documentation as swagger compliant JSON
|
||||
app.swagger:
|
||||
path: /api/doc.json
|
||||
methods: GET
|
||||
defaults: { _controller: nelmio_api_doc.controller.swagger }
|
||||
15
config/routes/test/test_routes.yaml
Normal file
15
config/routes/test/test_routes.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
app.test.activity_recent:
|
||||
path: /{_locale}/activities/recent
|
||||
controller: App\Controller\NavbarController::recentActivitiesAction
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
|
||||
app.test.sidebar_settings:
|
||||
path: /{_locale}/sidebar/settings
|
||||
controller: App\Controller\SidebarController::settingsAction
|
||||
requirements:
|
||||
_locale: '%app_locales%'
|
||||
defaults:
|
||||
_locale: '%locale%'
|
||||
32
config/serializer/App/API.Model.I18n.yml
Normal file
32
config/serializer/App/API.Model.I18n.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
App\API\Model\I18n:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [formDateTime, formDate, dateTime, date, time, duration, is24hours]
|
||||
properties:
|
||||
formDateTime:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
formDate:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
dateTime:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
time:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
date:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
duration:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
is24hours:
|
||||
include: true
|
||||
type: boolean
|
||||
groups: [Default]
|
||||
25
config/serializer/App/API.Model.Version.yml
Normal file
25
config/serializer/App/API.Model.Version.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
App\API\Model\Version:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [version, candidate, semver, name, copyright]
|
||||
properties:
|
||||
version:
|
||||
include: true
|
||||
type: string
|
||||
example: 0.9
|
||||
groups: [Default]
|
||||
candidate:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
semver:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
name:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
copyright:
|
||||
include: true
|
||||
type: string
|
||||
groups: [Default]
|
||||
32
config/serializer/App/Entity.Activity.yml
Normal file
32
config/serializer/App/Entity.Activity.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
App\Entity\Activity:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, comment, visible, project, fixedRate, hourlyRate]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
name:
|
||||
include: true
|
||||
groups: [Default]
|
||||
comment:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
visible:
|
||||
include: true
|
||||
groups: [Default]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
project:
|
||||
include: false
|
||||
exclude: true
|
||||
groups: [Default]
|
||||
virtual_properties:
|
||||
getProject:
|
||||
serialized_name: project
|
||||
exp: "object.getProject() === null ? null : object.getProject().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
58
config/serializer/App/Entity.Customer.yml
Normal file
58
config/serializer/App/Entity.Customer.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
App\Entity\Customer:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, 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]
|
||||
email:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
homepage:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
timezone:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
38
config/serializer/App/Entity.Project.yml
Normal file
38
config/serializer/App/Entity.Project.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
App\Entity\Project:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, name, comment, visible, budget, orderNumber, customer, fixedRate, hourlyRate]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
name:
|
||||
include: true
|
||||
groups: [Default]
|
||||
comment:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
visible:
|
||||
include: true
|
||||
groups: [Default]
|
||||
budget:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
orderNumber:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
customer:
|
||||
include: false
|
||||
exclude: true
|
||||
groups: [Default]
|
||||
virtual_properties:
|
||||
getCustomer:
|
||||
serialized_name: customer
|
||||
exp: "object.getCustomer() === null ? null : object.getCustomer().getId()"
|
||||
type: integer
|
||||
groups: [Default]
|
||||
61
config/serializer/App/Entity.Timesheet.yml
Normal file
61
config/serializer/App/Entity.Timesheet.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
App\Entity\Timesheet:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, begin, end, duration, rate, activity, project, user, description, fixedRate, hourlyRate, exported]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
begin:
|
||||
exclude: true
|
||||
end:
|
||||
exclude: true
|
||||
duration:
|
||||
include: true
|
||||
groups: [Default]
|
||||
description:
|
||||
include: true
|
||||
groups: [Default]
|
||||
rate:
|
||||
include: true
|
||||
groups: [Default]
|
||||
fixedRate:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
hourlyRate:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
exported:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
activity:
|
||||
exclude: true
|
||||
project:
|
||||
exclude: true
|
||||
user:
|
||||
exclude: true
|
||||
virtual_properties:
|
||||
getBegin:
|
||||
serialized_name: begin
|
||||
exp: "object.getBegin() === null ? null : object.getBegin()"
|
||||
type: DateTime
|
||||
groups: [Default]
|
||||
getEnd:
|
||||
serialized_name: end
|
||||
exp: "object.getEnd() === null ? null : object.getEnd()"
|
||||
type: DateTime
|
||||
groups: [Default]
|
||||
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]
|
||||
27
config/serializer/App/Entity.User.yml
Normal file
27
config/serializer/App/Entity.User.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
App\Entity\User:
|
||||
exclusion_policy: All
|
||||
custom_accessor_order: [id, alias, title, avatar, language, timezone]
|
||||
properties:
|
||||
id:
|
||||
include: true
|
||||
groups: [Default]
|
||||
alias:
|
||||
include: true
|
||||
groups: [Default]
|
||||
title:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
avatar:
|
||||
include: true
|
||||
groups: [Entity]
|
||||
virtual_properties:
|
||||
getLanguage:
|
||||
serialized_name: language
|
||||
exp: "object.getPreferenceValue('language') === null ? null : object.getPreferenceValue('language')"
|
||||
type: string
|
||||
groups: [Entity]
|
||||
getTimezone:
|
||||
serialized_name: timezone
|
||||
exp: "object.getPreferenceValue('timezone') === null ? null : object.getPreferenceValue('timezone')"
|
||||
type: string
|
||||
groups: [Entity]
|
||||
35
config/serializer/FOS/UserBundle/Model.User.yml
Normal file
35
config/serializer/FOS/UserBundle/Model.User.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
FOS\UserBundle\Model\User:
|
||||
exclusion_policy: All
|
||||
properties:
|
||||
username:
|
||||
include: true
|
||||
groups: [Default]
|
||||
enabled:
|
||||
include: true
|
||||
groups: [Default]
|
||||
roles:
|
||||
type: array<string>
|
||||
include: true
|
||||
groups: [Entity]
|
||||
groups:
|
||||
exclude: true
|
||||
email:
|
||||
exclude: true
|
||||
emailCanonical:
|
||||
exclude: true
|
||||
usernameCanonical:
|
||||
exclude: true
|
||||
password:
|
||||
exclude: true
|
||||
plainPassword:
|
||||
exclude: true
|
||||
registeredAt:
|
||||
exclude: true
|
||||
lastLogin:
|
||||
exclude: true
|
||||
confirmationToken:
|
||||
exclude: true
|
||||
passwordRequestedAt:
|
||||
exclude: true
|
||||
salt:
|
||||
exclude: true
|
||||
@@ -2,7 +2,7 @@
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: en|de|ru
|
||||
app_locales: en|de|it|fr|es|ru|ar|hu|pt_BR|sv
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@@ -14,13 +14,12 @@ services:
|
||||
# The best practice is to be explicit about your dependencies anyway.
|
||||
bind:
|
||||
$projectDirectory: "%kernel.project_dir%"
|
||||
$durationOnly: "%kimai.timesheet.duration_only%"
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/*'
|
||||
exclude: '../src/{Entity,Migrations,Tests}'
|
||||
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Constants.php}'
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
@@ -32,63 +31,89 @@ services:
|
||||
# APPLICATION CORE
|
||||
# ================================================================================
|
||||
|
||||
App\Invoice\ServiceInvoice:
|
||||
class: App\Invoice\ServiceInvoice
|
||||
arguments: ['%kimai.invoice%']
|
||||
|
||||
# a route listener, that injects the default locale into the route
|
||||
App\EventSubscriber\RedirectToLocaleSubscriber:
|
||||
class: App\EventSubscriber\RedirectToLocaleSubscriber
|
||||
arguments: ['@router', '%app_locales%', '%locale%']
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
|
||||
|
||||
App\Calendar\Service:
|
||||
arguments:
|
||||
$config: "%kimai.calendar%"
|
||||
|
||||
App\Repository\WidgetRepository:
|
||||
arguments:
|
||||
$widgets: "%kimai.widgets%"
|
||||
|
||||
App\Controller\DashboardController:
|
||||
arguments:
|
||||
$dashboard: "%kimai.dashboard%"
|
||||
|
||||
App\Configuration\LanguageFormattings:
|
||||
arguments: ["%kimai.languages%"]
|
||||
|
||||
App\Configuration\TimesheetConfiguration:
|
||||
arguments:
|
||||
$settings: "%kimai.timesheet%"
|
||||
|
||||
App\Configuration\FormConfiguration:
|
||||
arguments:
|
||||
$settings: "%kimai.defaults%"
|
||||
|
||||
App\Configuration\SystemConfiguration:
|
||||
arguments:
|
||||
$settings: "%kimai.config%"
|
||||
|
||||
App\Utils\MPdfConverter:
|
||||
arguments: ['%kernel.cache_dir%']
|
||||
|
||||
App\Plugin\PluginManager:
|
||||
arguments: [!tagged kimai.plugin]
|
||||
|
||||
App\Export\ServiceExport:
|
||||
arguments: [!tagged export.renderer]
|
||||
|
||||
# ================================================================================
|
||||
# DATABASE
|
||||
# ================================================================================
|
||||
|
||||
# service that prefixes every database table
|
||||
App\Doctrine\TablePrefixSubscriber:
|
||||
class: App\Doctrine\TablePrefixSubscriber
|
||||
arguments: ["%env(resolve:DATABASE_PREFIX)%"]
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber }
|
||||
|
||||
# updates timesheet records and apply configured rate & rounding rules
|
||||
App\Doctrine\TimesheetSubscriber:
|
||||
class: App\Doctrine\TimesheetSubscriber
|
||||
arguments: [!tagged timesheet.calculator]
|
||||
tags:
|
||||
- { name: doctrine.event_listener, event: prePersist, lazy: true }
|
||||
- { name: doctrine.event_listener, event: preUpdate, lazy: true }
|
||||
- { name: doctrine.event_subscriber, priority: 50 }
|
||||
|
||||
# make sure, that sqlite supports foreign keys and cascade deletes
|
||||
App\Doctrine\SqliteSessionInitSubscriber:
|
||||
class: App\Doctrine\SqliteSessionInitSubscriber
|
||||
tags:
|
||||
- { name: doctrine.event_listener, event: postConnect }
|
||||
|
||||
# ================================================================================
|
||||
# FORMS
|
||||
# ================================================================================
|
||||
|
||||
# form type to edit user roles
|
||||
App\Form\Type\UserRoleType:
|
||||
class: App\Form\Type\UserRoleType
|
||||
arguments: ["%security.role_hierarchy.roles%"]
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
App\Form\Type\LanguageType:
|
||||
arguments: ["%app_locales%"]
|
||||
|
||||
App\Form\Extension\DocumentationLinkExtension:
|
||||
tags:
|
||||
- { name: form.type_extension, extended_type: Symfony\Component\Form\Extension\Core\Type\FormType }
|
||||
|
||||
App\Form\Extension\EnhancedChoiceTypeExtension:
|
||||
arguments: ["%kimai.theme.select_type%"]
|
||||
tags:
|
||||
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
||||
|
||||
App\Form\Extension\SelectWithApiDataExtension:
|
||||
tags:
|
||||
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
||||
|
||||
# ================================================================================
|
||||
# THEME
|
||||
# ================================================================================
|
||||
|
||||
App\Twig\Extensions:
|
||||
arguments:
|
||||
$locales: "%app_locales%"
|
||||
|
||||
App\Twig\DateExtensions:
|
||||
arguments:
|
||||
$dateSettings: "%kimai.languages%"
|
||||
|
||||
KevinPapst\AdminLTEBundle\Helper\ContextHelper:
|
||||
alias: admin_lte_theme.context_helper
|
||||
|
||||
@@ -101,3 +126,53 @@ services:
|
||||
|
||||
App\Timesheet\Calculator\RateCalculator:
|
||||
arguments: ["%kimai.timesheet.rates%"]
|
||||
|
||||
# ================================================================================
|
||||
# SECURITY & VOTER
|
||||
# ================================================================================
|
||||
|
||||
App\Security\RolePermissionManager:
|
||||
arguments: ["%kimai.permissions%"]
|
||||
|
||||
# ================================================================================
|
||||
# REPOSITORIES
|
||||
# ================================================================================
|
||||
|
||||
App\Repository\TimesheetRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Timesheet']
|
||||
|
||||
App\Repository\UserRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\User']
|
||||
|
||||
App\Repository\ActivityRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Activity']
|
||||
|
||||
App\Repository\ProjectRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Project']
|
||||
|
||||
App\Repository\CustomerRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Customer']
|
||||
|
||||
App\Repository\InvoiceTemplateRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\InvoiceTemplate']
|
||||
|
||||
App\Repository\ConfigurationRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Configuration']
|
||||
|
||||
App\Repository\InvoiceDocumentRepository:
|
||||
class: App\Repository\InvoiceDocumentRepository
|
||||
arguments: ['%kimai.invoice.documents%']
|
||||
|
||||
@@ -2,8 +2,12 @@ services:
|
||||
_defaults:
|
||||
public: true
|
||||
|
||||
# If you need to access services in a test, create an alias
|
||||
# and then fetch that alias from the container. As a convention,
|
||||
# aliases are prefixed with test. For example:
|
||||
#
|
||||
# test.App\Service\MyService: '@App\Service\MyService'
|
||||
test.PasswordEncoder: "@security.encoder_factory"
|
||||
|
||||
App\Configuration\SystemConfiguration:
|
||||
arguments: ['@App\Repository\ConfigurationRepository', "%kimai.config%"]
|
||||
|
||||
App\Repository\ConfigurationRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Configuration']
|
||||
|
||||
@@ -4,7 +4,7 @@ App\Entity\User:
|
||||
- App\Validator\Constraints\Role: ~
|
||||
username:
|
||||
- NotBlank: ~
|
||||
- Length: { min: 5, max: 60 }
|
||||
- Length: { min: 3, max: 60 }
|
||||
email:
|
||||
- NotBlank: ~
|
||||
- Email: ~
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "~5.1.1",
|
||||
"@symfony/webpack-encore": "^0.20.1",
|
||||
"admin-lte": "~2.3.11",
|
||||
"admin-lte": "~2.4.8",
|
||||
"bootstrap-sass": "^3.3.7",
|
||||
"bootstrap-select": "^1.13.1",
|
||||
"chart.js": "1.1.*",
|
||||
"chart.js": "~2.7.2",
|
||||
"daterangepicker": "^3.0.3",
|
||||
"fullcalendar": "^3.9.0",
|
||||
"icheck": "^1.0.2",
|
||||
"jquery": "^3.3.1",
|
||||
"jquery-slimscroll": "^1.3.8",
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
<env name="APP_ENV" value="test" force="true"/>
|
||||
<env name="APP_DEBUG" value="1" force="true"/>
|
||||
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
||||
<env name="DATABASE_PREFIX" value="kimai2_" force="true"/>
|
||||
<!-- define your env variables for the test env here -->
|
||||
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
||||
|
||||
<!-- ###+ doctrine/doctrine-bundle ### -->
|
||||
<env name="DATABASE_URL" value="sqlite:///var/data/kimai_test.sqlite" force="true"/>
|
||||
<!-- ###- doctrine/doctrine-bundle ### -->
|
||||
@@ -24,6 +25,10 @@
|
||||
<!-- ###+ symfony/swiftmailer-bundle ### -->
|
||||
<env name="MAILER_URL" value="null://localhost" force="true"/>
|
||||
<!-- ###- symfony/swiftmailer-bundle ### -->
|
||||
|
||||
<!-- ###+ nelmio/cors-bundle ### -->
|
||||
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
||||
<!-- ###- nelmio/cors-bundle ### -->
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
@@ -37,9 +42,11 @@
|
||||
<directory suffix=".php">src/</directory>
|
||||
<directory suffix=".php">templates/</directory>
|
||||
<exclude>
|
||||
<directory suffix=".php">src/Migrations/</directory>
|
||||
<directory suffix=".php">assets/</directory>
|
||||
<directory suffix=".php">bin/</directory>
|
||||
<directory suffix=".php">config/</directory>
|
||||
<directory suffix=".php">node_modules/</directory>
|
||||
<directory suffix=".php">public/</directory>
|
||||
<directory suffix=".php">tests/</directory>
|
||||
<directory suffix=".php">translations/</directory>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# mod_rewrite). Additionally, this reduces the matching process for the
|
||||
# start page (path "/") because otherwise Apache will apply the rewriting rules
|
||||
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
|
||||
DirectoryIndex app.php
|
||||
DirectoryIndex index.php
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
@@ -30,7 +30,7 @@ DirectoryIndex app.php
|
||||
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
|
||||
# following RewriteCond (best solution)
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} ^$
|
||||
RewriteRule ^app\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
|
||||
RewriteRule ^index\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]
|
||||
|
||||
# If the requested filename exists, simply serve it.
|
||||
# We only want to let Apache serve files and not directories.
|
||||
@@ -38,7 +38,7 @@ DirectoryIndex app.php
|
||||
RewriteRule .? - [L]
|
||||
|
||||
# Rewrite all other queries to the front controller.
|
||||
RewriteRule .? %{ENV:BASE}/app.php [L]
|
||||
RewriteRule .? %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_rewrite.c>
|
||||
@@ -46,7 +46,7 @@ DirectoryIndex app.php
|
||||
# When mod_rewrite is not available, we instruct a temporary redirect of
|
||||
# the start page to the front controller explicitly so that the website
|
||||
# and the generated links can still be used.
|
||||
RedirectMatch 302 ^/$ /app.php/
|
||||
RedirectMatch 302 ^/$ /index.php/
|
||||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB |
BIN
public/build/images/signature.png
Normal file
BIN
public/build/images/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,28 +1,26 @@
|
||||
{
|
||||
"build/app.js": "/build/app.js?df75851c817558ca8803",
|
||||
"build/app.css": "/build/app.css?b1944e8d2023603075ed8d913c21a8e8",
|
||||
"build/fonts/fa-solid-900.woff": "/build/fonts/fa-solid-900.woff?dfc040d5",
|
||||
"build/images/boxed-bg.jpg": "/build/images/boxed-bg.jpg?7799dece",
|
||||
"build/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",
|
||||
"build/fonts/glyphicons-halflings-regular.eot": "/build/fonts/glyphicons-halflings-regular.eot?f4769f9b",
|
||||
"build/fonts/fa-solid-900.ttf": "/build/fonts/fa-solid-900.ttf?a7a790d4",
|
||||
"build/fonts/fa-brands-400.eot": "/build/fonts/fa-brands-400.eot?69b310ed",
|
||||
"build/fonts/fa-regular-400.eot": "/build/fonts/fa-regular-400.eot?0b7928f6",
|
||||
"build/fonts/fa-brands-400.woff": "/build/fonts/fa-brands-400.woff?e9f83339",
|
||||
"build/fonts/glyphicons-halflings-regular.woff2": "/build/fonts/glyphicons-halflings-regular.woff2?448c34a5",
|
||||
"build/fonts/fa-solid-900.eot": "/build/fonts/fa-solid-900.eot?0b93480e",
|
||||
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||
"build/fonts/fa-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",
|
||||
"build/fonts/fa-regular-400.ttf": "/build/fonts/fa-regular-400.ttf?cdd8fa76",
|
||||
"build/fonts/fa-brands-400.ttf": "/build/fonts/fa-brands-400.ttf?d94572dd",
|
||||
"build/fonts/glyphicons-halflings-regular.woff": "/build/fonts/glyphicons-halflings-regular.woff?fa277232",
|
||||
"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/app.js": "./app.js?[contenthash]",
|
||||
"build/app.css": "./app.css?996ae1142414de4354740600f29d7f64",
|
||||
"build/fonts/fa-solid-900.woff2": "./fonts/fa-solid-900.woff2?e8a92a29",
|
||||
"build/images/fa-solid-900.svg": "./images/fa-solid-900.svg?666a82cb",
|
||||
"build/images/glyphicons-halflings-regular.svg": "./images/glyphicons-halflings-regular.svg?89889688",
|
||||
"build/fonts/glyphicons-halflings-regular.eot": "./fonts/glyphicons-halflings-regular.eot?f4769f9b",
|
||||
"build/fonts/fa-solid-900.ttf": "./fonts/fa-solid-900.ttf?a7a790d4",
|
||||
"build/fonts/fa-brands-400.eot": "./fonts/fa-brands-400.eot?69b310ed",
|
||||
"build/fonts/fa-regular-400.eot": "./fonts/fa-regular-400.eot?0b7928f6",
|
||||
"build/fonts/fa-brands-400.woff": "./fonts/fa-brands-400.woff?e9f83339",
|
||||
"build/fonts/glyphicons-halflings-regular.woff2": "./fonts/glyphicons-halflings-regular.woff2?448c34a5",
|
||||
"build/fonts/fa-solid-900.eot": "./fonts/fa-solid-900.eot?0b93480e",
|
||||
"build/fonts/fa-solid-900.woff": "./fonts/fa-solid-900.woff?dfc040d5",
|
||||
"build/fonts/fa-regular-400.woff": "./fonts/fa-regular-400.woff?d9e29124",
|
||||
"build/fonts/fa-regular-400.woff2": "./fonts/fa-regular-400.woff2?930c1264",
|
||||
"build/images/fa-regular-400.svg": "./images/fa-regular-400.svg?4e41caaf",
|
||||
"build/fonts/fa-regular-400.ttf": "./fonts/fa-regular-400.ttf?cdd8fa76",
|
||||
"build/fonts/fa-brands-400.ttf": "./fonts/fa-brands-400.ttf?d94572dd",
|
||||
"build/fonts/glyphicons-halflings-regular.woff": "./fonts/glyphicons-halflings-regular.woff?fa277232",
|
||||
"build/fonts/fa-brands-400.woff2": "./fonts/fa-brands-400.woff2?66f625f1",
|
||||
"build/fonts/glyphicons-halflings-regular.ttf": "./fonts/glyphicons-halflings-regular.ttf?e18bbf61",
|
||||
"build/images/fa-brands-400.svg": "./images/fa-brands-400.svg?5da7e33c",
|
||||
"build/images/default_avatar.png": "./images/default_avatar.png?fc197350",
|
||||
"build/images/signature.png": "./images/signature.png?5f58501e"
|
||||
}
|
||||
267
src/API/ActivityController.php
Normal file
267
src/API/ActivityController.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?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\Activity;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* @RouteResource("Activity")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class ActivityController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param ActivityRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ActivityRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of activities
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns a collection of activity entities",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref="#/definitions/ActivityCollection")
|
||||
* )
|
||||
* )
|
||||
* @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter activities. If none is provided, all activities will be returned.")
|
||||
* @Rest\QueryParam(name="visible", requirements="1|2|3", strict=true, nullable=true, description="Visibility status to filter activities. Allowed values: 1=visible, 2=hidden, 3=all (default: 1)")
|
||||
* @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Use if you want to fetch only global activities. Allowed values: true (default: false)")
|
||||
* @Rest\QueryParam(name="globalsFirst", requirements="false", strict=true, nullable=true, description="Use if you don't want global activities to be listed first. Allowed values: false (default: true)")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|name|project", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, name, project (default: name)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: ASC)")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setOrderGlobalsFirst(true)
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
if (null !== ($orderBy = $paramFetcher->get('orderBy'))) {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
if (null !== $paramFetcher->get('globals')) {
|
||||
$query->setGlobalsOnly(true);
|
||||
}
|
||||
|
||||
if ('false' === $paramFetcher->get('globalsFirst')) {
|
||||
$query->setOrderGlobalsFirst(false);
|
||||
}
|
||||
|
||||
if (null !== ($project = $paramFetcher->get('project'))) {
|
||||
$query->setProject($project);
|
||||
}
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one activity
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one activity entity",
|
||||
* @SWG\Schema(ref="#/definitions/ActivityEntity"),
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Activity ID to fetch",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$data = $this->repository->find($id);
|
||||
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new activity
|
||||
*
|
||||
* @SWG\Post(
|
||||
* description="Creates a new activity and returns it afterwards",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the new created activity",
|
||||
* @SWG\Schema(ref="#/definitions/ActivityEntity"),
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/ActivityEditForm")
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \App\Repository\RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function postAction(Request $request)
|
||||
{
|
||||
if (!$this->isGranted('create_activity')) {
|
||||
throw new AccessDeniedHttpException('User cannot create activities');
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
|
||||
$form = $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View($activity, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$view = new View($form);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing activity
|
||||
*
|
||||
* @SWG\Patch(
|
||||
* description="Update an existing activity, you can pass all or just a subset of all attributes",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the updated activity",
|
||||
* @SWG\Schema(ref="#/definitions/ActivityEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/ActivityEditForm")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Activity ID to update",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $id
|
||||
* @return Response
|
||||
*/
|
||||
public function patchAction(Request $request, string $id)
|
||||
{
|
||||
$activity = $this->repository->find($id);
|
||||
|
||||
if (null === $activity) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $activity)) {
|
||||
throw new AccessDeniedHttpException('User cannot update activity');
|
||||
}
|
||||
|
||||
$form = $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
|
||||
$form->setData($activity);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
if (false === $form->isValid()) {
|
||||
$view = new View($form, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($activity);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View($activity, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Activity']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
21
src/API/BaseApiController.php
Normal file
21
src/API/BaseApiController.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
|
||||
abstract class BaseApiController extends AbstractController
|
||||
{
|
||||
public const DATE_FORMAT = DateTimeType::HTML5_FORMAT;
|
||||
public const DATE_FORMAT_PHP = 'Y-m-d\TH:m:s';
|
||||
}
|
||||
80
src/API/ConfigurationController.php
Normal file
80
src/API/ConfigurationController.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?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\API\Model\I18n;
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Entity\User;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
|
||||
/**
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class ConfigurationController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
/**
|
||||
* @var LanguageFormattings
|
||||
*/
|
||||
protected $formats;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param LanguageFormattings $formats
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, LanguageFormattings $formats)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->formats = $formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user specific locale configuration
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the locale specific configurations for this user",
|
||||
* @SWG\Schema(ref="#/definitions/I18nConfig")
|
||||
* )
|
||||
*
|
||||
* @Rest\Get(path="/config/i18n")
|
||||
*/
|
||||
public function i18nAction()
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$locale = $user->getLocale();
|
||||
|
||||
$model = new I18n();
|
||||
$model
|
||||
->setFormDateTime($this->formats->getDateTimeTypeFormat($locale))
|
||||
->setFormDate($this->formats->getDateTypeFormat($locale))
|
||||
->setDateTime($this->formats->getDateTimeFormat($locale))
|
||||
->setDate($this->formats->getDateFormat($locale))
|
||||
->setDuration($this->formats->getDurationFormat($locale))
|
||||
->setTime($this->formats->getTimeFormat($locale))
|
||||
->setIs24hours($this->formats->isTwentyFourHours($locale))
|
||||
;
|
||||
|
||||
$view = new View($model, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Config']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
245
src/API/CustomerController.php
Normal file
245
src/API/CustomerController.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?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\Customer;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* @RouteResource("Customer")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class CustomerController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param CustomerRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, CustomerRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of customers
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns a collection of customer entities",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref="#/definitions/CustomerCollection")
|
||||
* )
|
||||
* )
|
||||
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter activities (1=visible, 2=hidden, 3=both)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: ASC)")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|name", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, name (default: name)")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query
|
||||
->setResultType(CustomerQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
if (null !== ($orderBy = $paramFetcher->get('orderBy'))) {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one customer
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one customer entity",
|
||||
* @SWG\Schema(ref="#/definitions/CustomerEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$data = $this->repository->find($id);
|
||||
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new customer
|
||||
*
|
||||
* @SWG\Post(
|
||||
* description="Creates a new customer and returns it afterwards",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the new created customer",
|
||||
* @SWG\Schema(ref="#/definitions/CustomerEntity"),
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/CustomerEditForm")
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \App\Repository\RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function postAction(Request $request)
|
||||
{
|
||||
if (!$this->isGranted('create_customer')) {
|
||||
throw new AccessDeniedHttpException('User cannot create customers');
|
||||
}
|
||||
|
||||
$customer = new Customer();
|
||||
|
||||
$form = $this->createForm(CustomerEditForm::class, $customer, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View($customer, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$view = new View($form);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing customer
|
||||
*
|
||||
* @SWG\Patch(
|
||||
* description="Update an existing customer, you can pass all or just a subset of all attributes",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the updated customer",
|
||||
* @SWG\Schema(ref="#/definitions/CustomerEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/CustomerEditForm")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Customer ID to update",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $id
|
||||
* @return Response
|
||||
*/
|
||||
public function patchAction(Request $request, string $id)
|
||||
{
|
||||
$customer = $this->repository->find($id);
|
||||
|
||||
if (null === $customer) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $customer)) {
|
||||
throw new AccessDeniedHttpException('User cannot update customer');
|
||||
}
|
||||
|
||||
$form = $this->createForm(CustomerEditForm::class, $customer, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
|
||||
$form->setData($customer);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
if (false === $form->isValid()) {
|
||||
$view = new View($form, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($customer);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View($customer, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Customer']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
190
src/API/Model/I18n.php
Normal file
190
src/API/Model/I18n.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?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\Model;
|
||||
|
||||
class I18n
|
||||
{
|
||||
/**
|
||||
* Format used for 'begin' and 'end'
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $formDateTime = '';
|
||||
/**
|
||||
* Format used for toolbar queries
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $formDate = '';
|
||||
/**
|
||||
* Format used to display date-time values (see PHP function date_format)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dateTime = '';
|
||||
/**
|
||||
* Format used to display date values (see PHP function date_format)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $date = '';
|
||||
/**
|
||||
* Format used to display times (see PHP function date_format)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $time = '';
|
||||
/**
|
||||
* Format used to display durations (replace: %h with hours, %m with minutes, %s with seconds)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $duration = '';
|
||||
/**
|
||||
* Whether a twenty-four hour format is used (true) or 12-hours AM/PM format (false)
|
||||
* @var bool
|
||||
*/
|
||||
protected $is24hours = true;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFormDateTime(): string
|
||||
{
|
||||
return $this->formDateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $formDateTime
|
||||
* @return I18n
|
||||
*/
|
||||
public function setFormDateTime(string $formDateTime)
|
||||
{
|
||||
$this->formDateTime = $formDateTime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFormDate(): string
|
||||
{
|
||||
return $this->formDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $formDate
|
||||
* @return I18n
|
||||
*/
|
||||
public function setFormDate(string $formDate)
|
||||
{
|
||||
$this->formDate = $formDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDateTime(): string
|
||||
{
|
||||
return $this->dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dateTime
|
||||
* @return I18n
|
||||
*/
|
||||
public function setDateTime(string $dateTime)
|
||||
{
|
||||
$this->dateTime = $dateTime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDate(): string
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $date
|
||||
* @return I18n
|
||||
*/
|
||||
public function setDate(string $date)
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDuration(): string
|
||||
{
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $duration
|
||||
* @return I18n
|
||||
*/
|
||||
public function setDuration(string $duration)
|
||||
{
|
||||
$this->duration = $duration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTime(): string
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $time
|
||||
* @return I18n
|
||||
*/
|
||||
public function setTime(string $time)
|
||||
{
|
||||
$this->time = $time;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isIs24hours(): bool
|
||||
{
|
||||
return $this->is24hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $is24hours
|
||||
* @return I18n
|
||||
*/
|
||||
public function setIs24hours(bool $is24hours)
|
||||
{
|
||||
$this->is24hours = $is24hours;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
38
src/API/Model/Version.php
Normal file
38
src/API/Model/Version.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Model;
|
||||
|
||||
use App\Constants;
|
||||
|
||||
class Version
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $version = Constants::VERSION;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $candidate = Constants::STATUS;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $semver = Constants::VERSION . '-' . Constants::STATUS;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name = Constants::NAME;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $copyright = Constants::SOFTWARE . ' - ' . Constants::VERSION . ' ' . Constants::STATUS . ' (' . Constants::NAME . ') by Kevin Papst and contributors.';
|
||||
}
|
||||
22
src/API/NotFoundException.php
Normal file
22
src/API/NotFoundException.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class NotFoundException extends NotFoundHttpException
|
||||
{
|
||||
public function __construct(string $message = 'Not found', \Exception $previous = null, int $code = 0, array $headers = [])
|
||||
{
|
||||
parent::__construct($message, $previous, $code, $headers);
|
||||
}
|
||||
}
|
||||
249
src/API/ProjectController.php
Normal file
249
src/API/ProjectController.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?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\Project;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* @RouteResource("Project")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class ProjectController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param ProjectRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of projects
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns a collection of project entities",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref="#/definitions/ProjectCollection")
|
||||
* )
|
||||
* )
|
||||
* @Rest\QueryParam(name="customer", requirements="\d+", strict=true, nullable=true, description="Customer ID to filter projects")
|
||||
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter projects (1=visible, 2=hidden, 3=both)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: ASC)")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|name|customer", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, name, customer (default: name)")
|
||||
*
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query
|
||||
->setResultType(ProjectQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('name')
|
||||
;
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
if (null !== ($orderBy = $paramFetcher->get('orderBy'))) {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
if (null !== ($customer = $paramFetcher->get('customer'))) {
|
||||
$query->setCustomer($customer);
|
||||
}
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one project
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one project entity",
|
||||
* @SWG\Schema(ref="#/definitions/ProjectEntity"),
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$data = $this->repository->find($id);
|
||||
if (null === $data) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new project
|
||||
*
|
||||
* @SWG\Post(
|
||||
* description="Creates a new project and returns it afterwards",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the new created project",
|
||||
* @SWG\Schema(ref="#/definitions/ProjectEntity"),
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/ProjectEditForm")
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \App\Repository\RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function postAction(Request $request)
|
||||
{
|
||||
if (!$this->isGranted('create_project')) {
|
||||
throw new AccessDeniedHttpException('User cannot create projects');
|
||||
}
|
||||
|
||||
$project = new Project();
|
||||
|
||||
$form = $this->createForm(ProjectEditForm::class, $project, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($project);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View($project, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$view = new View($form);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing project
|
||||
*
|
||||
* @SWG\Patch(
|
||||
* description="Update an existing project, you can pass all or just a subset of all attributes",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the updated project",
|
||||
* @SWG\Schema(ref="#/definitions/ProjectEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/ProjectEditForm")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Project ID to update",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $id
|
||||
* @return Response
|
||||
*/
|
||||
public function patchAction(Request $request, string $id)
|
||||
{
|
||||
$project = $this->repository->find($id);
|
||||
|
||||
if (null === $project) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $project)) {
|
||||
throw new AccessDeniedHttpException('User cannot update project');
|
||||
}
|
||||
|
||||
$form = $this->createForm(ProjectEditForm::class, $project, [
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
|
||||
$form->setData($project);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
if (false === $form->isValid()) {
|
||||
$view = new View($form, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($project);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View($project, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Project']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
69
src/API/StatusController.php
Normal file
69
src/API/StatusController.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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\API\Model\Version;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Swagger\Annotations as SWG;
|
||||
|
||||
class StatusController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* A testing route for the API
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="A simple route that returns a 'pong', which you can use for testing the API",
|
||||
* examples={"{'message': 'pong'}"}
|
||||
* )
|
||||
*
|
||||
* @Rest\Get(path="/ping")
|
||||
*/
|
||||
public function pingAction()
|
||||
{
|
||||
$view = new View(['message' => 'pong'], 200);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about the Kimai release
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns version information about the current release",
|
||||
* @SWG\Schema(ref=@Model(type=Version::class))
|
||||
* )
|
||||
*
|
||||
* @Rest\Get(path="/version")
|
||||
*/
|
||||
public function versionAction()
|
||||
{
|
||||
return $this->viewHandler->handle(new View(new Version(), 200));
|
||||
}
|
||||
}
|
||||
395
src/API/TimesheetController.php
Normal file
395
src/API/TimesheetController.php
Normal file
@@ -0,0 +1,395 @@
|
||||
<?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\Configuration\TimesheetConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
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;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
|
||||
/**
|
||||
* @RouteResource("Timesheet")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class TimesheetController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
/**
|
||||
* @var TimesheetConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
protected $dateTime;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param TimesheetRepository $repository
|
||||
* @param UserDateTimeFactory $dateTime
|
||||
* @param TimesheetConfiguration $configuration
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dateTime = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of timesheet records
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns a collection of timesheets records. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref="#/definitions/TimesheetCollection")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Rest\QueryParam(name="user", requirements="\d+|all", strict=true, nullable=true, description="User ID to filter timesheets. Needs permission 'view_other_timesheet', pass 'all' to fetch data for all user (default: current user)")
|
||||
* @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="orderBy", requirements="id|begin|end|rate", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: DESC)")
|
||||
* @Rest\QueryParam(name="begin", requirements=@Constraints\DateTime, strict=true, nullable=true, description="Only records after this date will be included (format: ISO 8601)")
|
||||
* @Rest\QueryParam(name="end", requirements=@Constraints\DateTime, strict=true, nullable=true, description="Only records before this date will be included (format: ISO 8601)")
|
||||
* @Rest\QueryParam(name="exported", requirements="0|1", strict=true, nullable=true, description="Use this flag if you want to filter for export state. Allowed values: 0=not exported, 1=exported (default: all)")
|
||||
* @Rest\QueryParam(name="active", requirements="0|1", strict=true, nullable=true, description="Filter for running/active records. Allowed values: 0=stopped, 1=active. (default: all)")
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new TimesheetQuery();
|
||||
$query->setUser($this->getUser());
|
||||
$query->setResultType(TimesheetQuery::RESULT_TYPE_PAGER);
|
||||
|
||||
if ($this->isGranted('view_other_timesheet') && null !== ($user = $paramFetcher->get('user'))) {
|
||||
if ('all' === $user) {
|
||||
$user = null;
|
||||
}
|
||||
$query->setUser($user);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (null !== ($begin = $paramFetcher->get('begin'))) {
|
||||
$query->setBegin(new \DateTime($begin));
|
||||
}
|
||||
|
||||
if (null !== ($end = $paramFetcher->get('end'))) {
|
||||
$query->setEnd(new \DateTime($end));
|
||||
}
|
||||
|
||||
if (null !== ($active = $paramFetcher->get('active'))) {
|
||||
$active = (int) $active;
|
||||
if ($active === 1) {
|
||||
$query->setState(TimesheetQuery::STATE_RUNNING);
|
||||
} elseif ($active === 0) {
|
||||
$query->setState(TimesheetQuery::STATE_STOPPED);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== ($exported = $paramFetcher->get('exported'))) {
|
||||
$exported = (int) $exported;
|
||||
if ($exported === 1) {
|
||||
$query->setExported(TimesheetQuery::STATE_EXPORTED);
|
||||
} elseif ($exported === 0) {
|
||||
$query->setExported(TimesheetQuery::STATE_NOT_EXPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
/** @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one timesheet record
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns one timesheet record. Be aware that the datetime fields are given in the users local time including the timezone offset via ISO 8601.",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Timesheet record ID to fetch",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('view_own_timesheet') or is_granted('view_other_timesheet')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$timesheet = $this->repository->find($id);
|
||||
|
||||
if (null === $timesheet) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('view', $timesheet)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to view this timesheet');
|
||||
}
|
||||
|
||||
$view = new View($timesheet, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new timesheet record
|
||||
*
|
||||
* @SWG\Post(
|
||||
* description="Creates a new timesheet record for the current user and returns it afterwards.",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the new created timesheet",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity"),
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEditForm")
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('create_own_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \App\Repository\RepositoryException
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function postAction(Request $request)
|
||||
{
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setUser($this->getUser());
|
||||
$timesheet->setBegin($this->dateTime->createDateTime());
|
||||
|
||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||
'csrf_protection' => false,
|
||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||
'date_format' => self::DATE_FORMAT,
|
||||
]);
|
||||
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
if (null === $timesheet->getEnd()) {
|
||||
if (!$this->isGranted('start', $timesheet)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to start this timesheet record');
|
||||
}
|
||||
$this->repository->stopActiveEntries(
|
||||
$timesheet->getUser(),
|
||||
$this->configuration->getActiveEntriesHardLimit()
|
||||
);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing timesheet record
|
||||
*
|
||||
* @SWG\Patch(
|
||||
* description="Update an existing timesheet record, you can pass all or just a subset of the attributes.",
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the updated timesheet",
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEntity")
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="body",
|
||||
* in="body",
|
||||
* required=true,
|
||||
* @SWG\Schema(ref="#/definitions/TimesheetEditForm")
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Timesheet record ID to update",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int $id the timesheet to update
|
||||
* @return Response
|
||||
*/
|
||||
public function patchAction(Request $request, int $id)
|
||||
{
|
||||
$timesheet = $this->repository->find($id);
|
||||
|
||||
if (null === $timesheet) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('edit', $timesheet)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to update this timesheet');
|
||||
}
|
||||
|
||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||
'csrf_protection' => false,
|
||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||
'date_format' => self::DATE_FORMAT,
|
||||
]);
|
||||
|
||||
$form->setData($timesheet);
|
||||
$form->submit($request->request->all(), false);
|
||||
|
||||
if (false === $form->isValid()) {
|
||||
$view = new View($form, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($timesheet);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View($timesheet, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing timesheet record
|
||||
*
|
||||
* @SWG\Delete(
|
||||
* @SWG\Response(
|
||||
* response=204,
|
||||
* description="Delete one timesheet record"
|
||||
* ),
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Timesheet record ID to delete",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('delete_own_timesheet') or is_granted('delete_other_timesheet')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction($id)
|
||||
{
|
||||
$timesheet = $this->repository->find($id);
|
||||
|
||||
if (null === $timesheet) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('delete', $timesheet)) {
|
||||
throw $this->createAccessDeniedException('You are not allowed to delete this timesheet');
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($timesheet);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
136
src/API/UserController.php
Normal file
136
src/API/UserController.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\UserRepository;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* @RouteResource("User")
|
||||
*
|
||||
* @Security("is_granted('ROLE_USER')")
|
||||
*/
|
||||
class UserController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param UserRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection of all registered users
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all registered users. Required permission: view_user",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref="#/definitions/UserCollection")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Rest\QueryParam(name="visible", requirements="1|2|3", strict=true, nullable=true, description="Visibility status to filter users. Allowed values: 1=visible, 2=hidden, 3=all (default: 1)")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|username|alias|email", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, username, alias, email (default: username)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: ASC)")
|
||||
*
|
||||
* @Security("is_granted('view_user')")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query
|
||||
->setResultType(UserQuery::RESULT_TYPE_OBJECTS)
|
||||
->setOrderBy('username')
|
||||
;
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
if (null !== ($orderBy = $paramFetcher->get('orderBy'))) {
|
||||
$query->setOrderBy($orderBy);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection', 'User']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return one user entity
|
||||
*
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Return one user entity. Required permission: view_user",
|
||||
* @SWG\Schema(ref="#/definitions/UserEntity"),
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="User ID to fetch",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function getAction($id)
|
||||
{
|
||||
$user = $this->repository->find($id);
|
||||
|
||||
if (null === $user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!$this->isGranted('view', $user)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to view this profile');
|
||||
}
|
||||
|
||||
$view = new View($user, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Entity', 'User']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
66
src/Calendar/Config.php
Normal file
66
src/Calendar/Config.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getBusinessDays()
|
||||
{
|
||||
return $this->config['businessHours']['days'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBusinessTimeBegin()
|
||||
{
|
||||
return $this->config['businessHours']['begin'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBusinessTimeEnd()
|
||||
{
|
||||
return $this->config['businessHours']['end'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDayLimit()
|
||||
{
|
||||
return $this->config['day_limit'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isShowWeekNumbers()
|
||||
{
|
||||
return $this->config['week_numbers'];
|
||||
}
|
||||
}
|
||||
48
src/Calendar/Google.php
Normal file
48
src/Calendar/Google.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
class Google
|
||||
{
|
||||
/**
|
||||
* @var Source[]
|
||||
*/
|
||||
protected $sources = [];
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $apiKey = null;
|
||||
|
||||
/**
|
||||
* @param string $apiKey
|
||||
* @param Source[] $sources
|
||||
*/
|
||||
public function __construct($apiKey, $sources = [])
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
$this->sources = $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getSources()
|
||||
{
|
||||
return $this->sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getApiKey()
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
}
|
||||
59
src/Calendar/Service.php
Normal file
59
src/Calendar/Service.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
class Service
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Service constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Config
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return new Config($this->config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Google
|
||||
*/
|
||||
public function getGoogle()
|
||||
{
|
||||
$apiKey = $this->config['google']['api_key'] ?? null;
|
||||
$sources = [];
|
||||
|
||||
if (isset($this->config['google']['sources'])) {
|
||||
foreach ($this->config['google']['sources'] as $name => $config) {
|
||||
$source = new Source();
|
||||
$source
|
||||
->setColor($config['color'])
|
||||
->setUri($config['id'])
|
||||
->setId($name)
|
||||
;
|
||||
|
||||
$sources[] = $source;
|
||||
}
|
||||
}
|
||||
|
||||
return new Google($apiKey, $sources);
|
||||
}
|
||||
}
|
||||
83
src/Calendar/Source.php
Normal file
83
src/Calendar/Source.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
class Source
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $uri;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $color;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return Source
|
||||
*/
|
||||
public function setId(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUri(): ?string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @return Source
|
||||
*/
|
||||
public function setUri(string $uri)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $color
|
||||
* @return Source
|
||||
*/
|
||||
public function setColor(string $color)
|
||||
{
|
||||
$this->color = $color;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
268
src/Calendar/TimesheetEntity.php
Normal file
268
src/Calendar/TimesheetEntity.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
class TimesheetEntity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $id;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
protected $start;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*/
|
||||
protected $end;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $title;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $customer;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $project;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $activity;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $borderColor;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $backgroundColor;
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
*/
|
||||
public function __construct(Timesheet $entry)
|
||||
{
|
||||
$this->id = $entry->getId();
|
||||
$this->start = $entry->getBegin();
|
||||
$this->title = $entry->getActivity()->getName();
|
||||
$this->description = $entry->getDescription();
|
||||
$this->customer = $entry->getProject()->getCustomer()->getName();
|
||||
$this->project = $entry->getProject()->getName();
|
||||
$this->activity = $entry->getActivity()->getName();
|
||||
|
||||
if (null === $entry->getEnd()) {
|
||||
// TODO move these colors to the controller
|
||||
$this->borderColor = '367fa9'; //'#f39c12';
|
||||
$this->backgroundColor = '#3c8dbc'; //'#f39c12';
|
||||
} else {
|
||||
$this->end = $entry->getEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setId(int $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getStart(): \DateTime
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $start
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setStart(\DateTime $start)
|
||||
{
|
||||
$this->start = $start;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getEnd(): ?\DateTime
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime|null $end
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setEnd(?\DateTime $end)
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setTitle(string $title)
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setDescription(string $description)
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCustomer(): string
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $customer
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setCustomer(string $customer)
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getProject(): string
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $project
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setProject(string $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getActivity(): string
|
||||
{
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $activity
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setActivity(string $activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBorderColor(): ?string
|
||||
{
|
||||
return $this->borderColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $borderColor
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setBorderColor(?string $borderColor)
|
||||
{
|
||||
$this->borderColor = $borderColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBackgroundColor(): ?string
|
||||
{
|
||||
return $this->backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $backgroundColor
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setBackgroundColor(?string $backgroundColor)
|
||||
{
|
||||
$this->backgroundColor = $backgroundColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
43
src/Command/BashExecutor.php
Normal file
43
src/Command/BashExecutor.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\Command;
|
||||
|
||||
class BashExecutor
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rootDir;
|
||||
|
||||
/**
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct(string $projectDirectory)
|
||||
{
|
||||
$this->rootDir = realpath($projectDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
* @return BashResult
|
||||
*/
|
||||
public function execute(string $command)
|
||||
{
|
||||
$exitCode = 0;
|
||||
|
||||
$command = rtrim($this->rootDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($command, DIRECTORY_SEPARATOR);
|
||||
|
||||
ob_start();
|
||||
passthru($command, $exitCode);
|
||||
$result = ob_get_clean();
|
||||
|
||||
return new BashResult($exitCode, $result);
|
||||
}
|
||||
}
|
||||
70
src/Command/BashResult.php
Normal file
70
src/Command/BashResult.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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;
|
||||
|
||||
class BashResult
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $exitCode;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @param string $exitCode
|
||||
* @param string $result
|
||||
*/
|
||||
public function __construct($exitCode, $result)
|
||||
{
|
||||
$this->exitCode = $exitCode;
|
||||
$this->result = $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getExitCode(): string
|
||||
{
|
||||
return $this->exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $exitCode
|
||||
* @return BashResult
|
||||
*/
|
||||
public function setExitCode(string $exitCode)
|
||||
{
|
||||
$this->exitCode = $exitCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getResult(): string
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $result
|
||||
* @return BashResult
|
||||
*/
|
||||
public function setResult(string $result)
|
||||
{
|
||||
$this->result = $result;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
149
src/Command/ConvertTimezoneCommand.php
Normal file
149
src/Command/ConvertTimezoneCommand.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use Doctrine\DBAL\Types\DateTimeType;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* This command was added with v0.8.
|
||||
* Kimai saved the DateTime before with the local timezone, which can cause big problems when used in different environments.
|
||||
* You should convert all timesheet records that were saved with Kimai 2 directly, but NOT the ones migrated from Kimai v1.
|
||||
*
|
||||
* Please read https://github.com/kevinpapst/kimai2/pull/372 to find out more!
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ConvertTimezoneCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @param TimesheetRepository $repository
|
||||
*/
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:convert-timezone')
|
||||
->setDescription('Convert timesheet dates between timezones, only made for updates from 0.7 to 0.8')
|
||||
->setHelp('This command should only be required if you imported data from Kimai v1')
|
||||
->addOption('first-id', 'f', InputArgument::OPTIONAL, 'The database ID which should be converted first')
|
||||
->addOption('last-id', 'l', InputArgument::OPTIONAL, 'The database ID which should be converted last')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $start
|
||||
* @param null|string $end
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
protected function getTimesheets($start = null, $end = null)
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('t');
|
||||
|
||||
$qb->select('t');
|
||||
if (!empty($start)) {
|
||||
$qb->andWhere($qb->expr()->gte('t.id', $start));
|
||||
}
|
||||
if (!empty($end)) {
|
||||
$qb->andWhere($qb->expr()->lte('t.id', $end));
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param \DateTimeZone $timeZone
|
||||
* @return Timesheet
|
||||
*/
|
||||
protected function convertTimesheet(Timesheet $timesheet, \DateTimeZone $timeZone): Timesheet
|
||||
{
|
||||
$oldTimezone = $timesheet->getTimezone();
|
||||
|
||||
if (null !== $timesheet->getBegin()) {
|
||||
$beginDate = clone $timesheet->getBegin();
|
||||
$beginDate->setTimezone($timeZone);
|
||||
$timesheet->setBegin($beginDate);
|
||||
}
|
||||
|
||||
if (null !== $timesheet->getEnd()) {
|
||||
$endDate = clone $timesheet->getEnd();
|
||||
$endDate->setTimezone($timeZone);
|
||||
$timesheet->setEnd($endDate);
|
||||
}
|
||||
|
||||
$timesheet->setTimezone($oldTimezone);
|
||||
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
Type::overrideType(Type::DATETIME, DateTimeType::class);
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$start = $input->getOption('first-id');
|
||||
$end = $input->getOption('last-id');
|
||||
|
||||
$result = $this->getTimesheets($start, $end);
|
||||
$amount = count($result);
|
||||
|
||||
$answer = $io->ask(sprintf('This will update %s timesheet records, continue (y/n) ? ', $amount), 'y');
|
||||
|
||||
if ('y' !== $answer) {
|
||||
$io->text('Aborting.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
$i = 0;
|
||||
|
||||
/** @var Timesheet $timesheet */
|
||||
foreach ($result as $timesheet) {
|
||||
$timesheet = $this->convertTimesheet($timesheet, $utc);
|
||||
$this->repository->save($timesheet);
|
||||
|
||||
if (++$i % 80 === 0) {
|
||||
$io->writeln('. (' . $i . '/' . $amount . ')');
|
||||
} else {
|
||||
$io->write('.');
|
||||
}
|
||||
}
|
||||
|
||||
$io->writeln('. (' . $i . '/' . $amount . ')');
|
||||
$io->writeln('');
|
||||
}
|
||||
}
|
||||
158
src/Command/CreateReleaseCommand.php
Normal file
158
src/Command/CreateReleaseCommand.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?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 create a release package with pre-installed composer, SQLite database and user.
|
||||
*/
|
||||
class CreateReleaseCommand extends Command
|
||||
{
|
||||
public const CLONE_CMD = 'git clone -b %s --depth 1 https://github.com/kevinpapst/kimai2.git';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rootDir = '';
|
||||
|
||||
/**
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct(string $projectDirectory)
|
||||
{
|
||||
$this->rootDir = realpath($projectDirectory);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:create-release')
|
||||
->setDescription('Create a pre-installed release package')
|
||||
->setHelp('This command will create a release package with pre-installed composer, SQLite database and user.')
|
||||
->addOption('directory', null, InputOption::VALUE_OPTIONAL, 'Directory where the release package will be stored', 'var/data/')
|
||||
->addOption('release', null, InputOption::VALUE_OPTIONAL, 'The version that should be zipped', Constants::VERSION)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @return int|null
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if (getenv('APP_ENV') === 'prod') {
|
||||
$io->error('kimai:create-release is not allowed in production');
|
||||
|
||||
return -2;
|
||||
}
|
||||
|
||||
$directory = $input->getOption('directory');
|
||||
|
||||
if ($directory[0] === '/') {
|
||||
$directory = realpath($directory);
|
||||
} else {
|
||||
$directory = realpath($this->rootDir . '/' . $directory);
|
||||
}
|
||||
|
||||
$tmpDir = $directory . '/' . uniqid('kimai_release_');
|
||||
|
||||
if (!is_dir($directory)) {
|
||||
$io->error('Given directory is not existing: ' . $directory);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (is_dir($directory) && !is_writable($directory)) {
|
||||
$io->error('Cannot write in directory: ' . $directory);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$version = $input->getOption('release');
|
||||
|
||||
$io->success('Prepare new packages for Kimai ' . $version . ' in ' . $tmpDir);
|
||||
|
||||
$gitCmd = sprintf(self::CLONE_CMD, $version);
|
||||
$tar = 'kimai-release-' . $version;
|
||||
$zip = 'kimai-release-' . $version;
|
||||
|
||||
if ($version === Constants::VERSION && Constants::STATUS !== 'stable') {
|
||||
$tar .= '_' . Constants::STATUS;
|
||||
$zip .= '_' . Constants::STATUS;
|
||||
}
|
||||
|
||||
$tar .= '.tar.gz';
|
||||
$zip .= '.zip';
|
||||
|
||||
// this removes the current env settings, as they might differ from the release ones
|
||||
// if we don't unset them, the .env file won't be read when executing bin/console commands
|
||||
putenv('DATABASE_URL');
|
||||
putenv('APP_ENV');
|
||||
|
||||
$commands = [
|
||||
'Clone repository' => $gitCmd . ' ' . $tmpDir,
|
||||
'Install composer dependencies' => 'cd ' . $tmpDir . ' && composer install --no-dev --optimize-autoloader',
|
||||
'Create .env file' => 'cd ' . $tmpDir . ' && cp .env.dist .env',
|
||||
'Create database' => 'cd ' . $tmpDir . ' && bin/console doctrine:database:create -n',
|
||||
'Create tables' => 'cd ' . $tmpDir . ' && bin/console doctrine:schema:create -n',
|
||||
'Add all migrations' => 'cd ' . $tmpDir . ' && bin/console doctrine:migrations:version --add --all -n',
|
||||
];
|
||||
|
||||
$filesToDelete = [
|
||||
'.git*',
|
||||
'var/cache/*',
|
||||
'var/data/kimai_test.sqlite',
|
||||
'var/log/*.log',
|
||||
'var/sessions/*',
|
||||
];
|
||||
|
||||
foreach ($filesToDelete as $deleteMe) {
|
||||
$commands['Delete ' . $deleteMe] = 'cd ' . $tmpDir . ' && rm -rf ' . $deleteMe;
|
||||
}
|
||||
|
||||
$commands = array_merge($commands, [
|
||||
'Create tar' => 'cd ' . $tmpDir . ' && tar -czf ' . $directory . '/' . $tar . ' .',
|
||||
'Create zip' => 'cd ' . $tmpDir . ' && zip -r ' . $directory . '/' . $zip . ' .',
|
||||
'Remove tmp directory' => 'rm -rf ' . $tmpDir,
|
||||
]);
|
||||
|
||||
$exitCode = 0;
|
||||
foreach ($commands as $title => $command) {
|
||||
$io->success($title);
|
||||
passthru($command, $exitCode);
|
||||
if ($exitCode !== 0) {
|
||||
$io->error('Failed with command: ' . $command);
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
$io->success(
|
||||
'New release packages available at: ' . PHP_EOL .
|
||||
$directory . '/' . $tar . PHP_EOL .
|
||||
$directory . '/' . $zip
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -15,10 +15,14 @@ use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Timesheet\Util;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\DBAL\Configuration;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\Types\DateTimeType;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Symfony\Bridge\Doctrine\RegistryInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -32,6 +36,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
/**
|
||||
* Command used to import data from a Kimai v1 installation.
|
||||
* Getting help in improving this script would be fantastic, it currently only handles the most basic use-cases.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class KimaiImporterCommand extends Command
|
||||
{
|
||||
@@ -81,15 +87,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
|
||||
@@ -124,7 +129,8 @@ class KimaiImporterCommand extends Command
|
||||
)
|
||||
->addArgument('prefix', InputArgument::REQUIRED, 'The database prefix for the old Kimai v1 tables')
|
||||
->addArgument('password', InputArgument::REQUIRED, 'The new password for all imported user')
|
||||
->addArgument('country', InputArgument::OPTIONAL, 'The default country for customer', 'de')
|
||||
->addArgument('country', InputArgument::OPTIONAL, 'The default country for customer (2-character uppercase)', 'DE')
|
||||
->addArgument('currency', InputArgument::OPTIONAL, 'The default currency for customer (code like EUR, CHF, GBP or USD)', 'EUR')
|
||||
;
|
||||
}
|
||||
|
||||
@@ -133,6 +139,10 @@ class KimaiImporterCommand extends Command
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
// do not convert the times, Kimai 1 stored them already in UTC
|
||||
Type::overrideType(Type::DATETIME, DateTimeType::class);
|
||||
|
||||
// don't calculate rates ... this was done in Kimai 1
|
||||
$this->deactivateLifecycleCallbacks($this->getDoctrine()->getConnection());
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
@@ -152,7 +162,14 @@ class KimaiImporterCommand extends Command
|
||||
|
||||
$country = $input->getArgument('country');
|
||||
if (2 != trim(strlen($country))) {
|
||||
$io->error('Country length needs to be exactly 2 character');
|
||||
$io->error('Country code needs to be exactly 2 character');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$currency = $input->getArgument('currency');
|
||||
if (3 != trim(strlen($currency))) {
|
||||
$io->error('Currency code needs to be exactly 3 character');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -161,16 +178,9 @@ class KimaiImporterCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
// pre-load all data to make sure we can fully import everything
|
||||
$users = null;
|
||||
$customer = null;
|
||||
$projects = null;
|
||||
$activities = null;
|
||||
$records = null;
|
||||
$activityToProject = null;
|
||||
|
||||
$bytesStart = memory_get_usage(true);
|
||||
|
||||
// pre-load all data to make sure we can fully import everything
|
||||
try {
|
||||
$users = $this->fetchAllFromImport('users');
|
||||
} catch (\Exception $ex) {
|
||||
@@ -219,6 +229,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 ...');
|
||||
@@ -226,59 +252,55 @@ class KimaiImporterCommand extends Command
|
||||
$allImports = 0;
|
||||
|
||||
try {
|
||||
$counter = $this->importUsers($io, $password, $users);
|
||||
$counter = $this->importUsers($io, $password, $users, $rates);
|
||||
$allImports += $counter;
|
||||
$io->success('Imported users: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import users: ' . $ex->getMessage());
|
||||
$io->error('Failed to import users: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importCustomers($io, $customer, $country);
|
||||
$counter = $this->importCustomers($io, $customer, $country, $currency);
|
||||
$allImports += $counter;
|
||||
$io->success('Imported customers: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import customers: ' . $ex->getMessage());
|
||||
$io->error('Failed to import customers: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importProjects($io, $projects);
|
||||
$counter = $this->importProjects($io, $projects, $fixedRates, $rates);
|
||||
$allImports += $counter;
|
||||
$io->success('Imported projects: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import projects: ' . $ex->getMessage());
|
||||
$io->error('Failed to import projects: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importActivities($io, $activities, $activityToProject);
|
||||
$counter = $this->importActivities($io, $activities, $activityToProject, $fixedRates, $rates);
|
||||
$allImports += $counter;
|
||||
$io->success('Imported activities: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import activities: ' . $ex->getMessage());
|
||||
$io->error('Failed to import activities: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$counter = $this->importTimesheetRecords($io, $records);
|
||||
$counter = $this->importTimesheetRecords($io, $records, $fixedRates, $rates);
|
||||
$allImports += $counter;
|
||||
$io->success('Imported timesheet records: ' . $counter);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import timesheet records: ' . $ex->getMessage());
|
||||
$io->error('Failed to import timesheet records: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO support fixedRates (projectID, activityID, rate)
|
||||
// TODO support rates (userID, projectID, activityID, rate)
|
||||
// TODO dump yaml config from configuration (adminmail, currency_name, date_format_0, language, roundPrecision)
|
||||
// TODO support preferences (ui.lang, timezone)
|
||||
// TODO support expenses - new database required
|
||||
|
||||
$bytesImported = memory_get_usage(true);
|
||||
@@ -294,22 +316,35 @@ class KimaiImporterCommand extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the ghiven database connection for import has an underlying database with a compatible structure.
|
||||
* This is checked againstto the Kimai version and database revision.
|
||||
* Checks if the given database connection for import has an underlying database with a compatible structure.
|
||||
* This is checked against the Kimai version and database revision.
|
||||
*
|
||||
* @param SymfonyStyle $io
|
||||
* @param $requiredVersion
|
||||
* @param $requiredRevision
|
||||
* @param string $requiredVersion
|
||||
* @param string $requiredRevision
|
||||
* @return bool
|
||||
* @throws \Doctrine\DBAL\DBALException
|
||||
*/
|
||||
protected function checkDatabaseVersion(SymfonyStyle $io, $requiredVersion, $requiredRevision)
|
||||
{
|
||||
$versionQuery = 'SELECT `value` from ' . $this->dbPrefix . 'configuration WHERE `option` = "version"';
|
||||
$revisionQuery = 'SELECT `value` from ' . $this->dbPrefix . 'configuration WHERE `option` = "revision"';
|
||||
$optionColumn = $this->connection->quoteIdentifier('option');
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
|
||||
$version = $this->getImportConnection()->query($versionQuery)->fetchColumn();
|
||||
$revision = $this->getImportConnection()->query($revisionQuery)->fetchColumn();
|
||||
$version = $this->connection->createQueryBuilder()
|
||||
->select('value')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration'))
|
||||
->where($qb->expr()->eq($optionColumn, ':option'))
|
||||
->setParameter('option', 'version')
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
$revision = $this->connection->createQueryBuilder()
|
||||
->select('value')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration'))
|
||||
->where($qb->expr()->eq($optionColumn, ':option'))
|
||||
->setParameter('option', 'revision')
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
if (1 == version_compare($requiredVersion, $version)) {
|
||||
$io->error(
|
||||
@@ -359,26 +394,28 @@ class KimaiImporterCommand extends Command
|
||||
protected function bytesHumanReadable($size)
|
||||
{
|
||||
$unit = ['b', 'kB', 'MB', 'GB'];
|
||||
$i = floor(log($size, 1024));
|
||||
$a = (int) $i;
|
||||
|
||||
return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
|
||||
return @round($size / pow(1024, $i), 2) . ' ' . $unit[$a];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $table
|
||||
* @param array $where
|
||||
* @return array
|
||||
* @throws \Doctrine\DBAL\DBALException
|
||||
*/
|
||||
protected function fetchAllFromImport($table)
|
||||
protected function fetchAllFromImport($table, array $where = [])
|
||||
{
|
||||
return $this->getImportConnection()->query('SELECT * from ' . $this->dbPrefix . $table)->fetchAll();
|
||||
}
|
||||
$query = $this->connection->createQueryBuilder()
|
||||
->select('*')
|
||||
->from($this->connection->quoteIdentifier($this->dbPrefix . $table));
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Connection
|
||||
*/
|
||||
protected function getImportConnection()
|
||||
{
|
||||
return $this->connection;
|
||||
foreach ($where as $column => $value) {
|
||||
$query->andWhere($query->expr()->eq($column, $value));
|
||||
}
|
||||
|
||||
return $query->execute()->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,12 +438,8 @@ class KimaiImporterCommand extends Command
|
||||
if ($errors->count() > 0) {
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolation $error */
|
||||
foreach ($errors as $error) {
|
||||
$value = $error->getInvalidValue();
|
||||
$io->error(
|
||||
$error->getPropertyPath()
|
||||
. ' (' . (is_array($value) ? implode(',', $value) : $value) . ')'
|
||||
. "\n "
|
||||
. $error->getMessage()
|
||||
(string) $error
|
||||
);
|
||||
}
|
||||
|
||||
@@ -442,10 +475,11 @@ class KimaiImporterCommand extends Command
|
||||
* @param SymfonyStyle $io
|
||||
* @param string $password
|
||||
* @param array $users
|
||||
* @param array $rates
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function importUsers(SymfonyStyle $io, $password, $users)
|
||||
protected function importUsers(SymfonyStyle $io, $password, $users, $rates)
|
||||
{
|
||||
$counter = 0;
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
@@ -470,6 +504,34 @@ class KimaiImporterCommand extends Command
|
||||
throw new \Exception('Failed to validate user: ' . $user->getUsername());
|
||||
}
|
||||
|
||||
// find and migrate user preferences
|
||||
$prefsToImport = ['ui.lang' => 'language', 'timezone' => 'timezone'];
|
||||
$preferences = $this->fetchAllFromImport('preferences', ['userID' => $oldUser['userID']]);
|
||||
foreach ($preferences as $pref) {
|
||||
$key = $pref['option'];
|
||||
|
||||
if (!array_key_exists($key, $prefsToImport)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newPref = new UserPreference();
|
||||
$newPref
|
||||
->setName($prefsToImport[$key])
|
||||
->setValue($pref['value']);
|
||||
$user->addPreference($newPref);
|
||||
}
|
||||
|
||||
// find hourly rate
|
||||
foreach ($rates as $ratesRow) {
|
||||
if ($ratesRow['userID'] === $oldUser['userID'] && $ratesRow['activityID'] === null && $ratesRow['projectID'] === null) {
|
||||
$newPref = new UserPreference();
|
||||
$newPref
|
||||
->setName(UserPreference::HOURLY_RATE)
|
||||
->setValue($ratesRow['rate']);
|
||||
$user->addPreference($newPref);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
@@ -508,18 +570,19 @@ 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"
|
||||
*
|
||||
* @param SymfonyStyle $io
|
||||
* @param array $customers
|
||||
* @param string $country
|
||||
* @param string $currency
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function importCustomers(SymfonyStyle $io, $customers, $country)
|
||||
protected function importCustomers(SymfonyStyle $io, $customers, $country, $currency)
|
||||
{
|
||||
$counter = 0;
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
@@ -538,13 +601,16 @@ class KimaiImporterCommand extends Command
|
||||
->setComment($oldCustomer['comment'])
|
||||
->setCompany($oldCustomer['company'])
|
||||
->setFax($oldCustomer['fax'])
|
||||
->setHomepage($oldCustomer['homepage'])
|
||||
->setMobile($oldCustomer['mobile'])
|
||||
->setEmail($oldCustomer['mail'])
|
||||
->setPhone($oldCustomer['phone'])
|
||||
->setContact($oldCustomer['contact'])
|
||||
->setAddress($oldCustomer['street'] . PHP_EOL . $oldCustomer['zipcode'] . ' ' . $oldCustomer['city'])
|
||||
->setTimezone($oldCustomer['timezone'])
|
||||
->setVisible($isActive)
|
||||
->setCountry($country)
|
||||
->setCountry(strtoupper($country))
|
||||
->setCurrency(strtoupper($currency))
|
||||
;
|
||||
|
||||
if (!$this->validateImport($io, $customer)) {
|
||||
@@ -586,10 +652,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 +679,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,36 +740,49 @@ 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
|
||||
);
|
||||
}
|
||||
|
||||
$project = $this->projects[$projectId];
|
||||
|
||||
$this->unassignedActivities[$oldActivity['activityID']] = $oldActivity;
|
||||
$this->createActivity($io, $entityManager, $project, $oldActivity);
|
||||
$this->createActivity($io, $entityManager, $oldActivity, $fixedRates, $rates, $projectId);
|
||||
++$counter;
|
||||
} else {
|
||||
$this->unassignedActivities[$oldActivity['activityID']] = $oldActivity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,20 +792,25 @@ class KimaiImporterCommand extends Command
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param ObjectManager $entityManager
|
||||
* @param Project $project
|
||||
* @param array $oldActivity
|
||||
* @param array $fixedRates
|
||||
* @param array $rates
|
||||
* @param int $projectId
|
||||
* @return Activity
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function createActivity(
|
||||
SymfonyStyle $io,
|
||||
ObjectManager $entityManager,
|
||||
Project $project,
|
||||
array $oldActivity
|
||||
array $oldActivity,
|
||||
array $fixedRates,
|
||||
array $rates,
|
||||
$projectId
|
||||
) {
|
||||
$activityId = $oldActivity['activityID'];
|
||||
if (isset($this->activities[$activityId][$project->getId()])) {
|
||||
return $this->activities[$activityId][$project->getId()];
|
||||
|
||||
if (isset($this->activities[$activityId][$projectId])) {
|
||||
return $this->activities[$activityId][$projectId];
|
||||
}
|
||||
|
||||
$isActive = (bool) $oldActivity['visible'] && !(bool) $oldActivity['trash'];
|
||||
@@ -716,14 +820,50 @@ class KimaiImporterCommand extends Command
|
||||
$io->warning('Found empty activity name, setting it to: ' . $name);
|
||||
}
|
||||
|
||||
if (null !== $projectId && !isset($this->projects[$projectId])) {
|
||||
throw new \Exception(
|
||||
sprintf('Did not find project [%s], skipping activity creation [%s] %s', $projectId, $activityId, $name)
|
||||
);
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
$activity
|
||||
->setName($name)
|
||||
->setComment($oldActivity['comment'] ?: null)
|
||||
->setVisible($isActive)
|
||||
->setProject($project)
|
||||
;
|
||||
|
||||
if (null !== $projectId) {
|
||||
$project = $this->projects[$projectId];
|
||||
$activity->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 +882,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;
|
||||
}
|
||||
@@ -760,7 +900,7 @@ class KimaiImporterCommand extends Command
|
||||
* ["description"]=> NULL
|
||||
* ["comment"]=> string(36) "a work description"
|
||||
* -- ["commentType"]=> string(1) "0"
|
||||
* -- ["cleared"]=> string(1) "0"
|
||||
* ["cleared"]=> string(1) "0"
|
||||
* -- ["location"]=> string(0) ""
|
||||
* -- ["trackingNumber"]=> NULL
|
||||
* ["rate"]=> string(5) "50.00"
|
||||
@@ -772,14 +912,24 @@ 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)
|
||||
{
|
||||
$errors = [
|
||||
'projectActivityMismatch' => [],
|
||||
];
|
||||
$counter = 0;
|
||||
$failed = 0;
|
||||
$activityCounter = 0;
|
||||
$userCounter = 0;
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$total = count($records);
|
||||
|
||||
$io->writeln('Importing timesheets, please wait');
|
||||
|
||||
foreach ($records as $oldRecord) {
|
||||
$activity = null;
|
||||
@@ -791,46 +941,137 @@ class KimaiImporterCommand extends Command
|
||||
$project = $this->projects[$projectId];
|
||||
} else {
|
||||
$io->error('Could not create timesheet record, missing project with ID: ' . $projectId);
|
||||
$failed++;
|
||||
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, $projectId);
|
||||
++$activityCounter;
|
||||
}
|
||||
|
||||
// this should not happen at all
|
||||
if (null === $activity) {
|
||||
$io->error('Could not create timesheet record, missing activity with ID: ' . $activityId);
|
||||
$io->error('Could not import timesheet record, missing activity with ID: ' . $activityId . '/' . $projectId . '/' . $customerId);
|
||||
$failed++;
|
||||
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);
|
||||
if (empty($oldRecord['end']) || $oldRecord['end'] === 0) {
|
||||
$io->error('Cannot import running timesheet record, skipping: ' . $oldRecord['timeEntryID']);
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$duration = (int) ($oldRecord['end'] - $oldRecord['start']);
|
||||
|
||||
// ----------------------- unknown user, damned missing data integrity in Kimai v1 -----------------------
|
||||
if (!isset($this->users[$oldRecord['userID']])) {
|
||||
$tempUserName = uniqid();
|
||||
$tempPassword = uniqid() . uniqid();
|
||||
|
||||
$user = new User();
|
||||
$user->setUsername($tempUserName)
|
||||
->setAlias('Import: ' . $tempUserName)
|
||||
->setEmail($tempUserName . '@example.com')
|
||||
->setPlainPassword($tempPassword)
|
||||
->setEnabled(false)
|
||||
->setRoles([USER::ROLE_USER])
|
||||
;
|
||||
|
||||
$pwd = $this->encoder->encodePassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($pwd);
|
||||
|
||||
if (!$this->validateImport($io, $user)) {
|
||||
$io->error('Found timesheet record for unknown user and failed to create user, skipping timesheet: ' . $oldRecord['timeEntryID']);
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
if ($this->debug) {
|
||||
$io->success('Created deactivated user: ' . $user->getUsername());
|
||||
}
|
||||
$userCounter++;
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to create user: ' . $user->getUsername());
|
||||
$io->error('Reason: ' . $ex->getMessage());
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->users[$oldRecord['userID']] = $user;
|
||||
}
|
||||
// ----------------------- unknown user end -----------------------
|
||||
|
||||
$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 = (float) $timesheet->getHourlyRate();
|
||||
$rate = Util::calculateRate($hourlyRate, $duration);
|
||||
$timesheet->setRate($rate);
|
||||
}
|
||||
|
||||
$user = $this->users[$oldRecord['userID']];
|
||||
$timezone = $user->getPreferenceValue('timezone', date_default_timezone_get());
|
||||
$dateTimezone = new \DateTimeZone('UTC');
|
||||
|
||||
$begin = new \DateTime('@' . $oldRecord['start']);
|
||||
$begin->setTimezone($dateTimezone);
|
||||
$end = new \DateTime('@' . $oldRecord['end']);
|
||||
$end->setTimezone($dateTimezone);
|
||||
|
||||
// ---------- workaround for localizeDates ----------
|
||||
// if getBegin() is not executed first, then the dates will we re-written in validateImport() below
|
||||
$timesheet->setBegin($begin)->setEnd($end)->getBegin();
|
||||
// --------------------------------------------------
|
||||
|
||||
// ---------- this was a bug in the past, should not happen anymore ----------
|
||||
if ($activity->getProject() !== null && $project->getId() !== $activity->getProject()->getId()) {
|
||||
$errors['projectActivityMismatch'][] = $oldRecord['timeEntryID'];
|
||||
continue;
|
||||
}
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
$timesheet
|
||||
->setDescription($oldRecord['description'] ?: ($oldRecord['comment'] ?: null))
|
||||
->setUser($this->users[$oldRecord['userID']])
|
||||
->setBegin(new \DateTime('@' . $oldRecord['start']))
|
||||
->setEnd(new \DateTime('@' . $oldRecord['end']))
|
||||
->setBegin($begin)
|
||||
->setEnd($end)
|
||||
->setDuration($duration)
|
||||
->setActivity($activity)
|
||||
->setRate($rate)
|
||||
->setProject($project)
|
||||
->setExported(intval($oldRecord['cleared']) !== 0)
|
||||
->setTimezone($timezone)
|
||||
;
|
||||
|
||||
if (!$this->validateImport($io, $timesheet)) {
|
||||
throw new \Exception('Failed to validate timesheet record: ' . $timesheet->getId());
|
||||
$io->error('Failed to validate timesheet record: ' . $oldRecord['timeEntryID'] . ' - skipping!');
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -841,17 +1082,33 @@ class KimaiImporterCommand extends Command
|
||||
}
|
||||
++$counter;
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to create timesheet record: ' . $timesheet->getId());
|
||||
$io->error('Reason: ' . $ex->getMessage());
|
||||
$io->error('Failed to create timesheet record: ' . $ex->getMessage());
|
||||
$failed++;
|
||||
}
|
||||
|
||||
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 ($userCounter > 0) {
|
||||
$io->success('Created new users during timesheet import: ' . $userCounter);
|
||||
}
|
||||
if ($activityCounter > 0) {
|
||||
$io->success('Created new (previously unattached) activities during timesheet import: ' . $activityCounter);
|
||||
$io->success('Created new activities during timesheet import: ' . $activityCounter);
|
||||
}
|
||||
if (count($errors['projectActivityMismatch']) > 0) {
|
||||
$io->error('Found invalid mapped project - activity combinations in these old timesheet recors: ' . implode(',', $errors['projectActivityMismatch']));
|
||||
}
|
||||
if ($failed > 0) {
|
||||
$io->error(sprintf('Failed importing %s timesheet records', count($failed)));
|
||||
}
|
||||
|
||||
return $counter;
|
||||
|
||||
@@ -49,6 +49,12 @@ EOT
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if (getenv('APP_ENV') === 'prod') {
|
||||
$io->error('kimai:reset-dev is not allowed in production');
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($this->askConfirmation($input, $output, 'Do you want to create the database y/N ?')) {
|
||||
try {
|
||||
$command = $this->getApplication()->find('doctrine:database:create');
|
||||
|
||||
@@ -18,18 +18,24 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
/**
|
||||
* Command used to check and apply the projects coding styles.
|
||||
*/
|
||||
class RunCodeSnifferCommand extends Command
|
||||
class RunCodestyleCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var BashExecutor
|
||||
*/
|
||||
protected $executor;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rootDir = '';
|
||||
|
||||
/**
|
||||
* @param BashExecutor $executor
|
||||
* @param string $projectDirectory
|
||||
*/
|
||||
public function __construct($projectDirectory)
|
||||
public function __construct(BashExecutor $executor, string $projectDirectory)
|
||||
{
|
||||
$this->executor = $executor;
|
||||
$this->rootDir = realpath($projectDirectory);
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -40,9 +46,9 @@ class RunCodeSnifferCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:phpcs')
|
||||
->setDescription('Run PHP_CodeSniffer to check for the projects coding style')
|
||||
->addOption('fix', null, InputOption::VALUE_NONE, 'Fix all found problems (risky: modifies your files)')
|
||||
->setName('kimai:codestyle')
|
||||
->setDescription('Check and fix the projects coding style')
|
||||
->addOption('fix', null, InputOption::VALUE_NONE, 'Fix all found problems')
|
||||
->addOption('checkstyle', null, InputOption::VALUE_OPTIONAL, '')
|
||||
;
|
||||
}
|
||||
@@ -55,7 +61,6 @@ class RunCodeSnifferCommand extends Command
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$filename = null;
|
||||
ob_start();
|
||||
|
||||
$args = [];
|
||||
if (!$input->getOption('fix')) {
|
||||
@@ -78,13 +83,11 @@ class RunCodeSnifferCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$exitCode = 0;
|
||||
passthru($this->rootDir . '/vendor/bin/php-cs-fixer fix ' . implode(' ', $args), $exitCode);
|
||||
$result = ob_get_clean();
|
||||
$result = $this->executor->execute('/vendor/bin/php-cs-fixer fix ' . implode(' ', $args));
|
||||
|
||||
$io->write($result);
|
||||
$io->write($result->getResult());
|
||||
|
||||
if ($exitCode > 0) {
|
||||
if ($result->getExitCode() > 0) {
|
||||
$io->error(
|
||||
'Found problems while checking your code styles' .
|
||||
(!empty($filename) ? '. Saved checkstyle data to: ' . $filename : '')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user