Compare commits

...

124 Commits
1.2 ... 1.8

Author SHA1 Message Date
Kevin Papst
b8273c4441 prepare release 1.8 (#1505) 2020-03-01 14:25:59 +01:00
Kevin Papst
8d3a305ec3 handle negative duration (#1510) 2020-03-01 13:46:37 +01:00
Kevin Papst
72b73bc9a6 support multi-line values in excel invoices (#1507) 2020-02-29 20:06:45 +01:00
Kevin Papst
ec31d206e2 added UI configuration for default start-time (#1506) 2020-02-28 16:06:00 +01:00
Kevin Papst
bf11de82fc Upload invoice documents via UI (#1495) 2020-02-27 23:19:19 +01:00
Kevin Papst
34d2228d5d option to use decimal format in user timesheet export (#1489) 2020-02-24 18:03:30 +01:00
Kevin Papst
0e507804b1 human friendly name for bundles (#1491) 2020-02-22 22:56:37 +01:00
Kevin Papst
6c7477f1a9 edit single system config section in modal (#1490) 2020-02-22 18:53:03 +01:00
Kevin Papst
886fc8d74e action to duplicate timesheet record (#1478) 2020-02-18 15:32:33 +01:00
Kevin Papst
7b218c33f9 added generic rules for auth firewall (#1472) 2020-02-18 13:54:03 +01:00
Kevin Papst
c67617262f duplicate projects with all settings (#1465) 2020-02-13 16:47:20 +01:00
Kevin Papst
057c824e9f replaced mailer library, added email templates (#1462) 2020-02-12 16:05:13 +01:00
Kevin Papst
1b4f4bba22 added option to use only pre-defined tags (#1463) 2020-02-12 15:35:42 +01:00
Philipp Strobel
847798eec1 use different arrow for paginated box widget (#1431) 2020-02-10 22:16:16 +01:00
Kevin Papst
52c7437076 added user-specific rates (#1455) 2020-02-10 20:29:43 +01:00
Kevin Papst
465d7166d4 remove pagerfanta bundle (#1458) 2020-02-10 16:38:26 +01:00
MikKrz
733aa58de8 added Polish translations (#1426) 2020-02-10 00:23:37 +01:00
Kevin Papst
9620627ea6 fix dates in project API (#1451) 2020-02-10 00:16:44 +01:00
Kevin Papst
e06314e802 replace travis with github actions (#1452) 2020-02-09 23:54:04 +01:00
Kevin Papst
986d922855 invoices: choose language and duration format (#1438) 2020-02-04 20:27:36 +01:00
Kevin Papst
75dcae6fbe update to Symfony 4.4 (#1275) 2020-02-04 15:29:26 +01:00
Luca Buoncristiani
375691365e added missing italian translation files (#1437) 2020-02-04 13:31:20 +01:00
infeeeee
5c5af05408 fixed glyphs in pdf export table header (#1435) 2020-02-03 17:18:39 +01:00
Kevin Papst
69cf7015d8 optimize detail pages, add new permission for comment posting (#1432) 2020-02-02 18:16:23 +01:00
Kevin Papst
6a533579b7 added support for saml login (#1408) 2020-01-31 19:47:34 +01:00
Kevin Papst
3ff46e06c0 code improvements (#1423)
* removed unused mapping information
* added support for further field types
* fixed changing date objects for begin and end
* added more project fields as invoice variables
* theme update and asset rebuild
2020-01-31 17:07:14 +01:00
Kevin Papst
9e2fccb105 added pagination to working time widget (#1418) 2020-01-30 21:51:27 +01:00
Kevin Papst
f2c6d8aee1 better handling of custom field columns in tables (#1419) 2020-01-29 15:18:10 +01:00
Kevin Papst
836a530b86 API methods to handle teams and users (#1384) 2020-01-28 22:28:31 +01:00
Kevin Papst
a31e027c54 updated slovak translations (#1414) 2020-01-28 22:02:37 +01:00
Kevin Papst
a1b554b3ac various code improvements (#1415) 2020-01-28 21:57:34 +01:00
Kevin Papst
79c4c237fa support for auto linking urls with file protocol (#1402) 2020-01-22 17:25:11 +01:00
Kevin Papst
85a35a3063 prepare release 1.7 (#1388) 2020-01-19 21:50:29 +01:00
Kevin Papst
e0e391efbe prepare release 1.7 (#1388) 2020-01-19 21:49:41 +01:00
Kevin Papst
9eff89963c prepare release 1.7 (#1388) 2020-01-19 21:49:04 +01:00
Lukas
352fac26f3 added missing raw filter (#1393) 2020-01-19 21:47:40 +01:00
Lukas
8c16bf126a added missing raw filter (#1393) 2020-01-19 21:47:17 +01:00
Lukas
42de968f8f added missing raw filter (#1393) 2020-01-19 21:46:47 +01:00
ikusimakusi
ed8f024e42 added Basque translations (#1382) 2020-01-17 15:28:12 +01:00
Kevin Papst
e534369c5b fix widgets imports (#1387) 2020-01-17 00:02:02 +01:00
Kevin Papst
fed797df93 order user roles and permissions (#1372) 2020-01-16 16:54:42 +01:00
Kevin Papst
6a44dbfe83 detail pages for customers and projects (#1371) 2020-01-16 16:25:28 +01:00
Kevin Papst
28c5357f11 allow to hide rate or duration in html export summary (#1370) 2020-01-14 13:46:07 +01:00
Kevin Papst
a8824f92a1 added support on PHP 7.4 (#1355) 2020-01-07 21:43:33 +01:00
Kevin Papst
e57b69973f re-written exporter for simpler extendability (#1349) 2020-01-07 21:20:45 +01:00
Kevin Papst
d6798eed1e added project start and end date (#1303)
* added sortable js library
* activity in invoice is optional
* added javascript widget for paginated boxes
* fix activity dropdown for globals only
* added timesheet service to reduce code duplication
* use repository to query for teams in dropdowns
* added project validator
* validate project start and end against timesheet
* include begin and end in dynamic form requests for projects
* added timezone and language option to import flag, improve timesheet import speed
* deactivate cross-timezone filter
* add virtual fields to field order list
* composer update
* added param to ignore dates
* position loader icon fixed - fixes #1330
* permission problem when creating a new project - fixes #1340
* remove dev dependencies webserver and thanks bundle
* stop information leak (begin and end date) in duration mode - fixes #1307
* unify timesheet edit dialog for user and admins
* fix security issue, own rates exposed to unauthorized users in multi-update dialog
2020-01-05 02:49:01 +01:00
hmr-it-jr
50383673ca added api endpoint to return instance specific timesheet config (#1335) 2020-01-04 01:44:11 +01:00
Kevin Papst
022dbe32e5 allow empty date in export (#1338) 2019-12-31 20:24:47 +01:00
Kevin Papst
baac4eb698 set rates via batch update (#1326) 2019-12-29 16:52:43 +01:00
Kevin Papst
8aa8f452d5 added exported filter in timesheet view (#1327) 2019-12-29 13:55:15 +01:00
Kevin Papst
54255cf923 validate tag name (#1325) 2019-12-16 16:55:34 +01:00
Kevin Papst
bd3d82d224 fix daterange filter for multi-timezone setups (#1321) 2019-12-16 16:23:03 +01:00
Kevin Papst
b14ad9f3ab renamed translation files from .xliff to .xlf (#1314) 2019-12-09 13:25:31 +01:00
Kevin Papst
261b4646e4 added chinese translations (#1310) 2019-12-09 12:56:38 +01:00
Kevin Papst
d62e1c4468 simplify to add widgets via plugin (#1308) 2019-12-06 23:36:03 +01:00
benzel
5b0266c462 clarified translation (#1309) 2019-12-06 22:05:33 +01:00
Kevin Papst
06118dcabb loose email validation on customer (#1224) 2019-12-02 15:06:06 +01:00
Kevin Papst
984c852ab6 Release 1.6.2 (#1289)
* include user teams in user entity
* prevent unauthorized access via API
* improve teamlead permission handling in team timesheets
* add team data to user entity
* add security tests
* highlight menu for invoice template copy
* unified handling of invoice data across all templates
* access to the current users data in invoice templates
* permission improvement in invoice form
* allow to skip record rows
* allow to add new invoice locations without overwriting the global ones
* allow to order user preferences
* change permission for normal users with access to view_other_timesheets
* properly validate invoice template field length
* allow to replace multiple variables in cell values text
* upgraded office invoice template
* doctrine deprecation fix
* upgrade phpoffice/phpword
* fix future begin check for default rounding rules
* dashboard widget counter: respect visibility and teams - fixes #1161
* fix future begin check for default rounding rules
* added new events for pre and post invoice rendering
* fix permission issue for users without team seeing all records
* prevent error in spreadsheet renderer for empty invoices
2019-12-02 10:57:03 +01:00
Badgie
47414cfd0e Danish translations (#1296) 2019-12-02 00:25:28 +01:00
Kevin Papst
fdf4fdfb6a fix sqlite installer (#1283) 2019-11-26 19:32:16 +01:00
Kevin Papst
5ee0396a02 Release 1.6.1 (#1282) 2019-11-26 17:22:31 +01:00
Kevin Papst
724eb9c658 release 1.6 (#1277) 2019-11-24 15:29:01 +01:00
Kevin Papst
936ab05882 prepare release 1.6 (#1270) 2019-11-24 00:02:43 +01:00
Kevin Papst
817a43b178 added csv importer for timesheet and project data (#1216) 2019-11-23 22:02:55 +01:00
Kevin Papst
0f16f42887 Select2 search enhancement (#1276) 2019-11-23 14:26:35 +01:00
Kevin Papst
641c199c8c API: allow to set user for timesheet (#1259) 2019-11-23 14:21:46 +01:00
Aptevo
04dd6b900f added de_CH language (#1272) 2019-11-23 11:29:29 +01:00
Kevin Papst
536a05210e enable interactive features of Swagger UI (#1273) 2019-11-22 21:59:25 +01:00
Kevin Papst
60d16016e8 allow to configure calendar slot duration (#1269) 2019-11-22 01:30:25 +01:00
Kevin Papst
9222075c7a Added update command (#1268) 2019-11-21 23:15:41 +01:00
1willem
641dcdff85 Dutch translation files (#1226) 2019-11-18 21:48:16 +01:00
Kevin Papst
668649be7a use homepage link to redirect from error page (#1260) 2019-11-18 12:29:21 +01:00
Kevin Papst
0de936d474 fix the password reset page layout (#1261) 2019-11-18 12:28:59 +01:00
Kevin Papst
47bd8b0ce7 added new invoice fields, improved invoice templates (#1258) 2019-11-18 12:01:26 +01:00
Kevin Papst
2287c9951c invoice: money fields without currency, title cleanup (#1254) 2019-11-15 18:54:08 +01:00
Kevin Papst
94c28ebbc5 Update and delete multi timesheets and tags (#1240) 2019-11-15 01:25:31 +01:00
Kevin Papst
b4739f8f03 allow to include bundle configuration in system configuration screen (#1235) 2019-11-14 19:25:38 +01:00
Kevin Papst
c32cd36861 fix reset input for forms (#1251) 2019-11-14 19:09:55 +01:00
Kevin Papst
945562b34d fixes wrong foreign keys on timesheet_tags join table (#1242) 2019-11-14 01:57:58 +01:00
Kevin Papst
af0f89774e User roles and permission management via Admin UI (#1231) 2019-11-10 18:53:56 +01:00
Kevin Papst
c6c4098759 rounding rules via admin screen, round begin when starting record (#1229) 2019-11-10 13:57:05 +01:00
Kevin Papst
fa1c79e15c support different formats in user timesheet exports (#1222) 2019-11-08 16:10:38 +01:00
Kevin Papst
0705c26513 highlight menus on sub-pages (#1220) 2019-11-07 11:19:10 +01:00
Kevin Papst
140fa4b7c0 prepare release 1.5 (#1214) 2019-11-03 17:31:33 +01:00
Kevin Papst
cd88901868 re-style error pages (#1210) 2019-10-31 14:20:53 +01:00
Kevin Papst
8eec97deb5 respect users timezone in statistic widgets (#1207) 2019-10-31 00:17:31 +01:00
Kevin Papst
4331a7c9f4 fix error pages (#1209) 2019-10-31 00:16:25 +01:00
Kevin Papst
f4fcfeba9d dynamic branding options (#1205) 2019-10-30 14:22:34 +01:00
Kevin Papst
48aefb1862 fix avatar path for subdirectory installations (#1201) 2019-10-30 14:00:48 +01:00
Kevin Papst
bda9584346 reformat edit forms (#1192) 2019-10-30 13:59:53 +01:00
Kevin Papst
ed1154af67 fix absolute url (#1200) 2019-10-30 13:45:59 +01:00
Kevin Papst
c368eacd87 fixed renamed filter name, added that disabled users cannot be filtered (#1193) 2019-10-27 13:56:45 +01:00
Kevin Papst
949c59230f improve list views (#1191) 2019-10-25 16:21:30 +02:00
Kevin Papst
1114538f41 duration field in timesheet edit dialog (#1180) 2019-10-24 23:46:22 +02:00
Kevin Papst
b0f83291ee upgraded to phpunit 8 (#1155) 2019-10-24 17:35:05 +02:00
Kevin Papst
e91e4ff430 support search term in API collection requests (#1185) 2019-10-24 17:31:15 +02:00
Ahmed Şeref
4ad290d4bd added turkish translations (#1160) 2019-10-24 17:30:34 +02:00
Kevin Papst
37c6e2c0af added order-date field to project (#1186) 2019-10-24 17:04:21 +02:00
Kevin Papst
b4a45afce9 fix API login in subdirectory installations (#1181) 2019-10-24 11:06:12 +02:00
yayitazale
330ff6eb47 updated spanish translations (#1183) 2019-10-24 10:53:23 +02:00
Adrian Rudnik
c681eb1bdb docker fix to hide details for 500 errors (#1173) 2019-10-21 16:22:29 +02:00
Kevin Papst
de533e7031 Kimai 1.4.2 (#1167) 2019-10-14 12:30:11 +02:00
Kevin Papst
ae6ed3f23f Bugfixes for Kimai 1.4.1 (#1158)
* fix missing translation for preset datatable headers
* better dependency check for avatar service
* show only own teams to teamleads
2019-10-07 16:04:19 +02:00
Kevin Papst
32aa209b75 prepare release 1.4 (#1154) 2019-10-04 07:36:59 +02:00
Kevin Papst
e8c25d8ac5 added avatars, show user teams in list, new team dashboard widgets (#1150) 2019-10-03 13:44:16 +02:00
Kevin Papst
718ad4b398 fix migrations for older database versions (#1148) 2019-09-27 14:52:22 +02:00
Kevin Papst
8d81a394ee updated theme, fix deprecations in events dispatching (#1145) 2019-09-25 19:12:12 +02:00
Kevin Papst
d661c8b54e fix closing search dropdown (#1142)
* using a different library for javascript selects, fixing the closing search dropdown
* fix closing dropdown for daterangepicker
* added missing search button for mobile on customer page
* fix meta fields with same names than existing columns
2019-09-25 18:34:25 +02:00
Kevin Papst
e45e782e7d fix invoice preview (#1129) 2019-09-20 16:00:03 +02:00
Kevin Papst
5aa422dde1 Display meta-fields in datatables (#1116) 2019-09-19 20:28:31 +02:00
Kevin Papst
24cbe3d281 post 1.3 fixes (#1106) 2019-09-17 22:20:36 +02:00
Kevin Papst
13f5e76015 added czech translations (#1075) 2019-09-16 23:19:46 +02:00
Kevin Papst
35c65ba890 composer update (#1104) 2019-09-13 17:16:53 +02:00
Kevin Papst
8f2b82b832 bump version to 1.3 (#1102) 2019-09-12 00:36:48 +02:00
Kevin Papst
7f1a206a52 support installation specific translations (#1099) 2019-09-11 23:18:35 +02:00
Kevin Papst
8c503a226e fix API version string (#1096) 2019-09-11 21:44:14 +02:00
Kevin Papst
cdcf9eaf67 handles session timeouts in modals (#1092) 2019-09-10 17:07:34 +02:00
Kevin Papst
a651e55dc9 added table-column ordering (#1086) 2019-09-09 23:47:42 +02:00
Kevin Papst
d041a3f4f9 improved tag auto-completion and added tag management (#1083) 2019-09-04 23:47:33 +02:00
Kevin Papst
3ea05cb705 support multiple invoice repositories (#1084) 2019-09-04 22:50:53 +02:00
hmr-it-jr
3f27ab1e44 added mail icons (#1077) 2019-09-04 19:49:03 +02:00
Kevin Papst
e99b170d0a refactored search with free search term support (#1064) 2019-09-04 18:54:06 +02:00
Kevin Papst
49e1a1c410 use timesheet description in invoices (#1079) 2019-09-04 10:54:21 +02:00
Kevin Papst
93ab3d5666 post release 1.2 fixes (#1068)
* fix release command
* fix permission name in UPGRADING
* added missing korean calendar translations
* fixed entry.total for invoice with fixed rates
2019-09-03 11:59:34 +02:00
1146 changed files with 54456 additions and 14092 deletions

View File

@@ -3,7 +3,8 @@ root = true
; Unix-style newlines
[*]
end_of_line = LF
end_of_line = lf
charset = utf-8
[*.php]
indent_style = space

View File

@@ -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
MAILER_FROM=kimai@example.com
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=change_this_to_something_unique
@@ -19,13 +17,18 @@ APP_SECRET=change_this_to_something_unique
DATABASE_URL=sqlite:///%kernel.project_dir%/var/data/kimai.sqlite
###< doctrine/doctrine-bundle ###
###> symfony/swiftmailer-bundle ###
# For Gmail as a transport, use: "gmail://username:password@localhost"
# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode="
# 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 ###
### Email configuration
# SMTP: smtp://localhost:25?encryption=&auth_mode=
# Google: gmail://username:password@default
# Amazon: ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1
# Mailchimp: mandrill://KEY@default
# Mailgun: mailgun://KEY:DOMAIN@default
# Postmark: postmark://ID@default
# Sendgrid: sendgrid://KEY@default
# Disable emails: null://null
MAILER_URL=null://null
MAILER_FROM=kimai@example.com

35
.github/workflows/coverage.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: CI
on:
pull_request: null
push:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.4']
name: Coverage - PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v1
with:
php-version: ${{ matrix.php }}
coverage: pcov
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite
- run: |
composer install --no-progress
composer require zendframework/zend-ldap
- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Run tests
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
env:
TEST_WITH_BUNDLES: 1
- name: Upload code coverage
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml

28
.github/workflows/linting.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: CI
on:
pull_request: null
push:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.4']
name: Linting - PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v1
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: intl
tools: cs2pr:1.0.1
- run: composer install --no-progress
- run: composer validate --no-check-all --strict
- run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php_cs.dist --using-cache=no --show-progress=none --format=checkstyle | cs2pr
- run: vendor/bin/phpstan analyse src -c phpstan.neon --level=3 --no-progress --error-format=checkstyle | cs2pr
- run: vendor/bin/phpstan analyse tests -c tests/phpstan.neon --level=4 --no-progress --error-format=checkstyle | cs2pr
- run: composer kimai:code-lint

58
.github/workflows/testing.yaml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: CI
on:
pull_request: null
push:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: false
MYSQL_ROOT_PASSWORD: kimai
MYSQL_DATABASE: kimai
ports:
- 3306/tcp
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
matrix:
php: ['7.2', '7.3', '7.4']
name: Tests - PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v1
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql
- run: |
composer install --no-progress
composer require zendframework/zend-ldap
- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Run unit tests
run: composer kimai:tests-unit
- name: Run migrations on MySQL
run: |
bin/console doctrine:database:create --if-not-exists -n
bin/console doctrine:migrations:migrate -n
bin/console doctrine:migrations:migrate first -n
env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai
APP_ENV: dev
MAILER_URL: null://localhost
- name: Run migrations on SQLite
run: |
bin/console doctrine:database:create -n
bin/console doctrine:migrations:migrate -n
bin/console doctrine:migrations:migrate first -n
env:
DATABASE_URL: sqlite:///%kernel.project_dir%/var/data/kimai_migrations.sqlite
APP_ENV: dev
MAILER_URL: null://localhost
- name: Run integration tests
run: composer kimai:tests-integration

View File

@@ -1,5 +1,5 @@
unreleased=true
future-release=1.2
future-release=1.8
exclude-labels=duplicate,question,invalid,wontfix,release
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt
issues-wo-labels=false

12
.gitignore vendored
View File

@@ -1,11 +1,15 @@
# ========== KIMAI ==========
# IDE
.idea/
.DS_Store
/bin/*
!bin/console
/config/packages/local.yaml
/config/packages/*-local.yaml
/config/packages/*/local.yaml
/config/packages/*/*-local.yaml
public/avatars/*.png
templates/invoice/renderer/.~lock*
@@ -18,6 +22,8 @@ templates/invoice/renderer/.~lock*
/var/invoices/*
/var/packages/*
!var/packages/.gitkeep
/var/plugins/*
!var/plugins/.gitkeep
@@ -34,10 +40,6 @@ templates/invoice/renderer/.~lock*
/vendor/
###< symfony/framework-bundle ###
###> symfony/web-server-bundle ###
.web-server-pid
###< symfony/web-server-bundle ###
###> phpunit/phpunit ###
/phpunit.xml
/.phpunit.result.cache

View File

@@ -1,78 +0,0 @@
dist: xenial
language: php
cache:
directories:
- $HOME/.composer/cache/files
matrix:
include:
- stage: Code quality
env: CODECOVERAGE=0 DB=sqlite
php: '7.2'
script:
- composer validate --no-check-all --strict
- composer kimai:codestyle
- composer kimai:phpstan
- stage: Test
php: '7.2'
env: CODECOVERAGE=0 DB=mysql
sudo: required
services:
- mysql
- stage: Test
php: '7.3'
env: CODECOVERAGE=0 DB=mysql
sudo: required
services:
- mysql
- stage: Test
php: '7.2'
env: CODECOVERAGE=0 DB=sqlite
- stage: Test
#php: '7.4snapshot'
php: '7.3'
env: CODECOVERAGE=0 DB=sqlite
- stage: Test
php: '7.2'
env: CODECOVERAGE=0 DB=mariadb
addons:
mariadb: '10.2'
services:
- mysql
before_script:
- mysql -u root -e 'CREATE USER IF NOT EXISTS travis@localhost; GRANT ALL ON *.* TO travis@localhost;'
- stage: Test
php: '7.3'
env: CODECOVERAGE=0 DB=mariadb
addons:
mariadb: '10.2'
services:
- mysql
before_script:
- mysql -u root -e 'CREATE USER IF NOT EXISTS travis@localhost; GRANT ALL ON *.* TO travis@localhost;'
- stage: Code coverage
php: '7.3'
env: CODECOVERAGE=1 DB=sqlite
after_success:
- if [[ $CODECOVERAGE == 1 ]]; then bash <(curl -s https://codecov.io/bash); fi
before_install:
- if [[ $CODECOVERAGE == 0 ]]; then phpenv config-rm xdebug.ini; fi;
- composer self-update
- php -i
- php -m
install:
- composer install
- composer require zendframework/zend-ldap
script:
- if [[ $CODECOVERAGE == 1 ]]; then vendor/bin/phpunit tests/ --coverage-clover=coverage.xml; fi;
- if [[ $CODECOVERAGE == 0 ]]; then vendor/bin/phpunit tests/; fi;
- if [[ $DB == 'sqlite' ]]; then cp tests/.env.dist.sqlite .env; fi;
- if [[ $DB == "mysql" || $DB == "mariadb" ]]; then cp tests/.env.dist.mysql .env; fi;
- bin/console doctrine:database:create -n
- bin/console doctrine:migrations:migrate -n
- bin/console doctrine:migrations:migrate first -n

View File

@@ -1,75 +1,125 @@
# Change Log
## [1.2](https://github.com/kevinpapst/kimai2/tree/1.2) (2019-08-28)
## [1.8](https://github.com/kevinpapst/kimai2/tree/1.8)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.7...1.8)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.8)
## [1.7](https://github.com/kevinpapst/kimai2/tree/1.7)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6.2...1.7)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.7)
## [1.6.2](https://github.com/kevinpapst/kimai2/tree/1.6.2)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6.1...1.6.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6.2)
## [1.6.1](https://github.com/kevinpapst/kimai2/tree/1.6.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6...1.6.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1)
## [1.6](https://github.com/kevinpapst/kimai2/tree/1.6)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.5...1.6)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6)
## [1.5](https://github.com/kevinpapst/kimai2/tree/1.5)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4.2...1.5)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.5)
## [1.4.2](https://github.com/kevinpapst/kimai2/tree/1.4)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4.1...1.4.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4.2)
## [1.4.1](https://github.com/kevinpapst/kimai2/tree/1.4)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4...1.4.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4.1)
## [1.4](https://github.com/kevinpapst/kimai2/tree/1.4)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.3...1.4)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4)
## [1.3](https://github.com/kevinpapst/kimai2/tree/1.3)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.2...1.3)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.3)
## [1.2](https://github.com/kevinpapst/kimai2/tree/1.2)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.1...1.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
## [1.1](https://github.com/kevinpapst/kimai2/tree/1.1) (2019-07-24)
## [1.1](https://github.com/kevinpapst/kimai2/tree/1.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.0.1...1.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.1)
## [1.0.1](https://github.com/kevinpapst/kimai2/tree/1.0.1) (2019-07-05)
## [1.0.1](https://github.com/kevinpapst/kimai2/tree/1.0.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.0...1.0.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.0.1)
## [1.0](https://github.com/kevinpapst/kimai2/tree/1.0) (2019-07-02)
## [1.0](https://github.com/kevinpapst/kimai2/tree/1.0)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.9...1.0)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.0)
## [0.9](https://github.com/kevinpapst/kimai2/tree/0.9) (2019-04-29)
## [0.9](https://github.com/kevinpapst/kimai2/tree/0.9)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8.1...0.9)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.9)
## [0.8.1](https://github.com/kevinpapst/kimai2/tree/0.8.1) (2019-02-22)
## [0.8.1](https://github.com/kevinpapst/kimai2/tree/0.8.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8...0.8.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1)
## [0.8](https://github.com/kevinpapst/kimai2/tree/0.8) (2019-02-19)
## [0.8](https://github.com/kevinpapst/kimai2/tree/0.8)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.7...0.8)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.8)
## [0.7](https://github.com/kevinpapst/kimai2/tree/0.7) (2019-01-28)
## [0.7](https://github.com/kevinpapst/kimai2/tree/0.7)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6.1...0.7)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.7)
## [0.6.1](https://github.com/kevinpapst/kimai2/tree/0.6.1) (2018-11-19)
## [0.6.1](https://github.com/kevinpapst/kimai2/tree/0.6.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6...0.6.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.6.1)
## [0.6](https://github.com/kevinpapst/kimai2/tree/0.6) (2018-11-18)
## [0.6](https://github.com/kevinpapst/kimai2/tree/0.6)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.5...0.6)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.6)
## [0.5](https://github.com/kevinpapst/kimai2/tree/0.5) (2018-09-27)
## [0.5](https://github.com/kevinpapst/kimai2/tree/0.5)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.4...0.5)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.5)
## [0.4](https://github.com/kevinpapst/kimai2/tree/0.4) (2018-09-01)
## [0.4](https://github.com/kevinpapst/kimai2/tree/0.4)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.3...0.4)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.4)
## [0.3](https://github.com/kevinpapst/kimai2/tree/0.3) (2018-07-22)
## [0.3](https://github.com/kevinpapst/kimai2/tree/0.3)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.2...0.3)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.3)
## [0.2](https://github.com/kevinpapst/kimai2/tree/0.2) (2018-06-23)
## [0.2](https://github.com/kevinpapst/kimai2/tree/0.2)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.1...0.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.2)
## [0.1](https://github.com/kevinpapst/kimai2/tree/0.1) (2018-01-10)
## [0.1](https://github.com/kevinpapst/kimai2/tree/0.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.1)

View File

@@ -33,7 +33,8 @@ RUN apt update && \
zip && \
apt remove -y wget && \
apt -y autoremove && \
apt clean
apt clean && \
cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
FROM tmp_kimai2_base

View File

@@ -1,9 +1,9 @@
# Kimai 2 - online time-tracker
[![CI Status](https://github.com/kevinpapst/kimai2/workflows/CI/badge.svg)](https://github.com/kevinpapst/kimai2/actions)
[![Code Coverage](https://codecov.io/gh/kevinpapst/kimai2/branch/master/graph/badge.svg)](https://codecov.io/gh/kevinpapst/kimai2)
[![Latest Stable Version](https://poser.pugx.org/kevinpapst/kimai2/v/stable)](https://packagist.org/packages/kevinpapst/kimai2)
[![License](https://poser.pugx.org/kevinpapst/kimai2/license)](https://packagist.org/packages/kevinpapst/kimai2)
[![Travis Status](https://travis-ci.org/kevinpapst/kimai2.svg?branch=master)](https://travis-ci.org/kevinpapst/kimai2)
[![Code Coverage](https://codecov.io/gh/kevinpapst/kimai2/branch/master/graph/badge.svg)](https://codecov.io/gh/kevinpapst/kimai2)
[![Gitter](https://badges.gitter.im/kimai2/support.svg)](https://gitter.im/kimai2/support)
[![Bountysource](https://img.shields.io/bountysource/team/kimai2/activity)](https://www.bountysource.com/teams/kimai2)
@@ -20,19 +20,19 @@ It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Do
### Requirements
- PHP 7.2 or higher
- Database (MySQL, MariaDB, SQLite)
- PHP 7.2.9 or higher
- Database (MySQL/MariaDB, SQLite for development)
- Webserver (nginx, Apache)
- A modern browser
- [Other libraries](https://www.kimai.org/download/)
### About
This is new version of the open source timetracker Kimai. It is stable and production ready, ships
This is the new version of the open source timetracker Kimai. It is stable and production ready, ships
with most advanced features from Kimai 1 and many new ones, including but not limited to:
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
LDAP and built-in authentication, customizable role permissions, responsive and ready for your mobile device,
authentication via SAML/LDAP/Database, customizable role permissions, responsive and ready for your mobile device,
hourly and fixed rates, advanced filtering, money and time budgets with report, support for plugins and many more.
## Installation
@@ -59,9 +59,9 @@ You can see a rough development roadmap in the [Milestones](https://github.com/k
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome.
> 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.
> You can upgrade Kimai at any time , you don't need to wait for the next official release.
Release versions will be created on a regular base (approx. every second month) and you can use these tags if you are familiar with git.
Release versions will be created on a regular base (approx. one release per month) and you can should use these tags if you are not familiar with git.
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 my effort into the software instead of backporting changes for old versions.

View File

@@ -7,8 +7,8 @@ As announced in the [README](README.md) I only support the latest available rele
| Version | Supported |
| ------- | ------------------ |
| master | :white_check_mark: |
| 1.2 | :white_check_mark: |
| < 1.2 | :x: |
| 1.7 | :white_check_mark: |
| < 1.7 | :x: |
## Reporting a Vulnerability

View File

@@ -8,28 +8,116 @@ you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [1.8](https://github.com/kevinpapst/kimai2/releases/tag/1.8)
- New mailer library: check if emails are still working (eg. by using the "password forgotten" function) or if you need to adjust your configuration, [see docs at symfony.com](https://symfony.com/doc/current/components/mailer.html#transport)
- Support for line breaks in multiline invoice fields for spreadsheets (check your invoice templates after the update)
Permission changes:
- `comments_create_customer` - NEW: permission that allows to add new comments for customers
- `comments_create_team_customer` - NEW: permission that allows to add new comments for team members of the current customer
- `comments_create_teamlead_customer` - NEW: permission that allows to add new comments for a teamlead of the current customer
- `comments_create_project` - NEW: permission that allows to add new comments for project
- `comments_create_team_project` - NEW: permission that allows to add new comments for team members of the current project
- `comments_create_teamlead_project` - NEW: permission that allows to add new comments for a teamlead of the current project
- `edit_teamlead_project` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
- `edit_teamlead_customer` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
- `upload_invoice_template` - NEW: permission that allows to upload invoice documents from the UI
## [1.7](https://github.com/kevinpapst/kimai2/releases/tag/1.7)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
New permissions:
- `comments_customer` - show comment list on customer detail page (new feature)
- `details_customer` - show detail information for customers (customer number, vat, rates, meta-fields, assigned teams ...)
- `comments_project` - show comment list on project detail page (new feature)
- `details_project` - show detail information for projects (rates, meta-fields, assigned teams ...)
If you are using teams, please read on: The following list of permissions are now also available in the UI and they (can) replace the `X_project` and `X_customer` permissions.
They are more strict, as they allow only access to team specific items, the older permissions without `_teamlead_`/`_team_` work on a global level instead.
- `view_teamlead_customer`, `edit_teamlead_customer`, `budget_teamlead_customer`, `permissions_teamlead_customer`, `comments_teamlead_customer`, `details_teamlead_customer` - allows access to customer data when user is teamlead of a team assigned to the customer (replaces more global permission like `view_customer` for teamleads)
- `view_team_customer`, `edit_team_customer`, `budget_team_customer`, `comments_team_customer`, `details_team_customer` - allows access to customer data when user is member of a team assigned to the customer (replaces more global permission like `view_customer` for users)
- `view_teamlead_project`, `edit_teamlead_project`, `budget_teamlead_project`, `permissions_teamlead_project`, `comments_teamlead_project`, `details_teamlead_project` - allows access to customer data when user is teamlead of a team assigned to the project (replaces more global permission like `view_project` for teamleads)
- `view_team_project`, `edit_team_project`, `budget_team_project`, `comments_team_project`, `details_team_project` - allows access to customer data when user is member of a team assigned to the project (replaces more global permission like `view_project` for users)
### ExpenseBundle
**ATTENTION** due to incompatibilities in the underlying frameworks users of the ExpenseBundle need to do one more step:
You need to delete the bundle before updating: `rm -r var/plugins/ExpenseBundle`, otherwise you will run into errors during the update.
After the Kimai update was successful, you have to re-install the latest bundle version, which is compatible with Kimai 1.7 only.
### Developer
- Projects now have a start and end date and the API will only return those, which are either unconfigured or currently active, you might want to reload the list of projects once the user entered begin and end datetime OR use the new `ignoreDates` parameter.
- Doctrine bundle was updated to v2, check your code for [the usage of RegistryInterface and ObjectManager](https://github.com/doctrine/DoctrineBundle/blob/master/UPGRADE-2.0.md)
- Removed the webserver bundle and the command `server:run` - see [docs](https://www.kimai.org/documentation/developers.html)
## [1.6](https://github.com/kevinpapst/kimai2/releases/tag/1.6), [1.6.1](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1), [1.6.2](https://github.com/kevinpapst/kimai2/releases/tag/1.6.2)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- Invoice changes:
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates). Using them? Install them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html)).
- Added new invoice fields (VAT, contact, payment details) and customer field (VAT). Used the twig settings before? Move them to the respective invoice template settings.
- Permissions can be managed via Admin UI. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html).
- Important permission change: regular users with the `view_other_timesheet` permission could see all timesheets. This was a legacy from the time before team permissions were introduced. If you rely on this behavior, you need to create a team with all users and the teamlead being the user who needs access to all timesheets.
### Developer
Please add default permissions to your [plugin](https://www.kimai.org/documentation/plugins.html).
## [1.5](https://github.com/kevinpapst/kimai2/releases/tag/1.5)
[Update as usual](https://www.kimai.org/documentation/updates.html)
## [1.4](https://github.com/kevinpapst/kimai2/releases/tag/1.4)
**There is a new directory, which needs to be writable by the webserver: `public/avatars/`.**
New permission (used in new dashboard widget):
- `view_team_member` - display team assignments (names, teamleads and members) for the current user
Activated Javascript select component by default (check mobile devices).
### Developer: BC breaks
- Dashboard widgets and rows need to define their `type` by FQCN
- Switched to Symfony 4.3 event types, this could fail in plugins, but only if they didn't use the official constants for event names
## [1.3](https://github.com/kevinpapst/kimai2/releases/tag/1.3)
Added `manage_tag` permission for new tag features
### Developer: BC breaks
- Refactored toolbars and search, plugins needs to be checked
- Invoices now supports multiple repositories, some method signatures had to be changed (eg. `calculateSumIdentifier()`)
## [1.2](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
### Possible BC breaks
**If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.**
- Deleted timezone conversion command. If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.
- Deleted timezone conversion command.
- Minimum password length raised from 5 to 8 character (applies only for password changes and new users)
- Maximum customer name length lowered to 150 character
- Maximum project name length lowered to 150 character
- Maximum activity name length lowered to 150 character
- Added new permission: `manage_invoice_template`
- Removed permissions: `view_invoice_template`, `create_invoice_template`, `edit_invoice_template`, `delete_invoice_template`
- Removed permission: `view_invoice` (using `create_export` only)
- Removed permission: `view_export` (using `create_export` only)
### Developer: BC breaks
- Custom export renderer need to check for usage of `Timesheet::getEnd()` as running entries can now be exported as well
## [1.1](https://github.com/kevinpapst/kimai2/releases/tag/1.1)
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0 / 1.0.1.
## [1.0.1](https://github.com/kevinpapst/kimai2/releases/tag/1.0.1)
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0.
## [1.0](https://github.com/kevinpapst/kimai2/releases/tag/1.0)
This release contains several changes, as I still have the goal to stabilize the code base to prevent

View File

@@ -11,27 +11,58 @@ global.$ = global.jQuery = $;
require('bootstrap-sass');
require('jquery-slimscroll');
require('bootstrap-select');
require('select2');
require('select2/dist/js/i18n/ar');
require('select2/dist/js/i18n/cs');
require('select2/dist/js/i18n/da');
require('select2/dist/js/i18n/de');
require('select2/dist/js/i18n/es');
require('select2/dist/js/i18n/eu');
require('select2/dist/js/i18n/fr');
require('select2/dist/js/i18n/hu');
require('select2/dist/js/i18n/it');
require('select2/dist/js/i18n/ja');
require('select2/dist/js/i18n/ko');
require('select2/dist/js/i18n/nl');
require('select2/dist/js/i18n/pl');
require('select2/dist/js/i18n/pt-BR');
require('select2/dist/js/i18n/ru');
require('select2/dist/js/i18n/sk');
require('select2/dist/js/i18n/sv');
require('select2/dist/js/i18n/tr');
require('select2/dist/js/i18n/zh-CN');
const Moment = require('moment');
global.moment = Moment;
require('moment/locale/ar');
require('moment/locale/cs');
require('moment/locale/da');
require('moment/locale/de');
require('moment/locale/de-ch');
require('moment/locale/es');
require('moment/locale/eu');
require('moment/locale/fr');
require('moment/locale/hu');
require('moment/locale/it');
require('moment/locale/ja');
require('moment/locale/ko');
require('moment/locale/nl');
require('moment/locale/pl');
require('moment/locale/pt-br');
require('moment/locale/ru');
require('moment/locale/sk');
require('moment/locale/sv');
require('moment/locale/tr');
require('moment/locale/zh-cn');
require('daterangepicker');
const Sortable = require('sortablejs/Sortable.min');
global.Sortable = Sortable;
// ------ AdminLTE framework ------
require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte.scss');
require('./sass/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');
@@ -46,6 +77,8 @@ require('./sass/app.scss');
// ------ Kimai itself ------
require('./js/KimaiWebLoader.js');
global.KimaiPaginatedBoxWidget = require('./js/widgets/KimaiPaginatedBoxWidget').default;
global.KimaiReloadPageWidget = require('./js/widgets/KimaiReloadPageWidget').default;
// ------ Autocomplete for tags only ------
require('jquery-ui/ui/widgets/autocomplete');
require('jquery-ui/ui/widgets/autocomplete');

View File

@@ -2,16 +2,25 @@
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/cs');
require('fullcalendar/dist/locale/da');
require('fullcalendar/dist/locale/de');
require('fullcalendar/dist/locale/de-ch');
require('fullcalendar/dist/locale/es');
require('fullcalendar/dist/locale/eu');
require('fullcalendar/dist/locale/fr');
require('fullcalendar/dist/locale/hu');
require('fullcalendar/dist/locale/pt-br');
require('fullcalendar/dist/locale/sv');
require('fullcalendar/dist/locale/it');
require('fullcalendar/dist/locale/ja');
require('fullcalendar/dist/locale/ko');
require('fullcalendar/dist/locale/nl');
require('fullcalendar/dist/locale/pl');
require('fullcalendar/dist/locale/pt-br');
require('fullcalendar/dist/locale/ru');
require('fullcalendar/dist/locale/sk');
require('fullcalendar/dist/locale/sv');
require('fullcalendar/dist/locale/tr');
require('fullcalendar/dist/locale/zh-cn');
require('fullcalendar/dist/fullcalendar.min.css');

1348
assets/css/emails.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,6 @@ import KimaiContainer from "./KimaiContainer";
import KimaiActiveRecordsDuration from './plugins/KimaiActiveRecordsDuration.js';
import KimaiDatatableColumnView from './plugins/KimaiDatatableColumnView.js';
import KimaiThemeInitializer from "./plugins/KimaiThemeInitializer";
import KimaiJqueryPluginInitializer from "./plugins/KimaiJqueryPluginInitializer";
import KimaiDateRangePicker from "./plugins/KimaiDateRangePicker";
import KimaiDatatable from "./plugins/KimaiDatatable";
import KimaiToolbar from "./plugins/KimaiToolbar";
@@ -31,13 +30,18 @@ import KimaiEvent from "./plugins/KimaiEvent";
import KimaiAPILink from "./plugins/KimaiAPILink";
import KimaiAlert from "./plugins/KimaiAlert";
import KimaiAutocomplete from "./plugins/KimaiAutocomplete";
import KimaiToolbarAction from "./plugins/KimaiToolbarAction";
import KimaiFormSelect from "./plugins/KimaiFormSelect";
import KimaiForm from "./plugins/KimaiForm";
import KimaiDatePicker from "./plugins/KimaiDatePicker";
import KimaiConfirmationLink from "./plugins/KimaiConfirmationLink";
import KimaiMultiUpdateTable from "./plugins/KimaiMultiUpdateTable";
import KimaiDateUtils from "./plugins/KimaiDateUtils";
export default class KimaiLoader {
constructor(configurations, translations) {
// set the current locale for all javascript components
moment.locale(configurations['locale']);
moment.locale(configurations['locale'].replace('_', '-').toLowerCase());
const kimai = new KimaiContainer(
new KimaiConfiguration(configurations),
@@ -47,14 +51,16 @@ export default class KimaiLoader {
kimai.registerPlugin(new KimaiEvent());
kimai.registerPlugin(new KimaiAPI());
kimai.registerPlugin(new KimaiAlert());
kimai.registerPlugin(new KimaiDateUtils());
kimai.registerPlugin(new KimaiFormSelect('.selectpicker'));
kimai.registerPlugin(new KimaiConfirmationLink('confirmation-link'));
kimai.registerPlugin(new KimaiActiveRecordsDuration('[data-since]'));
kimai.registerPlugin(new KimaiDatatableColumnView('data-column-visibility'));
kimai.registerPlugin(new KimaiThemeInitializer());
kimai.registerPlugin(new KimaiJqueryPluginInitializer());
kimai.registerPlugin(new KimaiDateRangePicker('.content-wrapper'));
kimai.registerPlugin(new KimaiDateTimePicker('.content-wrapper'));
kimai.registerPlugin(new KimaiDatatable('table.dataTable'));
kimai.registerPlugin(new KimaiToolbar());
kimai.registerPlugin(new KimaiDateRangePicker('input[data-daterangepickerenable="on"]'));
kimai.registerPlugin(new KimaiDateTimePicker('input[data-datetimepicker="on"]'));
kimai.registerPlugin(new KimaiDatePicker('input[data-datepickerenable="on"]'));
kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable'));
kimai.registerPlugin(new KimaiToolbar('form.header-search', 'toolbar-action'));
kimai.registerPlugin(new KimaiSelectDataAPI('select[data-related-select]'));
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
@@ -62,7 +68,9 @@ export default class KimaiLoader {
kimai.registerPlugin(new KimaiActiveRecords('li.messages-menu', 'li.messages-menu-empty'));
kimai.registerPlugin(new KimaiAPILink('api-link'));
kimai.registerPlugin(new KimaiAutocomplete('.js-autocomplete'));
kimai.registerPlugin(new KimaiToolbarAction('toolbar-action'));
kimai.registerPlugin(new KimaiForm());
kimai.registerPlugin(new KimaiThemeInitializer());
kimai.registerPlugin(new KimaiMultiUpdateTable());
//kimai.registerPlugin(new KimaiPauseRecord('li.messages-menu ul.menu li'));
// notify all listeners that Kimai plugins can now be registered

View File

@@ -5,7 +5,7 @@
* file that was distributed with this source code.
*/
import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTableRow";
import KimaiPlugin from "../KimaiPlugin";
/**
* Needs to be initialized with a class name.
@@ -18,7 +18,7 @@ import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTabl
*
* @param selector
*/
export default class KimaiAPILink extends KimaiClickHandlerReducedInTableRow {
export default class KimaiAPILink extends KimaiPlugin {
constructor(selector) {
super();

View File

@@ -49,7 +49,7 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
const since = record.getAttribute('data-since');
const format = record.getAttribute('data-format');
const duration = this.formatDuration(since, format);
if (record.getAttribute('data-title') !== null) {
if (record.getAttribute('data-title') !== null && duration !== '?') {
durations.push(duration);
}
record.textContent = duration;
@@ -75,6 +75,10 @@ export default class KimaiActiveRecordsDuration extends KimaiPlugin {
let minutes = duration.minutes();
let seconds = duration.seconds();
if (hours < 0 || minutes < 0 || seconds < 0) {
return '?';
}
// special case for hours, as they can overflow the 24h barrier - Kimai does not support days as duration unit
if (hours.length === 1) {
hours = '0' + hours;

View File

@@ -13,9 +13,9 @@
*/
import jQuery from 'jquery';
import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTableRow";
import KimaiReducedClickHandler from "./KimaiReducedClickHandler";
export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableRow {
export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
constructor(selector) {
super();
@@ -29,7 +29,25 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
init() {
const self = this;
this._addClickHandlerReducedInTableRow(this.selector, function(href) {
this.modal = jQuery('#remote_form_modal');
this.modal.on('hide.bs.modal', function () {
self.getContainer().getPlugin('event').trigger('modal-hide');
});
this.modal.on('hidden.bs.modal', function () {
// kill all references, so GC can kick in
self.getContainer().getPlugin('form').destroyForm(self._getFormIdentifier());
jQuery('#remote_form_modal .modal-body').replaceWith('');
});
this.modal.on('show.bs.modal', function () {
self.getContainer().getPlugin('event').trigger('modal-show');
});
this.modal.on('shown.bs.modal', function () {
// workaround for autofocus attribute, as the modal "steals" it
jQuery(self._getFormIdentifier()).find('input[type=text],textarea,select').filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
this._addClickHandler(this.selector, function(href) {
self.openUrlInModal(href);
});
}
@@ -39,12 +57,9 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
if (errorHandler === undefined) {
errorHandler = function(xhr, err) {
if (xhr.status !== undefined && xhr.status === 403) {
const alert = self.getContainer().getPlugin('alert');
alert.error(xhr.statusText);
return;
if (xhr.status === undefined || xhr.status !== 403) {
window.location = url;
}
window.location = url;
};
}
@@ -57,17 +72,26 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
});
}
/**
* Returns the CSS selector for the modal form.
*
* @returns {string}
* @private
*/
_getFormIdentifier() {
return '#remote_form_modal .modal-content form';
}
_openFormInModal(html) {
const self = this;
// the modal that we use to render the form in
let formIdentifier = '#remote_form_modal .modal-content form';
let formIdentifier = this._getFormIdentifier();
// if any of these is found in a response, the form will be re-displayed
let flashErrorIdentifier = 'div.alert-error';
// messages to show above the form
let flashMessageIdentifier = 'div.alert';
let form = jQuery(formIdentifier);
let remoteModal = jQuery('#remote_form_modal');
let remoteModal = this.modal;
// will be (re-)activated later
form.off('submit');
@@ -90,11 +114,7 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
);
// activate new loaded widgets
self.getContainer().getPlugin('date-time-picker').activateDateTimePicker(formIdentifier);
self.getContainer().getPlugin('autocomplete').activateAutocomplete(formIdentifier + " .js-autocomplete");
// activate selectpicker if beta test is active
jQuery('.selectpicker').selectpicker('refresh');
self.getContainer().getPlugin('form').activateForm(formIdentifier);
}
// show error flash messages
@@ -113,11 +133,7 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
});
// -----------------------------------------------------------------------
// workaround for autofocus attribute, as the modal "steals" it
remoteModal.on('shown.bs.modal', function () {
jQuery(this).find('input[type=text],textarea,select').filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
this.getContainer().getPlugin('toolbar').hide();
remoteModal.modal('show');
// the new form that was loaded via ajax
@@ -181,6 +197,10 @@ export default class KimaiAjaxModalForm extends KimaiClickHandlerReducedInTableR
err = '[' + xhr.status +'] ' + xhr.statusText;
}
alert.error(message, err);
// this is useful for changing form fields and retrying to save (and in development to test form changes)
setTimeout(function() {
btn.button('reset');
}, 1500);
}
});
});

View File

@@ -27,7 +27,7 @@ export default class KimaiAlert extends KimaiPlugin {
message = translation.get(message);
}
Swal.fire({
type: 'error',
icon: 'error',
title: title.replace('%reason%', ''),
text: message,
});
@@ -42,10 +42,11 @@ export default class KimaiAlert extends KimaiPlugin {
Swal.fire({
timer: 2000,
timerProgressBar: true,
toast: true,
position: 'top-end',
showConfirmButton: false,
type: 'success',
icon: 'success',
title: message,
});
}
@@ -65,7 +66,7 @@ export default class KimaiAlert extends KimaiPlugin {
Swal.fire({
title: message,
type: 'question',
icon: 'question',
showCancelButton: true,
confirmButtonText: translation.get('confirm'),
cancelButtonText: translation.get('cancel')

View File

@@ -13,9 +13,9 @@
*/
import jQuery from 'jquery';
import KimaiClickHandlerReducedInTableRow from "./KimaiClickHandlerReducedInTableRow";
import KimaiReducedClickHandler from "./KimaiReducedClickHandler";
export default class KimaiAlternativeLinks extends KimaiClickHandlerReducedInTableRow {
export default class KimaiAlternativeLinks extends KimaiReducedClickHandler {
constructor(selector) {
super();
@@ -23,7 +23,7 @@ export default class KimaiAlternativeLinks extends KimaiClickHandlerReducedInTab
}
init() {
this._addClickHandlerReducedInTableRow(this.selector, function(href) {
this._addClickHandler(this.selector, function(href) {
window.location = href;
});
}

View File

@@ -20,7 +20,7 @@ export default class KimaiAutocomplete extends KimaiPlugin {
}
init() {
this.activateAutocomplete(this.selector);
this.minChars = this.getContainer().getConfiguration().get('autoComplete');
}
getId() {
@@ -35,60 +35,72 @@ export default class KimaiAutocomplete extends KimaiPlugin {
return this.splitTagList(term).pop();
}
activateAutocomplete(selector)
{
const apiUrl = jQuery(selector).attr('data-autocomplete-url');
activateAutocomplete(selector) {
const self = this;
const API = self.getContainer().getPlugin('api');
jQuery(selector + ' ' + this.selector).each(function(index) {
const currentField = jQuery(this);
const apiUrl = currentField.attr('data-autocomplete-url');
const API = self.getContainer().getPlugin('api');
jQuery(selector)
// don't navigate away from the field on tab when selecting an item
.on("keydown", function (event) {
if (event.keyCode === jQuery.ui.keyCode.TAB &&
jQuery(this).autocomplete("instance").menu.active) {
event.preventDefault();
}
})
.autocomplete({
source: function (request, response) {
const lastEntry = self.extractLastTag(request.term);
API.get(apiUrl, {'name': lastEntry}, function(data){
response(data);
});
},
search: function () {
// custom minLength
var term = self.extractLastTag(this.value);
if (term.length < 2) {
return false;
currentField
// don't navigate away from the field on tab when selecting an item
.on("keydown", function (event) {
if (event.keyCode === jQuery.ui.keyCode.TAB &&
jQuery(this).autocomplete("instance").menu.active) {
event.preventDefault();
}
},
focus: function () {
// prevent value inserted on focus
return false;
},
select: function (event, ui) {
var terms = self.splitTagList(this.value);
})
.autocomplete({
source: function (request, response) {
const lastEntry = self.extractLastTag(request.term);
API.get(apiUrl, {'name': lastEntry}, function(data){
response(data);
});
},
search: function () {
// custom minLength
var term = self.extractLastTag(this.value);
if (term.length < self.minChars) {
return false;
}
},
focus: function () {
// prevent value inserted on focus
return false;
},
select: function (event, ui) {
var terms = self.splitTagList(this.value);
// remove the current input
terms.pop();
// remove the current input
terms.pop();
// check if selected tag is already in list
if (!terms.includes(ui.item.value)) {
// add the selected item
terms.push(ui.item.value);
// check if selected tag is already in list
if (!terms.includes(ui.item.value)) {
// add the selected item
terms.push(ui.item.value);
}
// add placeholder to get the comma-and-space at the end
terms.push("");
this.value = terms.join(", ");
$(this).trigger('change');
return false;
}
}
// add placeholder to get the comma-and-space at the end
terms.push("");
)
;
});
}
this.value = terms.join(", ");
$(this).trigger('change');
return false;
}
}
);
destroyAutocomplete(selector) {
jQuery(selector + ' ' + this.selector).each(function(index) {
const currentField = jQuery(this);
currentField.autocomplete("destroy");
currentField.removeData('autocomplete');
});
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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 KimaiPlugin from "../KimaiPlugin";
/**
* Needs to be initialized with a class name.
*
* Allows to assign the given selector to any element, which then is used as click-handler
* calling an API method and trigger the event from data-event attribute afterwards.
*
* @param selector
*/
export default class KimaiConfirmationLink extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
init() {
const self = this;
document.addEventListener('click', function(event) {
let target = event.target;
while (target !== null && !target.matches('body')) {
if (target.classList.contains(self.selector)) {
const attributes = target.dataset;
// is this a link?
let url = attributes['href'];
// or another HTML element with a custom href
if (!url) {
url = target.getAttribute('href');
}
// or is this a button?
let form = null;
if (target.type === 'submit' && target.form !== undefined) {
form = target.form;
}
if (attributes.question !== undefined) {
self.getContainer().getPlugin('alert').question(attributes.question, function(value) {
if (value) {
if (form === null) {
document.location = url;
} else {
if (url !== null) {
form.action = url;
}
form.submit();
}
}
});
}
event.preventDefault();
event.stopPropagation();
}
target = target.parentNode;
}
});
}
}

View File

@@ -14,9 +14,10 @@ import KimaiPlugin from "../KimaiPlugin";
export default class KimaiDatatable extends KimaiPlugin {
constructor(selector) {
constructor(contentAreaSelector, tableSelector) {
super();
this.selector = selector;
this.contentArea = contentAreaSelector;
this.selector = tableSelector;
}
getId() {
@@ -44,16 +45,26 @@ export default class KimaiDatatable extends KimaiPlugin {
for (let eventName of events.split(' ')) {
document.addEventListener(eventName, handle);
}
if (this.getContainer().getConfiguration().get('autoReloadDatatable')) {
document.addEventListener('toolbar-change', handle);
} else {
document.addEventListener('pagination-change', handle);
document.addEventListener('filter-change', handle);
}
}
reloadDatatable() {
const contentArea = this.contentArea;
const durations = this.getContainer().getPlugin('timesheet-duration');
const form = jQuery('.toolbar form');
const toolbarSelector = this.getContainer().getPlugin('toolbar').getSelector();
const form = jQuery(toolbarSelector);
let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
jQuery('section.content').append(loading);
jQuery(contentArea).append(loading);
// remove the empty fields to prevent errors
let formData = jQuery('.toolbar form :input')
let formData = jQuery(toolbarSelector + ' :input')
.filter(function(index, element) {
return jQuery(element).val() != '';
})
@@ -64,8 +75,8 @@ export default class KimaiDatatable extends KimaiPlugin {
type: form.attr('method'),
data: formData,
success: function(html) {
jQuery('section.content').replaceWith(
jQuery(html).find('section.content')
jQuery(contentArea).replaceWith(
jQuery(html).find(contentArea)
);
durations.updateRecords();
},

View File

@@ -11,7 +11,6 @@
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
import moment from 'moment';
export default class KimaiDatePicker extends KimaiPlugin {
@@ -24,13 +23,11 @@ export default class KimaiDatePicker extends KimaiPlugin {
return 'date-picker';
}
init() {
this.activateDatePicker(this.selector);
}
activateDatePicker(selector) {
let translator = this.getContainer().getTranslation();
jQuery(selector + ' input[data-datepickerenable="on"]').each(function(index) {
const TRANSLATE = this.getContainer().getTranslation();
const DATE_UTILS = this.getContainer().getPlugin('date');
jQuery(selector + ' ' + this.selector).each(function(index) {
let localeFormat = jQuery(this).data('format');
jQuery(this).daterangepicker({
singleDatePicker: true,
@@ -39,11 +36,11 @@ export default class KimaiDatePicker extends KimaiPlugin {
locale: {
format: localeFormat,
firstDay: 1,
applyLabel: translator.get('confirm'),
cancelLabel: translator.get('cancel'),
customRangeLabel: translator.get('customRange'),
daysOfWeek: moment.weekdaysShort(),
monthNames: moment.months(),
applyLabel: TRANSLATE.get('confirm'),
cancelLabel: TRANSLATE.get('cancel'),
customRangeLabel: TRANSLATE.get('customRange'),
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
monthNames: DATE_UTILS.getMonthNames(),
}
});
@@ -54,4 +51,13 @@ export default class KimaiDatePicker extends KimaiPlugin {
});
}
destroyDatePicker(selector) {
jQuery(selector + ' ' + this.selector).each(function(index) {
if (jQuery(this).data('daterangepicker') !== undefined) {
jQuery(this).daterangepicker('destroy');
jQuery(this).data('daterangepicker').remove();
}
});
}
}

View File

@@ -24,25 +24,23 @@ export default class KimaiDateRangePicker extends KimaiPlugin {
return 'date-range-picker';
}
init() {
this.activateDateRangePicker(this.selector);
}
activateDateRangePicker(selector) {
let translator = this.getContainer().getTranslation();
jQuery(selector + ' input[data-daterangepickerenable="on"]').each(function(index) {
const TRANSLATE = this.getContainer().getTranslation();
const DATE_UTILS = this.getContainer().getPlugin('date');
jQuery(selector + ' ' + this.selector).each(function(index) {
let localeFormat = jQuery(this).data('format');
let separator = jQuery(this).data('separator');
let rangesList = {};
rangesList[translator.get('today')] = [moment(), moment()];
rangesList[translator.get('yesterday')] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
rangesList[translator.get('thisWeek')] = [moment().startOf('week'), moment().endOf('week')];
rangesList[translator.get('lastWeek')] = [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')];
rangesList[translator.get('thisMonth')] = [moment().startOf('month'), moment().endOf('month')];
rangesList[translator.get('lastMonth')] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')];
rangesList[translator.get('thisYear')] = [moment().startOf('year'), moment().endOf('year')];
rangesList[translator.get('lastYear')] = [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')];
rangesList[TRANSLATE.get('today')] = [moment(), moment()];
rangesList[TRANSLATE.get('yesterday')] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
rangesList[TRANSLATE.get('thisWeek')] = [moment().startOf('week'), moment().endOf('week')];
rangesList[TRANSLATE.get('lastWeek')] = [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')];
rangesList[TRANSLATE.get('thisMonth')] = [moment().startOf('month'), moment().endOf('month')];
rangesList[TRANSLATE.get('lastMonth')] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')];
rangesList[TRANSLATE.get('thisYear')] = [moment().startOf('year'), moment().endOf('year')];
rangesList[TRANSLATE.get('lastYear')] = [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')];
jQuery(this).daterangepicker({
showDropdowns: true,
@@ -53,11 +51,11 @@ export default class KimaiDateRangePicker extends KimaiPlugin {
separator: separator,
format: localeFormat,
firstDay: 1,
applyLabel: translator.get('confirm'),
cancelLabel: translator.get('cancel'),
customRangeLabel: translator.get('customRange'),
daysOfWeek: moment.weekdaysShort(),
monthNames: moment.months(),
applyLabel: TRANSLATE.get('confirm'),
cancelLabel: TRANSLATE.get('cancel'),
customRangeLabel: TRANSLATE.get('customRange'),
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
monthNames: DATE_UTILS.getMonthNames(),
},
ranges: rangesList,
alwaysShowCalendars: true
@@ -65,9 +63,20 @@ export default class KimaiDateRangePicker extends KimaiPlugin {
jQuery(this).on('apply.daterangepicker', function(ev, picker) {
jQuery(this).val(picker.startDate.format(localeFormat) + ' - ' + picker.endDate.format(localeFormat));
jQuery(this).data('begin', picker.startDate.format(localeFormat));
jQuery(this).data('end', picker.endDate.format(localeFormat));
jQuery(this).trigger("change");
});
});
}
destroyDateRangePicker(selector) {
jQuery(selector + ' ' + this.selector).each(function(index) {
if (jQuery(this).data('daterangepicker') !== undefined) {
jQuery(this).daterangepicker('destroy');
jQuery(this).data('daterangepicker').remove();
}
});
}
}

View File

@@ -11,7 +11,6 @@
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
import moment from 'moment';
export default class KimaiDateTimePicker extends KimaiPlugin {
@@ -24,30 +23,27 @@ export default class KimaiDateTimePicker extends KimaiPlugin {
return 'date-time-picker';
}
init() {
this.activateDateTimePicker(this.selector);
}
activateDateTimePicker(selector) {
let translator = this.getContainer().getTranslation();
let configuration = this.getContainer().getConfiguration();
const TRANSLATE = this.getContainer().getTranslation();
const CONFIG = this.getContainer().getConfiguration();
const DATE_UTILS = this.getContainer().getPlugin('date');
jQuery(selector + ' input[data-datetimepicker="on"]').each(function(index) {
jQuery(selector + ' ' + this.selector).each(function(index) {
let localeFormat = jQuery(this).data('format');
jQuery(this).daterangepicker({
singleDatePicker: true,
timePicker: true,
timePicker24Hour: configuration.get('twentyFourHours'),
timePicker24Hour: CONFIG.get('twentyFourHours'),
showDropdowns: true,
autoUpdateInput: false,
locale: {
format: localeFormat,
firstDay: 1,
applyLabel: translator.get('confirm'),
cancelLabel: translator.get('cancel'),
customRangeLabel: translator.get('customRange'),
daysOfWeek: moment.weekdaysShort(),
monthNames: moment.months(),
applyLabel: TRANSLATE.get('confirm'),
cancelLabel: TRANSLATE.get('cancel'),
customRangeLabel: TRANSLATE.get('customRange'),
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
monthNames: DATE_UTILS.getMonthNames(),
}
});
@@ -58,4 +54,13 @@ export default class KimaiDateTimePicker extends KimaiPlugin {
});
}
destroyDateTimePicker(selector) {
jQuery(selector + ' ' + this.selector).each(function(index) {
if (jQuery(this).data('daterangepicker') !== undefined) {
jQuery(this).daterangepicker('destroy');
jQuery(this).data('daterangepicker').remove();
}
});
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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.
*/
/*!
* [KIMAI] KimaiDateUtils: responsible for handling date specific tasks
*/
import KimaiPlugin from '../KimaiPlugin';
import moment from 'moment';
export default class KimaiDateUtils extends KimaiPlugin {
getId() {
return 'date';
}
getWeekDaysShort() {
return moment.localeData().weekdaysShort();
}
getMonthNames() {
return moment.localeData().months();
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.
*/
/*!
* [KIMAI] KimaiForm: basic functions for all forms
*/
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiForm extends KimaiPlugin {
getId() {
return 'form';
}
activateForm(formSelector, container) {
this.getContainer().getPlugin('date-range-picker').activateDateRangePicker(formSelector);
this.getContainer().getPlugin('date-time-picker').activateDateTimePicker(formSelector);
this.getContainer().getPlugin('date-picker').activateDatePicker(formSelector);
this.getContainer().getPlugin('autocomplete').activateAutocomplete(formSelector);
this.getContainer().getPlugin('form-select').activateSelectPicker(formSelector, container);
}
destroyForm(formSelector) {
this.getContainer().getPlugin('form-select').destroySelectPicker(formSelector);
this.getContainer().getPlugin('autocomplete').destroyAutocomplete(formSelector);
this.getContainer().getPlugin('date-picker').destroyDatePicker(formSelector);
this.getContainer().getPlugin('date-time-picker').destroyDateTimePicker(formSelector);
this.getContainer().getPlugin('date-range-picker').destroyDateRangePicker(formSelector);
}
}

View File

@@ -0,0 +1,168 @@
/*
* 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.
*/
/*!
* [KIMAI] KimaiFormSelect: enhanced functionality for HTML select's
*/
import KimaiPlugin from "../KimaiPlugin";
import jQuery from "jquery";
export default class KimaiFormSelect extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'form-select';
}
activateSelectPicker(selector, container) {
const elementSelector = this.selector;
let options = {};
if (container !== undefined) {
options = {
dropdownParent: $(container),
};
}
// Function to match the name of the parent and not only the names of the children
// Based on the original matcher function of Select2: https://github.com/select2/select2/blob/5765090318c4d382ae56463cfa25ba8ca7bdd495/src/js/select2/defaults.js#L272
// More information: https://select2.org/searching | https://github.com/select2/docs/blob/develop/pages/11.searching/docs.md
function matcher(params, data) {
// Always return the object if there is nothing to compare
if (jQuery.trim(params.term) === '') {
return data;
}
// Check whether options has children
let hasChildren = data.children && data.children.length > 0;
// Split search param by space to search for all terms and convert all to uppercase
let terms = params.term.toUpperCase().split(' ');
let original = data.text.toUpperCase();
// Always return the parent option including its children, when the name matches one of the params
// Check if the text contains all or at least one of the terms
let foundAll = true;
let foundOne = false;
let missingTerms = [];
terms.forEach(function(item, index) {
if (original.indexOf(item) > -1) {
foundOne = true;
} else {
foundAll = false;
missingTerms.push(item);
}
});
// If the option element contains all terms, return it
if(foundAll) {
return data;
}
// Do a recursive check for options with children
if (hasChildren) {
// If the parent already contains one or more search terms, proceed only with the missing ones
// First: Clone the original params object...
let newParams = jQuery.extend(true, {}, params);
if (foundOne) {
newParams.term = missingTerms.join(' ');
} else {
newParams.term = params.term;
}
// Clone the data object if there are children
// This is required as we modify the object to remove any non-matches
let match = jQuery.extend(true, {}, data);
// Check each child of the option
for (let c = data.children.length - 1; c >= 0; c--) {
let child = data.children[c];
let matches = matcher(newParams, child);
// If there wasn't a match, remove the object in the array
if (matches === null) {
match.children.splice(c, 1);
}
}
// If any children matched, return the new object
if (match.children.length > 0) {
return match;
}
}
// If the option or its children do not contain the term, don't return anything
return null;
}
options = {...options, ...{
language: this.getContainer().getConfiguration().get('locale').replace('_', '-'),
theme: "bootstrap",
matcher: matcher
}};
jQuery(selector + ' ' + elementSelector).select2(options);
jQuery('body').on('reset', 'form', function(event){
setTimeout(function() {
jQuery(event.target).find(elementSelector).trigger('change');
}, 10);
});
}
destroySelectPicker(selector) {
jQuery(selector + ' ' + this.selector).select2('destroy');
}
updateOptions(selectIdentifier, data) {
let select = jQuery(selectIdentifier);
let emptyOption = jQuery(selectIdentifier + ' option[value=""]');
const selectedValue = select.val();
select.find('option').remove().end().find('optgroup').remove().end();
if (emptyOption.length !== 0) {
select.append('<option value="">' + emptyOption.text() + '</option>');
}
let htmlOptions = '';
let emptyOptions = '';
for (const [key, value] of Object.entries(data)) {
if (key === '__empty__') {
for (const entity of value) {
emptyOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
}
continue;
}
htmlOptions += '<optgroup label="' + key + '">';
for (const entity of value) {
htmlOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
}
htmlOptions += '</optgroup>';
}
select.append(htmlOptions);
select.append(emptyOptions);
// if available, re-select the previous selected option (mostly usable for global activities)
select.val(selectedValue);
// 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
if (select.hasClass('selectpicker')) {
select.trigger('change.select2');
}
}
}

View File

@@ -1,24 +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.
*/
/*!
* [KIMAI] KimaiJqueryPluginInitializer: initialize jQuery plugins
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiJqueryPluginInitializer extends KimaiPlugin {
init() {
// activate the dropdown functionality
jQuery('.dropdown-toggle').dropdown();
// activate the tooltip functionality
jQuery('[data-toggle="tooltip"]').tooltip();
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.
*/
/*!
* [KIMAI] KimaiMultiUpdateForm: handle the multi update checkbox list and form
*/
import KimaiPlugin from '../KimaiPlugin';
import jQuery from "jquery";
export default class KimaiMultiUpdateTable extends KimaiPlugin {
init() {
const self = this;
jQuery('body').
on('change', '#multi_update_all', function(event) {
jQuery('.multi_update_single').prop('checked', jQuery(event.target).prop('checked'));
self.toggleForm();
})
.on('change', '.multi_update_single', function(event) {
self.toggleForm();
})
.on('change', '#multi_update_table_action', function(event) {
const selectedItem = jQuery('#multi_update_table_action option:selected');
const selectedVal = selectedItem.val();
if (selectedVal === '') {
return;
}
const form = jQuery('#multi_update_form form');
const selectedText = selectedItem.text();
const ids = self.getSelectedIds();
const question = form.attr('data-question').replace(/%action%/, selectedText).replace(/%count%/, ids.length);
self.getContainer().getPlugin('alert').question(question, function(value) {
if (value) {
form.attr('action', selectedVal).submit();
} else {
jQuery('#multi_update_table_action').val('').trigger('change');
}
});
});
}
getSelectedIds()
{
let ids = [];
jQuery('.multi_update_single:checked').each(function(i){
ids[i] = $(this).val();
});
return ids;
}
toggleForm()
{
const ids = this.getSelectedIds();
jQuery('#multi_update_table_entities').val(ids.join(','));
if (ids.length > 0) {
jQuery('#multi_update_form').show();
} else {
jQuery('#multi_update_form').hide();
}
}
}

View File

@@ -6,27 +6,28 @@
*/
/*!
* [KIMAI] KimaiClickHandlerReducedInTableRow: abstract class
* [KIMAI] KimaiReducedClickHandler: abstract class
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiClickHandlerReducedInTableRow extends KimaiPlugin {
export default class KimaiReducedClickHandler extends KimaiPlugin {
_addClickHandlerReducedInTableRow(selector, callback)  {
_addClickHandler(selector, callback)  {
jQuery('body').on('click', selector, function(event) {
// just in case an inner element is editable, than this should not be triggered
if (event.target.parentNode.isContentEditable || event.target.isContentEditable) {
return;
}
// handles the "click" on table rows to open an entry for editing: when a button within a row is clicked,
// we don't want the table row event to be processed - so we intercept it
// handles the "click" on table rows or list elements
let target = event.target;
if (event.currentTarget.matches('tr')) {
if (event.currentTarget.matches('tr') || event.currentTarget.matches('li')) {
while (target !== null && !target.matches('body')) {
if (target.matches('a') || target.matches ('button')) {
// when an element within the row is clicked, that can trigger stuff itself, we don't want the event to be processed
// don't act if a link, button or form element was clicked
if (target.matches('a') || target.matches ('button') || target.matches ('input')) {
return;
}
target = target.parentNode;

View File

@@ -11,6 +11,7 @@
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
import moment from 'moment';
export default class KimaiSelectDataAPI extends KimaiPlugin {
@@ -32,46 +33,85 @@ export default class KimaiSelectDataAPI extends KimaiPlugin {
const API = this.getContainer().getPlugin('api');
jQuery('body').on('change', selector, function(event) {
let apiUrl = jQuery(this).attr('data-api-url').replace('-s-', jQuery(this).val());
const targetSelect = '#' + jQuery(this).attr('data-related-select');
const targetSelect = '#' + this.dataset['relatedSelect'];
// if the related target select does not exist, we do not need to load the related data
if (jQuery(targetSelect).length === 0) {
return;
}
let formPrefix = jQuery(this).parents('form').first().attr('name');
if (formPrefix === undefined || formPrefix === null) {
formPrefix = '';
} else {
formPrefix += '_';
}
let newApiUrl = self._buildUrlWithFormFields(this.dataset['apiUrl'], formPrefix);
if (jQuery(this).val() === '') {
if (jQuery(this).attr('data-empty-url') === undefined) {
if (this.dataset['emptyUrl'] === undefined) {
self._updateSelect(targetSelect, {});
jQuery(targetSelect).attr('disabled', 'disabled');
return;
}
apiUrl = jQuery(this).attr('data-empty-url').replace('-s-', jQuery(this).val());
newApiUrl = self._buildUrlWithFormFields(this.dataset['emptyUrl'], formPrefix);
}
jQuery(targetSelect).removeAttr('disabled');
API.get(apiUrl, {}, function(data){
API.get(newApiUrl, {}, function(data){
self._updateSelect(targetSelect, data);
});
});
}
_buildUrlWithFormFields(apiUrl, formPrefix) {
let newApiUrl = apiUrl;
apiUrl.split('?')[1].split('&').forEach(item => {
let [key, value] = item.split('=');
let decoded = decodeURIComponent(value);
let test = decoded.match(/%(.*)%/);
if (test !== null) {
let targetField = jQuery('#' + formPrefix + test[1]);
let newValue = '';
if (targetField.length === 0) {
// debug: this case for example happens in duration only mode, when the end field is not found
//console.log('ERROR: Cannot find field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
} else {
if (targetField.val() !== null) {
newValue = targetField.val();
if (newValue !== '') {
// having that special case here is far from being perfect... but for now it works ;-)
if (targetField.data('daterangepicker') !== undefined) {
if (key === 'begin' || key === 'start' || targetField.data('daterangepicker').singleDatePicker) {
newValue = targetField.data('daterangepicker').startDate.format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
} else if (key === 'end') {
newValue = targetField.data('daterangepicker').endDate.format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
}
} else if (targetField.data('format') !== undefined) {
if (moment(newValue, targetField.data('format')).isValid()) {
newValue = moment(newValue, targetField.data('format')).format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
}
}
}
}
}
newApiUrl = newApiUrl.replace(value, newValue);
}
});
return newApiUrl;
}
_updateSelect(selectName, data) {
let select = jQuery(selectName);
let emptyOption = jQuery(selectName + ' option[value=""]');
select.find('option').remove().end().find('optgroup').remove().end();
if (emptyOption.length !== 0) {
select.append('<option value="">' + emptyOption.text() + '</option>');
}
const options = {};
for (const apiData of data) {
let title = apiData.parentTitle;
if (title === null) {
title = '__empty__';
let title = '__empty__';
if (apiData.hasOwnProperty('parentTitle') && apiData.parentTitle !== null) {
title = apiData.parentTitle;
}
if (!options.hasOwnProperty(title)) {
options[title] = [];
@@ -84,32 +124,7 @@ export default class KimaiSelectDataAPI extends KimaiPlugin {
ordered[key] = options[key];
});
let htmlOptions = '';
let emptyOptions = '';
for (const [key, value] of Object.entries(ordered)) {
if (key === '__empty__') {
for (const entity of value) {
emptyOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
}
continue;
}
htmlOptions += '<optgroup label="' + key + '">';
for (const entity of value) {
htmlOptions += '<option value="' + entity.id + '">' + entity.name + '</option>';
}
htmlOptions += '</optgroup>';
}
select.append(htmlOptions);
select.append(emptyOptions);
// if we don't trigger the change, the other selects won't be resetted
select.trigger('change');
// if the beta test kimai.theme.select_type is active, this will tell the selects to refresh
jQuery('.selectpicker').selectpicker('refresh');
this.getContainer().getPlugin('form-select').updateOptions(selectName, ordered);
}
}

View File

@@ -15,7 +15,35 @@ import KimaiPlugin from '../KimaiPlugin';
export default class KimaiThemeInitializer extends KimaiPlugin {
init() {
this.registerGlobalAjaxErrorHandler();
this.registerAutomaticAlertRemove('div.alert-success', 5000);
// activate the dropdown functionality
jQuery('.dropdown-toggle').dropdown();
// activate the tooltip functionality
jQuery('[data-toggle="tooltip"]').tooltip();
// activate all form plugins
this.getContainer().getPlugin('form').activateForm('.content-wrapper form', 'body');
}
/**
* redirect access denied / session timeouts to login page
*/
registerGlobalAjaxErrorHandler() {
const loginUrl = this.getContainer().getConfiguration().get('login');
const alert = this.getContainer().getPlugin('alert');
const translation = this.getContainer().getTranslation().get('login.required');
jQuery(document).ajaxError(function(event, jqxhr, settings, thrownError) {
if (jqxhr.status !== undefined && jqxhr.status === 403) {
const loginRequired = jqxhr.getResponseHeader('login-required');
if (loginRequired !== null) {
alert.question(translation, function (result) {
if (result === true) {
window.location.replace(loginUrl);
}
});
}
}
});
}
/**

View File

@@ -14,12 +14,183 @@ import KimaiPlugin from "../KimaiPlugin";
export default class KimaiToolbar extends KimaiPlugin {
init() {
const datatable = this.getContainer().getPlugin('datatable');
constructor(formSelector, formSubmitActionClass) {
super();
this.formSelector = formSelector;
this.actionClass = formSubmitActionClass;
}
// This catches all clicks on the pagination and prevents the default action, as we want to relad the page via JS
getId() {
return 'toolbar';
}
init() {
const formSelector = this.getSelector();
const self = this;
const EVENT = self.getContainer().getPlugin('event');
this._registerPagination(formSelector, EVENT);
this._registerSortableTables(formSelector, EVENT);
this._registerAlternativeSubmitActions(formSelector, this.actionClass);
this._registerSearchButtons(formSelector);
jQuery('body')
// prevent that the dropdown closes, when a form input is changed - eg. a select option was clicked
.on('click', formSelector + ' .dropdown-menu', function (event) {
event.stopPropagation();
})
// prevent that a click into the search field will close the dropdown
.on('click', '.select2-search__field', function (event) {
event.stopPropagation();
})
// prevent that the dropdown closes when a optgroup header is clicked
.on('click', '.select2-results__group', function (event) {
event.stopPropagation();
})
// prevent that a click into a daterangepicker will close the dropdown
.on('click', '.daterangepicker', function (event) {
event.stopPropagation();
})
// prevent that clicks in the dropdown elements, but outside of elements will close the dropdown (eg border besides the search field)
.on('click', '.select2-container', function (event) {
event.stopPropagation();
})
;
// Reset the page if filter values are changed, otherwise we might end up with a limited set of data,
// which does not support the given page - and it would be just wrong to stay in the same page
jQuery(formSelector +' input').change(function (event) {
switch (event.target.id) {
case 'order':
case 'orderBy':
case 'page':
break;
default:
jQuery(formSelector + ' input#page').val(1);
break;
}
self.triggerChange();
});
// when user selected a new customer or project, reset the pagination back to 1
// and then find out if the results should be reloaded
jQuery(formSelector + ' select').change(function (event) {
let reload = true;
switch (event.target.id) {
case 'customer':
if (jQuery(formSelector + ' select#project').length > 0) {
reload = false;
}
break;
case 'project':
if (jQuery(formSelector + ' select#activity').length > 0) {
reload = false;
}
break;
}
jQuery(formSelector + ' input#page').val(1);
if (reload) {
self.triggerChange();
}
});
// close all open selectpicker upon choosing any dropdown option
jQuery(formSelector + ' select.selectpicker').on('change', function(event) {
jQuery('.bootstrap-select.open').removeClass('open');
});
}
/**
* The search toggle button is not part of this component, but it is directly connected to it.
* @private
*/
_registerSearchButtons(formSelector) {
jQuery('body')
// only for mobile experience currently: show the search form field
.on('click', '.btn-search.search-toggle', function (event) {
event.preventDefault();
event.stopPropagation();
jQuery(formSelector).parent('section').toggleClass('search-open');
jQuery(formSelector).toggleClass('hidden-xs');
jQuery(formSelector + ' input#searchTerm').dropdown('toggle');
jQuery(formSelector + ' input#searchTerm').focus();
})
// hide the search form field
.on('click', formSelector + ' a.search-cancel', function (event) {
event.preventDefault();
event.stopPropagation();
jQuery(formSelector).parent('section').toggleClass('search-open');
jQuery(formSelector + ' input#searchTerm').dropdown('toggle');
jQuery(formSelector).toggleClass('hidden-xs');
})
;
}
/**
* Some actions utilize the filter from the search form and submit it to another URL.
* @private
*/
_registerAlternativeSubmitActions(toolbarSelector, actionBtnClass) {
document.addEventListener('click', function(event) {
let target = event.target;
while (target !== null && !target.matches('body')) {
if (target.classList.contains(actionBtnClass)) {
const form = document.querySelector(toolbarSelector);
if (form === null) {
return;
}
const prevAction = form.action;
const prevMethod = form.method;
form.target = '_blank';
form.action = target.href;
if (target.dataset.method !== undefined) {
form.method = target.dataset.method;
}
form.submit();
form.target = '';
form.action = prevAction;
form.method = prevMethod;
event.preventDefault();
event.stopPropagation();
}
target = target.parentNode;
}
});
}
/**
* Sortable datatables use hidden fields in the toolbar filter/search form
* @private
*/
_registerSortableTables(formSelector, EVENT) {
jQuery('body').on('click', 'th.sortable', function(event){
var $header = jQuery(event.target);
var order = 'DESC';
var orderBy = $header.data('order');
if ($header.hasClass('sorting_desc')) {
order = 'ASC';
}
jQuery(formSelector + ' input#orderBy').val(orderBy);
jQuery(formSelector + ' input#order').val(order);
// triggers the page reset - see below
jQuery(formSelector + ' input#order').trigger('change');
// triggers the datatable reload - search for the event name
EVENT.trigger('filter-change');
});
}
/**
* This catches all clicks on the pagination and prevents the default action, as we want to reload the page via JS
* @private
*/
_registerPagination(formSelector, EVENT) {
jQuery('body').on('click', 'div.navigation ul.pagination li a', function(event) {
let pager = jQuery(".toolbar form input[name='page']");
let pager = jQuery(formSelector + " input#page");
if (pager.length === 0) {
return;
}
@@ -29,41 +200,33 @@ export default class KimaiToolbar extends KimaiPlugin {
let page = urlParts[urlParts.length-1];
pager.val(page);
pager.trigger('change');
EVENT.trigger('pagination-change');
return false;
});
// Reset the page if any other value is changed, otherwise we might end up with a limited set
// of data which does not support the given page - and it would be just wrong to stay in the same page
jQuery('.toolbar form input').change(function (event) {
switch (event.target.id) {
case 'page':
break;
default:
jQuery('.toolbar form input#page').val(1);
}
datatable.reloadDatatable();
});
}
hide() {
const toggle = jQuery(this.getSelector() + ' #searchTerm.dropdown-toggle');
if (toggle.parent().hasClass('open')) {
toggle.dropdown('toggle');
}
}
jQuery('.toolbar form select').change(function (event) {
let reload = true;
switch (event.target.id) {
case 'customer':
if (jQuery('.toolbar form select#project').length > 0) {
reload = false;
}
break;
/**
* Triggers an event, that everyone can listen for.
*/
triggerChange() {
this.getContainer().getPlugin('event').trigger('toolbar-change');
}
case 'project':
if (jQuery('.toolbar form select#activity').length > 0) {
reload = false;
}
break;
}
jQuery('.toolbar form input#page').val(1);
if (reload) {
datatable.reloadDatatable();
}
});
/**
* Returns the CSS selector to target the toolbar form.
*
* @returns {string}
*/
getSelector() {
return this.formSelector;
}
}

View File

@@ -1,56 +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 KimaiPlugin from '../KimaiPlugin';
/**
* Needs to be initialized with a class name.
*
* A link like <a href=# class=remoteLink> can be activated with:
* new KimaiToolbarAction('remoteLink')
*
* @param selector
*/
export default class KimaiToolbarAction extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
init() {
const self = this;
document.addEventListener('click', function(event) {
let target = event.target;
while (target !== null && !target.matches('body')) {
if (target.classList.contains(self.selector)) {
const form = document.querySelector('div.toolbar form.navbar-form');
if (form === null) {
return;
}
const prevAction = form.action;
const prevMethod = form.method;
form.target = '_blank';
form.action = target.href;
if (target.dataset.method !== undefined) {
form.method = target.dataset.method;
}
form.submit();
form.target = '';
form.action = prevAction;
form.method = prevMethod;
event.preventDefault();
event.stopPropagation();
}
target = target.parentNode;
}
});
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.
*/
/*!
* [KIMAI] KimaiPaginatedBoxWidget: handles box widgets that have a pagination
*/
import jQuery from "jquery";
export default class KimaiPaginatedBoxWidget {
constructor(boxId) {
this.selector = boxId;
this.overlay = jQuery('<div class="overlay"><div class="fas fa-sync fa-spin"></div></div>');
this.widget = jQuery(this.selector);
this.href = this.widget.data('href');
this.events = this.widget.data('reload').split(' ');
const self = this;
const reloadPage = function (event) {
const page = jQuery(self.selector + ' .box-tools').data('page');
const url = self._buildUrl(page);
self.loadPage(url);
};
for (const eventName of this.events) {
document.addEventListener(eventName, reloadPage);
}
this.widget.on('click', '.box-tools a', function (event) {
event.preventDefault();
self.loadPage(jQuery(event.currentTarget).attr('href'));
});
}
static create(elementId) {
return new KimaiPaginatedBoxWidget(elementId);
}
_showOverlay() {
this.widget.append(this.overlay);
}
_hideOverlay() {
jQuery(this.overlay).remove();
}
loadPage(url) {
const self = this;
const selector = this.selector;
self._showOverlay();
jQuery.ajax({
url: url,
data: {},
success: function (response) {
const html = jQuery(response);
jQuery(selector + ' .box-tools').replaceWith(html.find('.box-tools'));
jQuery(selector + ' .box-body').replaceWith(html.find('.box-body'));
jQuery(selector + ' .box-title').replaceWith(html.find('.box-title'));
if (jQuery(selector + ' .box-footer').length > 0) {
jQuery(selector + ' .box-footer').replaceWith(html.find('.box-footer'));
}
jQuery(selector + ' .box-body [data-toggle="tooltip"]').tooltip();
self.widget.removeData('error-retry');
self._hideOverlay();
},
dataType: 'html',
error: function(jqXHR, textStatus, errorThrown) {
if (jqXHR.status !== undefined && jqXHR.status === 500) {
if (self.widget.data('error-retry') !== undefined) {
// TODO show error message ?
return;
}
const page = jQuery(selector + ' .box-tools').data('page');
if (page > 1) {
self.widget.data('error-retry', 1);
var url = self._buildUrl(page - 1);
self.loadPage(url);
}
}
self._hideOverlay();
}
});
}
_buildUrl(page) {
return this.href.replace('1', page);
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.
*/
/*!
* [KIMAI] KimaiReloadPageWidget: a simple helper to reload the page on events
*/
import jQuery from "jquery";
export default class KimaiReloadPageWidget {
constructor(events, fullReload) {
this.overlay = jQuery('<div class="overlay-wrapper"><div class="overlay"><div class="fas fa-sync fa-spin"></div></div></div>');
this.widget = jQuery('div.content-wrapper');
const self = this;
const reloadPage = function (event) {
if (fullReload) {
document.location.reload(true);
} else {
self.loadPage(document.location);
}
};
for (const eventName of events.split(' ')) {
document.addEventListener(eventName, reloadPage);
}
}
static create(events, fullReload) {
if (fullReload === undefined || fullReload === null) {
fullReload = false;
}
return new KimaiReloadPageWidget(events, fullReload);
}
_showOverlay() {
this.widget.append(this.overlay);
}
_hideOverlay() {
jQuery(this.overlay).remove();
}
loadPage(url) {
const self = this;
self._showOverlay();
jQuery.ajax({
url: url,
data: {},
success: function (response) {
jQuery('section.content').replaceWith(
jQuery(response).find('section.content')
);
document.dispatchEvent(new Event('kimai.reloadPage'));
self._hideOverlay();
},
dataType: 'html',
error: function(jqXHR, textStatus, errorThrown) {
self._hideOverlay();
document.location = url;
}
});
}
}

View File

@@ -0,0 +1,10 @@
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
@import '~bootstrap-sass/assets/stylesheets/bootstrap';
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
@import '~@fortawesome/fontawesome-free/scss/regular';
@import '~@fortawesome/fontawesome-free/scss/solid';
@import '~@fortawesome/fontawesome-free/scss/brands';
@import '~daterangepicker/daterangepicker.css';

View File

@@ -5,11 +5,11 @@
* file that was distributed with this source code.
*/
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
@import 'variables';
@import 'error-page';
@import 'print';
@import 'content';
@import 'content-header';
@import 'toolbar';
@import 'sidebar';
@import 'footer';
@@ -23,3 +23,4 @@
@import 'skins';
@import 'sweetalert';
@import 'autocomplete';
@import 'selectpicker';

View File

@@ -0,0 +1,52 @@
/*
* 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-header {
height: 50px;
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: 10px 0 0 10px;
h1 {
padding-top: 3px;
float: left;
small {
display: none;
}
}
}
/* Page based action buttons in the upper right corner of the content area */
.content-header>.breadcrumb {
position: absolute;
float: right;
background: transparent;
top: 0;
right: 0;
padding-left: 10px;
}
@media (max-width: $screen-md-min) {
.content-header>.breadcrumb {
margin-top: 0;
}
}
@media (min-width: $screen-sm-min) {
.content-header>.breadcrumb {
right: 10px;
}
.content-header {
h1 {
small {
display: inline-block;
}
}
}
}

View File

@@ -8,43 +8,8 @@
.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;
}
@@ -69,4 +34,27 @@ td {
text-align: center;
line-height: 35px;
margin-right: 15px;
}
}
/* Profile */
.box-profile {
img.img-circle {
width: 100px;
}
.profile-username {
margin-top: 15px;
}
}
.label-gray {
background-color: $gray-lte;
}
/* Delete link in action dropdowns */
.dropdown-menu > li.delete > a {
color: #dd4b39;
}
.open-edit {
cursor: pointer;
}

View File

@@ -5,6 +5,8 @@
* file that was distributed with this source code.
*/
span.label-color,
span.label-meta,
span.label-activity,
span.label-project,
span.label-customer {
@@ -21,19 +23,17 @@ span.label-customer {
table.dataTable {
/* action column */
.actions {
width: 90px;
li.delete a {
color: #dd4b39;
}
width: 40px;
}
tr {
/* summary row - provided in the users own timesheet table */
&.summary td {
font-weight: bold;
border-bottom: 1px solid #ccc;
background-color: #eaeaea;
}
&.open-edit {
cursor: pointer;
th.multiCheckbox {
width: 15px;
}
}
td {
@@ -48,6 +48,7 @@ table.dataTable {
font-size: 85%;
font-weight: 500;
padding: .3em .6em .3em;
line-height: 24px;
}
span.label-activity,
span.label-project,

View File

@@ -23,7 +23,7 @@ section.content .overlay > .fas,
section.content > .overlay,
.box > .loading-img,
.overlay-wrapper > .loading-img {
position: absolute;
position: fixed; /* see #1330 */
top: 0;
left: 0;
width: 100%;
@@ -125,34 +125,35 @@ table.dataTable thead .sorting_desc_disabled {
position: relative;
}
table.dataTable thead .sorting:after,
table.dataTable thead .sorting_asc:after,
table.dataTable thead .sorting_desc:after {
table.dataTable thead .sorting:before,
table.dataTable thead .sorting_asc:before,
table.dataTable thead .sorting_desc:before {
position: absolute;
top: 8px;
right: 8px;
top: 10px;
left: 5px;
display: block;
font-family: 'Glyphicons Halflings';
opacity: 0.5;
font-size: 12px;
}
table.dataTable thead .sorting:after {
table.dataTable thead .sorting:before {
opacity: 0.2;
content: "\e150"; /* sort */
}
table.dataTable thead .sorting_asc:after {
table.dataTable thead .sorting_asc:before {
content: "\e155"; /* sort-by-attributes */
}
table.dataTable thead .sorting_desc:after {
table.dataTable thead .sorting_desc:before {
content: "\e156"; /* sort-by-attributes-alt */
}
div.dataTables_scrollBody table.dataTable thead .sorting:after,
div.dataTables_scrollBody table.dataTable thead .sorting_asc:after,
div.dataTables_scrollBody table.dataTable thead .sorting_desc:after {
div.dataTables_scrollBody table.dataTable thead .sorting:before,
div.dataTables_scrollBody table.dataTable thead .sorting_asc:before,
div.dataTables_scrollBody table.dataTable thead .sorting_desc:before {
display: none;
}
table.dataTable thead .sorting_asc_disabled:after,
table.dataTable thead .sorting_desc_disabled:after {
table.dataTable thead .sorting_asc_disabled:before,
table.dataTable thead .sorting_desc_disabled:before {
color: #eee;
}
@@ -175,9 +176,9 @@ table.dataTable.table-condensed thead > tr > th {
padding-right: 20px;
}
table.dataTable.table-condensed thead .sorting:after,
table.dataTable.table-condensed thead .sorting_asc:after,
table.dataTable.table-condensed thead .sorting_desc:after {
table.dataTable.table-condensed thead .sorting:before,
table.dataTable.table-condensed thead .sorting_asc:before,
table.dataTable.table-condensed thead .sorting_desc:before {
top: 6px;
right: 6px;
}
@@ -264,3 +265,22 @@ div.dataTables_scrollHead table.table-bordered {
.table-striped.dataTable tbody tr.active:nth-child(odd) th {
background-color: #017ebc;
}
/* custom extension to make the column wide enough to show the sortable icons */
table.dataTable th.sortable {
padding-left: 22px;
}
table.dataTable.table > tbody > tr > td {
&.avatars {
line-height: 40px;
}
vertical-align: middle;
.img-circle {
max-width: 35px;
margin-right: 10px;
&.teamlead {
box-shadow: 0 0 5px 3px $gray-lte;
}
}
}

View File

@@ -5,301 +5,256 @@
* file that was distributed with this source code.
*/
.invoice {
margin: 0;
border: none;
}
@media (min-width: $screen-sm-min) {
body.invoice_print {
background-color: #eee;
.invoice {
margin: 25px;
margin: 105px auto 30px auto;
padding: 50px 65px;
min-height: 297mm;
position: relative;
max-width: 210mm;
box-shadow: 0 0 20px rgba(80,80,80,0.7);
}
}
.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;
.page-header h3 {
border-color: #ddd;
}
h2 {
font: bold 1.5em sans-serif;
margin-bottom: 1em;
div.invoice-address {
margin-top: 30px;
margin-bottom: 30px;
}
/* table */
table {
table-layout: fixed;
width: 100%;
table.invoice-meta th {
padding-right: 40px;
}
table {
border-collapse: separate;
border-spacing: 0;
table.invoice-sum th {
width: 70%;
}
/* 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) {
table.invoice-sum th, table.invoice-sum td {
text-align: right;
width: 20%;
}
.invoice-items {
margin-top: 2em;
margin-bottom: 3em;
.table {
thead th {
font-weight: bold;
border-bottom: 1px solid #ddd;
padding-bottom: 15px
}
tfoot {
border-top: 1px solid #ddd;
tr:first-child th,
tr:first-child td {
padding-top: 15px
}
tr {
th,td {
border:none;
padding: 4px 8px;
}
}
}
}
}
/* footer */
footer {
.footer {
border-top: 1px solid #ccc;
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;
padding-top: 10px;
width: 100%;
}
/* ================================ FREELANCER INVOICE ================================ */
#freelancer-invoice {
margin: 1em 2em;
span:empty {
display: none;
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 {
width: 100%;
}
table {
border-collapse: separate;
border-spacing: 0;
}
/* header */
header {
&:after {
clear: both;
content: "";
display: table;
font-size: 1em;
}
address {
float: left;
font-style: normal;
line-height: 1.25;
margin: 0 1em 1em 0;
font-size: 0.7em;
}
}
article address {
font-size: 1em;
float: left;
}
.add, .cut {
display: none;
article.address {
margin:1em 0 1.5em 0;
}
/* article */
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;
margin-top: 1em;
th {
text-align: right;
font-weight: normal;
}
td {
width: 140px;
text-align: right;
line-height: 22px;
}
.total {
font-weight: bold;
}
}
/* table items */
table.inventory {
clear: both;
width: 100%;
margin-top: 2em;
border-bottom: 1px solid #000;
}
table.inventory th,
table.inventory td {
padding: 5px;
}
table.inventory thead th {
font-weight: bold;
border-width: 0 0 1px 0;
border-bottom: 1px solid #000;
padding-bottom: 15px
}
table.inventory tbody tr:first-child td {
padding-top: 15px
}
table.inventory tbody tr:last-child td {
padding-bottom: 15px
}
.footer {
border-color: #000;
}
}
.invoice {
width: unset;
/* ================================ PRINTING RULES ================================ */
@media print {
* {
-webkit-print-color-adjust: exact;
}
body, .wrapper {
margin: 0;
padding: 0;
background-color: white;
}
#freelancer-invoice {
span:empty {
display: none;
}
.add, .cut {
display: none;
}
}
.invoice {
margin: 1em 2em;
font-size: 90%;
width: unset;
padding: 0;
min-height: unset;
position: unset;
max-width: unset;
box-shadow: unset;
}
}
}

View File

@@ -6,6 +6,10 @@
*/
.navbar-nav {
img.img-circle {
max-height: 26px;
margin-top: -2px;
}
li>a.ddt-large {
padding: 15px 12px 11px 10px;
font-size: 10px;

View File

@@ -0,0 +1,23 @@
@import '~select2/dist/css/select2.min.css';
@import '~select2-bootstrap-theme/dist/select2-bootstrap.min.css';
/* some labels were placed incorrect due to a margin-top: -4px */
.select2-container .select2-selection--single .select2-selection__rendered {
margin-top: 0;
}
/* make sure the border looks like all other form elements */
.select2-container--bootstrap .select2-selection {
border-radius: 0;
}
/* placeholder in multi-selects were cut off */
.select2-search.select2-search--inline .select2-search__field {
width: 100% !important;
}
/* make sure that <optgroup> elements are padded, to allow visual difference to global activities */
.select2-container--bootstrap .select2-results__options.select2-results__options--nested li.select2-results__option {
padding-left: 30px;
}
/* empty <option> were too small to be noticed as clickable element */
.select2-container--bootstrap .select2-results__option {
min-height: 30px;
}

View File

@@ -39,3 +39,13 @@
}
}
}
/*
Simplify focus detection, when using keyboard tab navigation
Deactivated, as it looks weird when the link is clicked
*/
/*
.sidebar-menu > li > a:focus {
text-decoration: underline;
}
*/

View File

@@ -5,13 +5,6 @@
* 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;
}
.toolbar-pad {
padding: 10px;
}
@@ -19,6 +12,7 @@
/* The filter form is available on most pages above the datatable */
@media (min-width: $screen-sm-min) {
.toolbar {
// @deprecated since 1.2 - using the toolbar is deprecated and will be removed with 2.0
form.navbar-form {
font-size: $font-size-base;
.form-control {
@@ -39,3 +33,58 @@
}
}
/* Search form with filter dropdown */
.content-header {
form {
width: 200px;
float: left;
.container-fluid {
padding-left: 0;
padding-right: 0;
}
.form-group {
margin: 0 0 5px 0;
}
input.search-has-error {
color: $red;
}
.input-group-addon.has-error {
color: $red;
border-color: $red;
}
input.has-error {
border-color: $red;
}
ul.dropdown-menu {
max-height: 100vh;
overflow-y: auto;
padding-top: 10px;
width: 500px;
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;
}
a.search-cancel {
margin-top: 8px;
}
}
}
@media (max-width: $screen-xs-min) {
.content-header.search-open {
h1 {
display: none;
}
.breadcrumb {
display: none;
}
form.header-search {
padding: 0 10px 0 0;
float: none;
width: 100%;
ul.dropdown-menu {
width: 100%;
max-width: 100%;
}
}
}
}

View File

@@ -5,6 +5,8 @@
* file that was distributed with this source code.
*/
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
/**
* A simple one to one clone of /node_modules/admin-lte/build/less/variables.less
*/

View File

@@ -1,23 +1,24 @@
#!/usr/bin/env php
<?php
set_time_limit(0);
if (!file_exists(__DIR__.'/../vendor/autoload.php')) {
echo 'Warning: You need to run "composer install" before you can use the console.' . \PHP_EOL;
die(1);
}
require __DIR__.'/../vendor/autoload.php';
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Debug\Debug;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\Dotenv\Dotenv;
set_time_limit(0);
require __DIR__.'/../vendor/autoload.php';
if (!class_exists(Application::class)) {
throw new \RuntimeException('You need to add "symfony/framework-bundle" as a Composer dependency.');
}
if (!isset($_SERVER['APP_ENV'])) {
if (!class_exists(Dotenv::class)) {
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables or change the variables in your .env file.');
}
(new Dotenv(true))->load(__DIR__.'/../.env');
}

View File

@@ -18,66 +18,88 @@
"ext-pdo": "*",
"ext-zip": "*",
"beberlei/doctrineextensions": "^1.2",
"doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^2.1",
"doctrine/orm": "^2.7",
"erusev/parsedown": "^1.6",
"friendsofsymfony/rest-bundle": "^2.5",
"friendsofsymfony/user-bundle": "dev-master",
"gedmo/doctrine-extensions": "^2.4",
"hslavich/oneloginsaml-bundle": "^1.4",
"jms/metadata": "^2.0",
"jms/serializer-bundle": "^3.2",
"kevinpapst/adminlte-bundle": "~2.6",
"kevinpapst/adminlte-bundle": "^3.0",
"kimai/kimai2-composer": "^0.1",
"mpdf/mpdf": "^7.1",
"laravolt/avatar": "^3.0",
"league/csv": "^9.4",
"league/html-to-markdown": "^4.9",
"mpdf/mpdf": "^8.0",
"nelmio/api-doc-bundle": "^3.2",
"nelmio/cors-bundle": "^1.5",
"ocramius/proxy-manager": "2.1.1",
"phpoffice/phpspreadsheet": "^1.4",
"phpoffice/phpword": "^0.15.0",
"ocramius/proxy-manager": "^2.1.1",
"onelogin/php-saml": "^3.4",
"pagerfanta/pagerfanta": "^2.1",
"phpoffice/phpspreadsheet": "^1.10",
"phpoffice/phpword": "^0.17",
"psr/log": "^1.1",
"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/amazon-mailer": "^4.4",
"symfony/asset": "^4.4",
"symfony/console": "^4.4",
"symfony/dotenv": "^4.4",
"symfony/expression-language": "^4.4",
"symfony/flex": "^1.0",
"symfony/form": "^4.0",
"symfony/framework-bundle": "^4.0",
"symfony/intl": "^4.0",
"symfony/form": "^4.4",
"symfony/framework-bundle": "^4.4",
"symfony/google-mailer": "^4.4",
"symfony/intl": "^4.4",
"symfony/mailchimp-mailer": "^4.4",
"symfony/mailer": "^4.4",
"symfony/mailgun-mailer": "^4.4",
"symfony/monolog-bundle": "^3.4",
"symfony/orm-pack": "^1.0",
"symfony/security-bundle": "^4.0",
"symfony/security-csrf": "^4.0",
"symfony/serializer": "^4.0",
"symfony/swiftmailer-bundle": "^3.2",
"symfony/translation": "^4.0",
"symfony/twig-bundle": "^4.0",
"symfony/validator": "^4.0",
"symfony/postmark-mailer": "^4.4",
"symfony/security-bundle": "^4.4",
"symfony/security-csrf": "^4.4",
"symfony/sendgrid-mailer": "^4.4",
"symfony/serializer": "^4.4",
"symfony/translation": "^4.4",
"symfony/twig-bundle": "^4.4",
"symfony/validator": "^4.4",
"symfony/webpack-encore-bundle": "^1.5",
"symfony/yaml": "^4.0",
"twig/extensions": "^1.5",
"white-october/pagerfanta-bundle": "^1.1"
"symfony/yaml": "^4.4",
"twig/cssinliner-extra": "^3.0",
"twig/extra-bundle": "^3.0",
"twig/inky-extra": "^3.0",
"twig/intl-extra": "^3.0",
"twig/string-extra": "^3.0"
},
"require-dev": {
"dama/doctrine-test-bundle": "^5.0",
"dama/doctrine-test-bundle": "^6.0",
"doctrine/doctrine-fixtures-bundle": "^3.2",
"friendsofphp/php-cs-fixer": "^2.10",
"fzaninotto/faker": "^1.8",
"phpstan/phpstan": "^0.11.7",
"phpstan/phpstan-doctrine": "^0.11.4",
"phpstan/phpstan-phpunit": "^0.11.2",
"phpstan/phpstan-symfony": "^0.11.6",
"phpunit/phpunit": "^7.0",
"symfony/browser-kit": "^4.0",
"symfony/css-selector": "^4.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-doctrine": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-symfony": "^0.12",
"phpunit/phpunit": "^8.0",
"symfony/browser-kit": "^4.4",
"symfony/css-selector": "^4.4",
"symfony/debug-bundle": "^4.4",
"symfony/maker-bundle": "^1.12",
"symfony/phpunit-bridge": "^4.0",
"symfony/phpunit-bridge": "^4.4",
"symfony/profiler-pack": "^1.0",
"symfony/thanks": "^1.0",
"symfony/web-server-bundle": "~4.2.0"
"symfony/var-dumper": "^4.4"
},
"repositories": [
{
"type": "artifact",
"url": "var/packages/"
}
],
"config": {
"platform": {
"php": "7.2"
"php": "7.2.9"
},
"preferred-install": {
"*": "dist"
@@ -116,10 +138,21 @@
"post-update-cmd": [
"@auto-scripts"
],
"kimai:code-check": [
"kimai:pre-commit": [
"@kimai:codestyle",
"@kimai:phpstan",
"@kimai:tests"
"@kimai:code-lint",
"@kimai:tests-unit"
],
"kimai:code-check": [
"@kimai:pre-commit",
"@kimai:tests-integration"
],
"kimai:code-lint": [
"bin/console lint:yaml config --parse-tags",
"bin/console lint:twig templates --show-deprecations",
"bin/console lint:xliff translations",
"bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction"
],
"kimai:tests": "vendor/bin/phpunit tests/",
"kimai:tests-unit": "vendor/bin/phpunit --exclude-group integration tests/",
@@ -138,7 +171,7 @@
"symfony": {
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
"allow-contrib": true,
"require": "4.3.*"
"require": "4.4.*"
}
}
}

6323
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,6 @@
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true],
Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
@@ -12,8 +10,6 @@ return [
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],
WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
@@ -24,4 +20,5 @@ return [
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
];

View File

@@ -1,3 +1,6 @@
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
packages:
avatars:
base_path: 'avatars'

View File

@@ -1,16 +0,0 @@
services:
EasyCorp\EasyLog\EasyLogHandler:
public: false
arguments: ['%kernel.logs_dir%/%kernel.environment%.log']
#// FIXME: How to add this configuration automatically without messing up with the monolog configuration?
#monolog:
# handlers:
# buffered:
# type: buffer
# handler: easylog
# channels: ['!event']
# level: debug
# easylog:
# type: service
# id: EasyCorp\EasyLog\EasyLogHandler

View File

@@ -0,0 +1,3 @@
framework:
mailer:
dsn: 'null://null'

View File

@@ -1,4 +0,0 @@
# See https://symfony.com/doc/current/email/dev_environment.html
swiftmailer:
# send all emails to a specific address
# delivery_addresses: ['me@example.com']

View File

@@ -28,7 +28,7 @@ doctrine:
entity_managers:
default:
connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:

View File

@@ -0,0 +1,119 @@
doctrine:
orm:
dql:
datetime_functions:
addtime: DoctrineExtensions\Query\Mysql\AddTime
convert_tz: DoctrineExtensions\Query\Mysql\ConvertTz
date: DoctrineExtensions\Query\Mysql\Date
date_format: DoctrineExtensions\Query\Mysql\DateFormat
dateadd: DoctrineExtensions\Query\Mysql\DateAdd
datesub: DoctrineExtensions\Query\Mysql\DateSub
datediff: DoctrineExtensions\Query\Mysql\DateDiff
day: DoctrineExtensions\Query\Mysql\Day
dayname: DoctrineExtensions\Query\Mysql\DayName
dayofweek: DoctrineExtensions\Query\Mysql\DayOfWeek
dayofyear: DoctrineExtensions\Query\Mysql\DayOfYear
div: DoctrineExtensions\Query\Mysql\Div
from_unixtime: DoctrineExtensions\Query\Mysql\FromUnixtime
hour: DoctrineExtensions\Query\Mysql\Hour
last_day: DoctrineExtensions\Query\Mysql\LastDay
makedate: DoctrineExtensions\Query\Mysql\MakeDate
minute: DoctrineExtensions\Query\Mysql\Minute
now: DoctrineExtensions\Query\Mysql\Now
month: DoctrineExtensions\Query\Mysql\Month
monthname: DoctrineExtensions\Query\Mysql\MonthName
period_diff: DoctrineExtensions\Query\Mysql\PeriodDiff
second: DoctrineExtensions\Query\Mysql\Second
sectotime: DoctrineExtensions\Query\Mysql\SecToTime
strtodate: DoctrineExtensions\Query\Mysql\StrToDate
time: DoctrineExtensions\Query\Mysql\Time
timediff: DoctrineExtensions\Query\Mysql\TimeDiff
timestampadd: DoctrineExtensions\Query\Mysql\TimestampAdd
timestampdiff: DoctrineExtensions\Query\Mysql\TimestampDiff
timetosec: DoctrineExtensions\Query\Mysql\TimeToSec
week: DoctrineExtensions\Query\Mysql\Week
weekday: DoctrineExtensions\Query\Mysql\WeekDay
year: DoctrineExtensions\Query\Mysql\Year
yearmonth: DoctrineExtensions\Query\Mysql\YearMonth
yearweek: DoctrineExtensions\Query\Mysql\YearWeek
unix_timestamp: DoctrineExtensions\Query\Mysql\UnixTimestamp
utc_timestamp: DoctrineExtensions\Query\Mysql\UtcTimestamp
extract: DoctrineExtensions\Query\Mysql\Extract
numeric_functions:
acos: DoctrineExtensions\Query\Mysql\Acos
asin: DoctrineExtensions\Query\Mysql\Asin
atan2: DoctrineExtensions\Query\Mysql\Atan2
atan: DoctrineExtensions\Query\Mysql\Atan
bit_count: DoctrineExtensions\Query\Mysql\BitCount
bit_xor: DoctrineExtensions\Query\Mysql\BitXor
ceil: DoctrineExtensions\Query\Mysql\Ceil
cos: DoctrineExtensions\Query\Mysql\Cos
cot: DoctrineExtensions\Query\Mysql\Cot
degrees: DoctrineExtensions\Query\Mysql\Degrees
exp: DoctrineExtensions\Query\Mysql\Exp
floor: DoctrineExtensions\Query\Mysql\Floor
log: DoctrineExtensions\Query\Mysql\Log
log10: DoctrineExtensions\Query\Mysql\Log10
log2: DoctrineExtensions\Query\Mysql\Log2
pi: DoctrineExtensions\Query\Mysql\Pi
power: DoctrineExtensions\Query\Mysql\Power
quarter: DoctrineExtensions\Query\Mysql\Quarter
radians: DoctrineExtensions\Query\Mysql\Radians
rand: DoctrineExtensions\Query\Mysql\Rand
round: DoctrineExtensions\Query\Mysql\Round
stddev: DoctrineExtensions\Query\Mysql\StdDev
sin: DoctrineExtensions\Query\Mysql\Sin
std: DoctrineExtensions\Query\Mysql\Std
tan: DoctrineExtensions\Query\Mysql\Tan
variance: DoctrineExtensions\Query\Mysql\Variance
string_functions:
aes_decrypt: DoctrineExtensions\Query\Mysql\AesDecrypt
aes_encrypt: DoctrineExtensions\Query\Mysql\AesEncrypt
any_value: DoctrineExtensions\Query\Mysql\AnyValue
ascii: DoctrineExtensions\Query\Mysql\Ascii
binary: DoctrineExtensions\Query\Mysql\Binary
cast: DoctrineExtensions\Query\Mysql\Cast
char_length: DoctrineExtensions\Query\Mysql\CharLength
collate: DoctrineExtensions\Query\Mysql\Collate
concat_ws: DoctrineExtensions\Query\Mysql\ConcatWs
countif: DoctrineExtensions\Query\Mysql\CountIf
crc32: DoctrineExtensions\Query\Mysql\Crc32
degrees: DoctrineExtensions\Query\Mysql\Degrees
field: DoctrineExtensions\Query\Mysql\Field
find_in_set: DoctrineExtensions\Query\Mysql\FindInSet
format: DoctrineExtensions\Query\Mysql\Format
greatest: DoctrineExtensions\Query\Mysql\Greatest
group_concat: DoctrineExtensions\Query\Mysql\GroupConcat
hex: DoctrineExtensions\Query\Mysql\Hex
ifelse: DoctrineExtensions\Query\Mysql\IfElse
ifnull: DoctrineExtensions\Query\Mysql\IfNull
inet_aton: DoctrineExtensions\Query\Mysql\InetAton
inet_ntoa: DoctrineExtensions\Query\Mysql\InetNtoa
inet6_aton: DoctrineExtensions\Query\Mysql\Inet6Aton
inet6_ntoa: DoctrineExtensions\Query\Mysql\Inet6Ntoa
instr: DoctrineExtensions\Query\Mysql\Instr
is_ipv4: DoctrineExtensions\Query\Mysql\IsIpv4
is_ipv4_compat: DoctrineExtensions\Query\Mysql\IsIpv4Compat
is_ipv4_mapped: DoctrineExtensions\Query\Mysql\IsIpv4Mapped
is_ipv6: DoctrineExtensions\Query\Mysql\IsIpv6
lag: DoctrineExtensions\Query\Mysql\Lag
lead: DoctrineExtensions\Query\Mysql\Lead
least: DoctrineExtensions\Query\Mysql\Least
lpad: DoctrineExtensions\Query\Mysql\Lpad
match: DoctrineExtensions\Query\Mysql\MatchAgainst
md5: DoctrineExtensions\Query\Mysql\Md5
nullif: DoctrineExtensions\Query\Mysql\NullIf
over: DoctrineExtensions\Query\Mysql\Over
radians: DoctrineExtensions\Query\Mysql\Radians
regexp: DoctrineExtensions\Query\Mysql\Regexp
replace: DoctrineExtensions\Query\Mysql\Replace
rpad: DoctrineExtensions\Query\Mysql\Rpad
sha1: DoctrineExtensions\Query\Mysql\Sha1
sha2: DoctrineExtensions\Query\Mysql\Sha2
soundex: DoctrineExtensions\Query\Mysql\Soundex
str_to_date: DoctrineExtensions\Query\Mysql\StrToDate
substring_index: DoctrineExtensions\Query\Mysql\SubstringIndex
unhex: DoctrineExtensions\Query\Mysql\Unhex
uuid_short: DoctrineExtensions\Query\Mysql\UuidShort

View File

@@ -0,0 +1,56 @@
doctrine:
orm:
dql:
datetime_functions:
convert_tz: App\Doctrine\Query\Sqlite\ConvertTz
date: DoctrineExtensions\Query\Sqlite\Date
date_format: DoctrineExtensions\Query\Sqlite\DateFormat
#dateadd: DoctrineExtensions\Query\Sqlite\DateAdd
#datediff: DoctrineExtensions\Query\Sqlite\DateDiff
day: DoctrineExtensions\Query\Sqlite\Day
dayname: DoctrineExtensions\Query\Sqlite\DayName
hour: DoctrineExtensions\Query\Sqlite\Hour
julianday: DoctrineExtensions\Query\Sqlite\JulianDay
minute: DoctrineExtensions\Query\Sqlite\Minute
#month: DoctrineExtensions\Query\Sqlite\Month
#monthname: DoctrineExtensions\Query\Sqlite\MonthName
second: DoctrineExtensions\Query\Sqlite\Second
month: DoctrineExtensions\Query\Sqlite\Month
strftime: DoctrineExtensions\Query\Sqlite\StrfTime
#strtodate: DoctrineExtensions\Query\Sqlite\StrToDate
#time: DoctrineExtensions\Query\Sqlite\Time
#timestampadd: DoctrineExtensions\Query\Sqlite\TimestampAdd
#timestampdiff: DoctrineExtensions\Query\Sqlite\TimestampDiff
week: DoctrineExtensions\Query\Sqlite\Week
weekday: DoctrineExtensions\Query\Sqlite\WeekDay
year: DoctrineExtensions\Query\Sqlite\Year
string_functions:
# binary: DoctrineExtensions\Query\Sqlite\Binary
# char_length: DoctrineExtensions\Query\Sqlite\CharLength
concat_ws: DoctrineExtensions\Query\Sqlite\ConcatWs
# countif: DoctrineExtensions\Query\Sqlite\CountIf
# crc32: DoctrineExtensions\Query\Sqlite\Crc32
# degrees: DoctrineExtensions\Query\Sqlite\Degrees
# field: DoctrineExtensions\Query\Sqlite\Field
# find_in_set: DoctrineExtensions\Query\Sqlite\FindInSet
# group_concat: DoctrineExtensions\Query\Sqlite\GroupConcat
greatest: DoctrineExtensions\Query\Sqlite\Greatest
ifelse: DoctrineExtensions\Query\Sqlite\IfElse
ifnull: DoctrineExtensions\Query\Sqlite\IfNull
least: DoctrineExtensions\Query\Sqlite\Least
# match: DoctrineExtensions\Query\Sqlite\MatchAgainst
# md5: DoctrineExtensions\Query\Sqlite\Md5
# nullif: DoctrineExtensions\Query\Sqlite\NullIf
# radians: DoctrineExtensions\Query\Sqlite\Radians
random: DoctrineExtensions\Query\Sqlite\Random
# regexp: DoctrineExtensions\Query\Sqlite\Regexp
replace: DoctrineExtensions\Query\Sqlite\Replace
# sha1: DoctrineExtensions\Query\Sqlite\Sha1
# sha2: DoctrineExtensions\Query\Sqlite\Sha2
# soundex: DoctrineExtensions\Query\Sqlite\Soundex
# uuid_short: DoctrineExtensions\Query\Sqlite\UuidShort
numeric_functions:
round: DoctrineExtensions\Query\Sqlite\Round

View File

@@ -8,18 +8,8 @@ fos_user:
registration:
confirmation:
enabled: false
template: '@FOSUser/Registration/email.txt.twig'
resetting:
retry_ttl: 7200
token_ttl: 86400
email:
template: '@FOSUser/Resetting/email.txt.twig'
service:
mailer: fos_user.mailer.twig_swift
# group:
# group_class: ~ # Required when using groups
# group_manager: fos_user.group_manager.default
# form:
# type: FOS\UserBundle\Form\Type\GroupFormType # or 'fos_user_group' on Symfony < 2.8
# name: fos_user_group_form
# validation_groups: [Registration, Default]
mailer: App\Mail\UserMails

View File

@@ -29,6 +29,3 @@ framework:
#validation:
# possible values: 'loose', 'strict', 'html5'
#email_validation_mode: 'html5'
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'

View File

@@ -21,11 +21,13 @@ kimai:
# --------------------------------------------------------------------------------
# TIME-TRACKING
# All configs related to time-tracking, timesheets and record management
# Most settings can be configured in the System configuration screen
# --------------------------------------------------------------------------------
timesheet:
# renders timesheet descriptions with markdown
markdown_content: false
# Allows to render timesheet descriptions with markdown
# This setting can be changed through the Administration screen
# markdown_content: false
# The time-tracking mode that should be used.
# See https://www.kimai.org/documentation/timesheet.html#tracking-modes
@@ -42,36 +44,36 @@ kimai:
# 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.
# 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
# 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".
# # See documentation at: https://www.kimai.org/documentation/timesheet.html#rate-calculation
# rates:
# weekend:
# days: ['saturday','sunday']
# factor: 1.5
#
# # If you want to limit the max. active entries per user, you can do it here.
# # The soft_limit is used as theme setting and displays a warning color if the user has reached 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: it is automatically stopped when a new one is started
# # - when hard_limit is > 1 and the user is trying to start a new entry after reaching the limit, a warning is shown
# # and the user has to stop an active entry first
# active_entries:
# soft_limit: 1
# hard_limit: 3
#
# # Rules that define timesheet validation and behaviour
# rules:
# # whether records in the future can be created
# allow_future_times: true
# 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".
# See documentation at: https://www.kimai.org/documentation/timesheet.html#rate-calculation
# rates:
# weekend:
# days: ['saturday','sunday']
# factor: 1.5
# If you want to limit the max. active entries per user, you can do it here.
# The soft_limit is used as theme setting and displays a warning color if the user has reached 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: it is automatically stopped when a new one is started
# - when hard_limit is > 1 and the user is trying to start a new entry after reaching the limit, a warning is shown
# and the user has to stop an active entry first
# active_entries:
# soft_limit: 1
# hard_limit: 3
# Rules that define timesheet validation and behaviour
# allow_future_times: whether records in the future can be created
# rules:
# allow_future_times: true
# --------------------------------------------------------------------------------
@@ -84,31 +86,35 @@ kimai:
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"])
sets:
ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','delete_activity']
ACTIVITIES_TEAM: ['view_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity']
PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project','permissions_project']
PROJECTS_TEAM: ['view_project','edit_teamlead_project','budget_teamlead_project','permissions_teamlead_project']
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer']
CUSTOMERS_TEAM: ['view_customer','edit_teamlead_customer','budget_teamlead_customer']
ACTIVITIES_TEAMLEAD: ['view_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity']
PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project','permissions_project','comments_project','comments_create_project','details_project']
PROJECTS_ALL_TEAMLEAD: ['view_teamlead_project','edit_teamlead_project','budget_teamlead_project','permissions_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project']
PROJECTS_ALL_TEAM: ['view_team_project','edit_team_project','budget_team_project','comments_team_project','comments_create_team_project','details_team_project']
PROJECTS_TEAMLEAD: ['view_teamlead_project','budget_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project']
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer','comments_customer','comments_create_customer','details_customer']
CUSTOMERS_ALL_TEAMLEAD: ['view_teamlead_customer','edit_teamlead_customer','budget_teamlead_customer','permissions_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer']
CUSTOMERS_ALL_TEAM: ['view_team_customer','edit_team_customer','budget_team_customer','comments_team_customer','comments_create_team_customer','details_team_customer']
CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer']
INVOICE: ['view_invoice','create_invoice']
INVOICE_TEMPLATE: ['manage_invoice_template']
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
PROFILE_OTHER: ['view_other_profile','edit_other_profile','delete_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
TAGS: ['view_tag','delete_tag']
PROFILE_OTHER: ['view_other_profile','edit_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
TAGS: ['view_tag','manage_tag','delete_tag']
USER: ['view_user','create_user','delete_user','role_permissions']
RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
RATE_OTHER: ['view_rate_other_timesheet','edit_rate_other_timesheet']
EXPORT: ['create_export','edit_export_own_timesheet','edit_export_other_timesheet']
TEAMS: ['view_team','create_team','edit_team','delete_team']
# some single default definitions for roles
SINGLE_USER: []
SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile']
SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile']
SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','delete_own_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile']
# link above sets to one set for each user role
ROLE_USER: ['@TIMESHEET','@PROFILE']
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAM','@PROJECTS_TEAM','@CUSTOMERS_TEAM','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@SINGLE_TEAMLEAD']
SINGLE_USER: ['view_team_member','budget_team_project']
SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member']
SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member']
SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template']
# link above sets to one complete set for each user role
ROLE_USER: ['@TIMESHEET','@PROFILE','@SINGLE_USER']
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAMLEAD','@PROJECTS_TEAMLEAD','@CUSTOMERS_TEAMLEAD','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@SINGLE_TEAMLEAD']
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_ADMIN']
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_SUPER_ADMIN']
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
@@ -117,6 +123,8 @@ kimai:
ROLE_TEAMLEAD: ['ROLE_TEAMLEAD']
ROLE_ADMIN: ['ROLE_ADMIN']
ROLE_SUPER_ADMIN: ['ROLE_SUPER_ADMIN']
# only here to register the (partially) unused permissions in the UI
ROLE_FAKE: ['CUSTOMERS_ALL_TEAMLEAD','CUSTOMERS_ALL_TEAM','PROJECTS_ALL_TEAMLEAD','PROJECTS_ALL_TEAM']
# add or remove single permissions
roles:
ROLE_USER: []
@@ -171,11 +179,15 @@ kimai:
# --------------------------------------------------------------------------------
dashboard:
user_duration:
title: dashboard.you
title: ~
order: 10
permission: view_own_timesheet
widgets: [PaginatedWorkingTimeChart]
user_teams:
title: ~
order: 15
permission: ROLE_USER
type: 'compoundChart'
widgets: [DailyWorkingTimeChart, userDurationToday, userDurationWeek, userDurationMonth, userDurationYear]
widgets: [UserTeams, UserTeamProjects]
user_rates:
title: ~
order: 20
@@ -204,18 +216,26 @@ kimai:
# Locale specific settings, like date formats
# --------------------------------------------------------------------------------
languages:
ar:
date_time_type: 'yyyy-MM-dd HH:mm'
date_type: 'yyyy-MM-dd'
date: 'Y-m-d'
date_time: 'm-d H:i'
duration: '%%h:%%m h'
cs:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy'
date: 'd.m.Y'
date_time: 'd.m H:i'
da:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy'
date: 'd.m.Y'
date_time: 'd.m. H:i'
de:
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'
de_CH:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy'
date: 'd.m.Y'
date_time: 'd.m. H:i'
en:
date_time_type: 'yyyy-MM-dd HH:mm'
date_type: 'yyyy-MM-dd'
@@ -227,6 +247,11 @@ kimai:
date_type: 'dd.MM.yyyy'
date: 'd.m.Y'
date_time: 'd.m. H:i'
eu:
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'
fr:
date_time_type: 'dd/MM/yyyy HH:mm'
@@ -239,49 +264,39 @@ kimai:
date_type: 'yyyy.MM.dd.'
date: 'Y.m.d.'
date_time: 'm.d. H:i'
duration: '%%h:%%m h'
it:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy'
date: 'd.m.Y'
date_time: 'd.m. H:i'
duration: '%%h:%%m h'
ja:
date_time_type: 'yyyy-MM-dd HH:mm'
date_type: 'yyyy-MM-dd'
date: 'Y-m-d'
date_time: 'm-d H:i'
duration: '%%h:%%m tim'
ko:
date_time_type: 'yyyy-MM-dd HH:mm'
date_type: 'yyyy-MM-dd'
date: 'Y-m-d'
date_time: 'm-d H:i'
duration: '%%h:%%m h'
nl:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy'
date: 'd.m.Y'
date_time: 'd.m. H:i'
duration: '%%hu%%m'
pt_BR:
date_time_type: 'dd-MM-yyyy HH:mm'
date_type: 'dd-MM-yyyy'
date: 'd-m-Y'
date_time: 'd-m H:i'
duration: '%%h:%%m h'
ru:
date_time_type: 'dd.MM.yyyy HH:mm'
date_type: 'dd.MM.yyyy'
date: 'd.m.Y'
date_time: 'd.m. H:i'
duration: '%%h:%%m h'
sk:
date_time_type: 'dd. MM. yyyy HH:mm'
date_type: 'dd. MM. yyyy'
date: 'd. m. Y'
date_time: 'd. m. H:i'
duration: '%%h:%%m h'
sv:
date_time_type: 'yyyy-MM-dd HH:mm'
date_type: 'yyyy-MM-dd'
date: 'Y-m-d'
date_time: 'm-d H:i'
duration: '%%h:%%m tim'
pl:
date_time_type: 'dd. MM. yyyy HH:mm'
date_type: 'dd. MM. yyyy'
date: 'd. m. Y'
date_time: 'd. m. H:i'
# --------------------------------------------------------------------------------

View File

@@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_URL)%'

View File

@@ -14,16 +14,22 @@ nelmio_api_doc:
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] }
- { alias: ActivityMetaField, type: App\Entity\ActivityMeta, groups: [Default, Activity] }
- { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] }
- { alias: TagEditForm, type: App\Form\API\TagApiEditForm, groups: [Default, Entity, Tag] }
- { alias: TagEntity, type: App\Entity\Tag, groups: [Default, Entity, Tag] }
- { alias: TimesheetEditForm, type: App\Form\API\TimesheetApiEditForm, groups: [Default, Entity, Timesheet] }
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] }
- { alias: TimesheetMeta, type: App\Entity\TimesheetMeta, groups: [Default, Timesheet] }
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
- { alias: UserCreateForm, type: App\Form\API\UserApiCreateForm, groups: [Default, Entity, User, User_Entity] }
- { alias: UserEditForm, type: App\Form\API\UserApiEditForm, groups: [Default, Entity, User, User_Entity] }
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User, User_Entity] }
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
- { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team] }
- { alias: TeamEditForm, type: App\Form\API\TeamApiEditForm, groups: [Default, Entity, Team] }
- { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team, Team_Entity] }
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
- { alias: TimesheetConfig, type: App\API\Model\TimesheetConfig, groups: [Default] }
areas:
path_patterns:
- ^/api(?!/doc)
@@ -35,9 +41,9 @@ nelmio_api_doc:
info:
title: Kimai 2 - API Docs
description: |
REST API for the Kimai 2 time-tracking software. Read more about its usage in the [API documentation](https://www.kimai.org/documentation/rest-api.html) and then download a [Swagger file](doc.json) for import e.g. in Postman.
JSON API for the Kimai 2 time-tracking software. Read more about its usage in the [API documentation](https://www.kimai.org/documentation/rest-api.html) and then download a [Swagger file](doc.json) for import e.g. in Postman.
Be aware: it is not yet considered stable and BC breaks might happen, but we try to avoid them.
version: 0.3
version: '0.4'
securityDefinitions:
apiUser:
type: apiKey

View File

@@ -5,11 +5,11 @@ security:
providers:
chain_provider:
chain:
providers: [fos_userbundle]
providers: [kimai_internal]
kimai_ldap:
id: App\Ldap\LdapUserProvider
fos_userbundle:
id: fos_user.user_provider.username_email
kimai_internal:
id: App\Security\DoctrineUserProvider
firewalls:
dev:
@@ -48,12 +48,15 @@ security:
allow_if_all_abstain: false
role_hierarchy:
ROLE_USER: ~
ROLE_TEAMLEAD: ROLE_USER
ROLE_ADMIN: ROLE_TEAMLEAD
ROLE_SUPER_ADMIN: ROLE_ADMIN
access_control:
- { path: '^/auth', roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)$', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/auth', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/login', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/register', role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/(%app_locales%)/resetting', role: IS_AUTHENTICATED_ANONYMOUSLY }

View File

@@ -1,3 +0,0 @@
swiftmailer:
url: '%env(MAILER_URL)%'
spool: { type: 'memory' }

View File

@@ -0,0 +1,3 @@
framework:
mailer:
dsn: 'null://null'

View File

@@ -1,2 +0,0 @@
swiftmailer:
disable_delivery: true

View File

@@ -4,3 +4,7 @@ twig:
strict_variables: '%kernel.debug%'
form_themes:
- 'form/kimai-theme.html.twig'
exception_controller: null
paths:
# used in templates/emails/layout.html.twig
'%kernel.project_dir%/assets/css': css

View File

@@ -1,12 +0,0 @@
services:
_defaults:
public: false
autowire: true
autoconfigure: true
#Twig\Extensions\ArrayExtension: ~
#Twig\Extensions\DateExtension: ~
Twig\Extensions\IntlExtension: ~
#Twig\Extensions\TextExtension: ~
Twig\Extensions\StringLoader:
class: Twig\Extension\StringLoaderExtension

View File

@@ -7,7 +7,10 @@ home:
homeLocale:
path: /{_locale}
requirements:
_locale: '%app_locales%'
defaults:
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction
_locale: '%locale%'
route: homepage
permanent: true

View File

@@ -1,5 +1,5 @@
controllers:
resource: '../src/Controller/'
resource: '../../src/Controller/'
type: annotation
prefix: /{_locale}
requirements:

View File

@@ -1,5 +1,5 @@
_errors:
resource: '@TwigBundle/Resources/config/routing/errors.xml'
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /{_locale}/_error
requirements:
_locale: '%app_locales%'

View File

@@ -1,6 +1,6 @@
app.test.sidebar_settings:
path: /{_locale}/sidebar/settings
controller: App\Controller\SidebarController::settingsAction
app.test.active_entries:
path: /{_locale}/layou/active_entries
controller: App\Controller\LayoutController::activeEntries
requirements:
_locale: '%app_locales%'
defaults:

View File

@@ -1,6 +1,6 @@
App\Entity\Activity:
exclusion_policy: All
custom_accessor_order: [id, name, comment, visible, project, fixedRate, hourlyRate, color, budget, timeBudget, metaFields]
custom_accessor_order: [id, name, comment, visible, project, color, budget, timeBudget, metaFields, parentTitle]
properties:
id:
include: true
@@ -20,12 +20,6 @@ App\Entity\Activity:
timeBudget:
include: true
groups: [Entity]
fixedRate:
include: true
groups: [Activity]
hourlyRate:
include: true
groups: [Activity]
project:
include: false
exclude: true

View File

@@ -1,6 +1,6 @@
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, color, budget, timeBudget, metaFields]
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, color, budget, timeBudget, metaFields, teams]
properties:
id:
include: true
@@ -53,16 +53,13 @@ App\Entity\Customer:
timezone:
include: true
groups: [Entity]
fixedRate:
include: true
groups: [Customer]
hourlyRate:
include: true
groups: [Customer]
color:
include: true
metaFields:
exclude: true
teams:
include: true
groups: [Customer]
virtual_properties:
getMetaFields:
serialized_name: metaFields

View File

@@ -1,6 +1,6 @@
App\Entity\Project:
exclusion_policy: All
custom_accessor_order: [id, name, comment, visible, orderNumber, customer, fixedRate, hourlyRate, color, budget, timeBudget, metaFields]
custom_accessor_order: [id, name, comment, visible, orderNumber, orderDate, customer, start, end, color, budget, timeBudget, metaFields, parentTitle, teams]
properties:
id:
include: true
@@ -20,10 +20,13 @@ App\Entity\Project:
orderNumber:
include: true
groups: [Entity]
fixedRate:
orderDate:
include: true
groups: [Entity]
start:
include: true
groups: [Project]
hourlyRate:
end:
include: true
groups: [Project]
customer:
@@ -32,6 +35,9 @@ App\Entity\Project:
include: true
metaFields:
exclude: true
teams:
include: true
groups: [Project]
virtual_properties:
parentTitle:
serialized_name: parentTitle

View File

@@ -1,14 +1,20 @@
App\Entity\Team:
exclusion_policy: All
custom_accessor_order: [id, name, users]
custom_accessor_order: [id, name, teamlead, users, customers, projects]
properties:
id:
include: true
name:
include: true
teamlead:
include: true
groups: [Team_Entity]
users:
exclude: true
include: true
groups: [Team_Entity]
customers:
exclude: true
include: true
groups: [Team_Entity]
projects:
exclude: true
include: true
groups: [Team_Entity]

View File

@@ -1,6 +1,6 @@
App\Entity\User:
exclusion_policy: All
custom_accessor_order: [id, alias, title, avatar, language, timezone]
custom_accessor_order: [id, alias, title, avatar, language, timezone, teams]
properties:
id:
include: true
@@ -10,18 +10,21 @@ App\Entity\User:
groups: [Default]
title:
include: true
groups: [Entity]
groups: [User_Entity]
avatar:
include: true
groups: [Entity]
groups: [User_Entity]
teams:
include: true
groups: [User_Entity]
virtual_properties:
getLanguage:
serialized_name: language
exp: "object.getPreferenceValue('language') === null ? null : object.getPreferenceValue('language')"
type: string
groups: [Entity]
groups: [User_Entity]
getTimezone:
serialized_name: timezone
exp: "object.getPreferenceValue('timezone') === null ? null : object.getPreferenceValue('timezone')"
type: string
groups: [Entity]
groups: [User_Entity]

View File

@@ -10,7 +10,7 @@ FOS\UserBundle\Model\User:
roles:
type: array<string>
include: true
groups: [Entity]
groups: [User_Entity]
groups:
exclude: true
email:

32
config/services-saml.yaml Normal file
View File

@@ -0,0 +1,32 @@
services:
# ================================================================================
# SAML
# ================================================================================
App\Saml\SamlAuth:
alias: onelogin_auth
OneLogin\Saml2\Auth:
alias: onelogin_auth
onelogin_auth:
class: App\Saml\SamlAuth
arguments: ['@request_stack', '%kimai.saml.connection%']
App\Saml\User\SamlUserFactory:
arguments: ['%kimai.saml%']
kimai.saml_listener:
class: Hslavich\OneloginSamlBundle\Security\Firewall\SamlListener
parent: security.authentication.listener.abstract
abstract: true
calls:
- [setOneLoginAuth, ["@onelogin_auth"]]
App\Saml\Provider\SamlProvider:
arguments: ['@App\Repository\UserRepository', '', '@App\Saml\SamlTokenFactory', '@App\Saml\User\SamlUserFactory']
App\Saml\Security\SamlAuthenticationSuccessHandler:
parent: security.authentication.success_handler

View File

@@ -2,7 +2,7 @@
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: en
app_locales: ar|de|en|es|fr|hu|it|ja|ko|pt_BR|ru|sk|sv
app_locales: ar|cs|de|de_CH|da|en|es|eu|fr|hu|it|ja|ko|nl|pl|pt_BR|ru|sk|sv|tr|zh_CN
services:
# default configuration for services in *this* file
@@ -74,13 +74,15 @@ services:
App\Plugin\PluginManager:
arguments: [!tagged kimai.plugin]
App\Export\ServiceExport:
arguments: [!tagged export.renderer]
App\Widget\WidgetService:
arguments:
$renderer: !tagged widget.renderer
App\Utils\Translator:
decorates: translator
arguments:
$localDomains: '%kimai.i18n_domains%'
# ================================================================================
# DATABASE
# ================================================================================
@@ -116,18 +118,25 @@ services:
# TIMESHEET RECORD CALCULATOR
# ================================================================================
App\Timesheet\Calculator\DurationCalculator:
arguments: ['%kimai.timesheet.rounding%']
App\Timesheet\RoundingService:
arguments:
$roundingModes: !tagged timesheet.rounding_mode
$rules: '%kimai.timesheet.rounding%'
App\Timesheet\Calculator\RateCalculator:
arguments: ['%kimai.timesheet.rates%']
App\Timesheet\TrackingModeService:
arguments:
$modes: !tagged timesheet.tracking_mode
# ================================================================================
# SECURITY & VOTER
# ================================================================================
App\Security\RoleService:
arguments: ['%security.role_hierarchy.roles%']
arguments:
$roles: '%security.role_hierarchy.roles%'
App\Security\RolePermissionManager:
arguments:
@@ -137,8 +146,7 @@ services:
# LDAP
# ================================================================================
kimai_ldap.security.authentication.provider:
class: App\Ldap\LdapAuthenticationProvider
App\Ldap\LdapAuthenticationProvider:
arguments: ['@App\Security\UserChecker', '', '', '', '@App\Configuration\LdapConfiguration', '%security.authentication.hide_user_not_found%']
# ================================================================================
@@ -190,6 +198,31 @@ services:
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\Configuration']
App\Repository\RoleRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\Role']
App\Repository\RolePermissionRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\RolePermission']
App\Repository\InvoiceDocumentRepository:
class: App\Repository\InvoiceDocumentRepository
arguments: ['%kimai.invoice.documents%']
App\Repository\CustomerRateRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\CustomerRate']
App\Repository\ActivityRateRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\ActivityRate']
App\Repository\ProjectRateRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\ProjectRate']

View File

@@ -1,3 +1,7 @@
parameters:
locale: en
app_locales: ar|cs|de|de_CH|da|en|es|fr|hu|it|ja|ko|nl|pt_BR|ru|sk|sv|tr|zh_CN
services:
_defaults:
public: true

View File

@@ -4,7 +4,7 @@ App\Entity\User:
- App\Validator\Constraints\Role: ~
username:
- NotBlank: ~
- Length: { min: 3, max: 60 }
- Length: { min: 2, max: 60 }
email:
- NotBlank: ~
- Email: ~

View File

@@ -21,24 +21,26 @@
"prod": "node_modules/.bin/encore production"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "~5.8.2",
"@symfony/webpack-encore": "^0.27.0",
"admin-lte": "~2.4.8",
"bootstrap-sass": "^3.3.7",
"bootstrap-select": "^1.13.1",
"@fortawesome/fontawesome-free": "^5.12",
"@symfony/webpack-encore": "^0.28",
"admin-lte": "^2.4",
"bootstrap-sass": "^3.4",
"chart.js": "~2.8.0",
"core-js": "^3.1.3",
"daterangepicker": "^3.0.3",
"fullcalendar": "^3.9.0",
"icheck": "^1.0.2",
"jquery": "^3.3.1",
"jquery-slimscroll": "^1.3.8",
"jquery-ui": "^1.12.1",
"js-cookie": "^2.2.0",
"moment": "^2.24.0",
"node-sass": "^4.9.0",
"sass-loader": "^7.0.2",
"sweetalert2": "^8.11.7",
"webpack-notifier": "^1.5.1"
"daterangepicker": "^3.0",
"fullcalendar": "^3.9",
"icheck": "^1.0",
"jquery": "^3.4",
"jquery-slimscroll": "^1.3",
"jquery-ui": "^1.12",
"js-cookie": "^2.2",
"moment": "^2.24",
"node-sass": "^4.13",
"sass-loader": "^7.3",
"select2": "^4.0",
"select2-bootstrap-theme": "^0.1.0-beta.10",
"sortablejs": "^1.10",
"sweetalert2": "^9.3",
"webpack-notifier": "^1.8"
}
}

View File

@@ -1,6 +1,7 @@
includes:
- vendor/phpstan/phpstan-symfony/extension.neon
- vendor/phpstan/phpstan-doctrine/extension.neon
- vendor/phpstan/phpstan-symfony/rules.neon
parameters:
tmpDir: %rootDir%/../../../var/cache/phpstan
@@ -10,14 +11,15 @@ parameters:
# container_xml_path: '%rootDir%/../../../var/cache/dev/srcApp_KernelDevDebugContainer.xml'
ignoreErrors:
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::scalarNode\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::children\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::useAttributeAsKey\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::integerNode\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::booleanNode\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::end\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::defaultValue\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::addDefaultsIfNotSet\(\).#'
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::requiresAtLeastOneElement\(\).#'
- '#Access to an undefined property Faker\\Generator::\$stateAbbr.#'
- '#Access to an undefined property Faker\\Generator::\$catchPhrase.#'
- '#Access to an undefined property Faker\\Generator::\$bs.#'
- '#Method Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface::dispatch\(\) invoked with 2 parameters, 1 required.#'
- '#Method Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface::dispatch\(\) invoked with 2 parameters, 1 required.#'
excludes_analyse:
- %rootDir%/../../../src/Command/KimaiImporterCommand.php
- %rootDir%/../../../src/Ldap/LdapDriver.php

View File

@@ -9,26 +9,16 @@
>
<php>
<ini name="error_reporting" value="-1" />
<ini name="max_execution_time" value="-1" />
<ini name="intl.default_locale" value="en_US" />
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
<env name="APP_ENV" value="test" force="true"/>
<env name="APP_DEBUG" value="1" force="true"/>
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" 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:///%kernel.project_dir%/var/data/kimai_test.sqlite" force="true"/>
<!-- ###- doctrine/doctrine-bundle ### -->
<!-- ###+ 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 ### -->
<env name="MAILER_URL" value="null://null"/>
</php>
<testsuites>
@@ -56,10 +46,16 @@
</whitelist>
</filter>
<!--
begins a database transaction before every testcase and rolls it back after the test finished, so
tests can manipulate the database without affecting other tests
@see https://github.com/dmaicher/doctrine-test-bundle
-->
<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
</extensions>
<listeners>
<!-- it begins a database transaction before every testcase and rolls it back after
the test finished, so tests can manipulate the database without affecting other tests -->
<listener class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitListener" />
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
</phpunit>

0
public/avatars/.gitignore vendored Normal file
View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More