Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8273c4441 | ||
|
|
8d3a305ec3 | ||
|
|
72b73bc9a6 | ||
|
|
ec31d206e2 | ||
|
|
bf11de82fc | ||
|
|
34d2228d5d | ||
|
|
0e507804b1 | ||
|
|
6c7477f1a9 | ||
|
|
886fc8d74e | ||
|
|
7b218c33f9 | ||
|
|
c67617262f | ||
|
|
057c824e9f | ||
|
|
1b4f4bba22 | ||
|
|
847798eec1 | ||
|
|
52c7437076 | ||
|
|
465d7166d4 | ||
|
|
733aa58de8 | ||
|
|
9620627ea6 | ||
|
|
e06314e802 | ||
|
|
986d922855 | ||
|
|
75dcae6fbe | ||
|
|
375691365e | ||
|
|
5c5af05408 | ||
|
|
69cf7015d8 | ||
|
|
6a533579b7 | ||
|
|
3ff46e06c0 | ||
|
|
9e2fccb105 | ||
|
|
f2c6d8aee1 | ||
|
|
836a530b86 | ||
|
|
a31e027c54 | ||
|
|
a1b554b3ac | ||
|
|
79c4c237fa | ||
|
|
85a35a3063 | ||
|
|
e0e391efbe | ||
|
|
9eff89963c | ||
|
|
352fac26f3 | ||
|
|
8c16bf126a | ||
|
|
42de968f8f | ||
|
|
ed8f024e42 | ||
|
|
e534369c5b | ||
|
|
fed797df93 | ||
|
|
6a44dbfe83 | ||
|
|
28c5357f11 | ||
|
|
a8824f92a1 | ||
|
|
e57b69973f | ||
|
|
d6798eed1e | ||
|
|
50383673ca | ||
|
|
022dbe32e5 | ||
|
|
baac4eb698 | ||
|
|
8aa8f452d5 | ||
|
|
54255cf923 | ||
|
|
bd3d82d224 | ||
|
|
b14ad9f3ab | ||
|
|
261b4646e4 | ||
|
|
d62e1c4468 | ||
|
|
5b0266c462 | ||
|
|
06118dcabb | ||
|
|
984c852ab6 | ||
|
|
47414cfd0e | ||
|
|
fdf4fdfb6a | ||
|
|
5ee0396a02 | ||
|
|
724eb9c658 | ||
|
|
936ab05882 | ||
|
|
817a43b178 | ||
|
|
0f16f42887 | ||
|
|
641c199c8c | ||
|
|
04dd6b900f | ||
|
|
536a05210e | ||
|
|
60d16016e8 | ||
|
|
9222075c7a | ||
|
|
641dcdff85 | ||
|
|
668649be7a | ||
|
|
0de936d474 | ||
|
|
47bd8b0ce7 | ||
|
|
2287c9951c | ||
|
|
94c28ebbc5 | ||
|
|
b4739f8f03 | ||
|
|
c32cd36861 | ||
|
|
945562b34d | ||
|
|
af0f89774e | ||
|
|
c6c4098759 | ||
|
|
fa1c79e15c | ||
|
|
0705c26513 | ||
|
|
140fa4b7c0 | ||
|
|
cd88901868 | ||
|
|
8eec97deb5 | ||
|
|
4331a7c9f4 | ||
|
|
f4fcfeba9d | ||
|
|
48aefb1862 | ||
|
|
bda9584346 | ||
|
|
ed1154af67 | ||
|
|
c368eacd87 | ||
|
|
949c59230f | ||
|
|
1114538f41 | ||
|
|
b0f83291ee | ||
|
|
e91e4ff430 | ||
|
|
4ad290d4bd | ||
|
|
37c6e2c0af | ||
|
|
b4a45afce9 | ||
|
|
330ff6eb47 | ||
|
|
c681eb1bdb | ||
|
|
de533e7031 | ||
|
|
ae6ed3f23f | ||
|
|
32aa209b75 | ||
|
|
e8c25d8ac5 | ||
|
|
718ad4b398 | ||
|
|
8d81a394ee | ||
|
|
d661c8b54e | ||
|
|
e45e782e7d | ||
|
|
5aa422dde1 | ||
|
|
24cbe3d281 | ||
|
|
13f5e76015 | ||
|
|
35c65ba890 | ||
|
|
8f2b82b832 | ||
|
|
7f1a206a52 | ||
|
|
8c503a226e | ||
|
|
cdcf9eaf67 | ||
|
|
a651e55dc9 | ||
|
|
d041a3f4f9 | ||
|
|
3ea05cb705 | ||
|
|
3f27ab1e44 | ||
|
|
e99b170d0a | ||
|
|
49e1a1c410 | ||
|
|
93ab3d5666 |
@@ -3,7 +3,8 @@ root = true
|
||||
|
||||
; Unix-style newlines
|
||||
[*]
|
||||
end_of_line = LF
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
|
||||
21
.env.dist
21
.env.dist
@@ -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
35
.github/workflows/coverage.yaml
vendored
Normal 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
28
.github/workflows/linting.yaml
vendored
Normal 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
58
.github/workflows/testing.yaml
vendored
Normal 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
|
||||
@@ -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
12
.gitignore
vendored
@@ -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
|
||||
|
||||
78
.travis.yml
78
.travis.yml
@@ -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
|
||||
|
||||
80
CHANGELOG.md
80
CHANGELOG.md
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
16
README.md
16
README.md
@@ -1,9 +1,9 @@
|
||||
# Kimai 2 - online time-tracker
|
||||
|
||||
[](https://github.com/kevinpapst/kimai2/actions)
|
||||
[](https://codecov.io/gh/kevinpapst/kimai2)
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
[](https://packagist.org/packages/kevinpapst/kimai2)
|
||||
[](https://travis-ci.org/kevinpapst/kimai2)
|
||||
[](https://codecov.io/gh/kevinpapst/kimai2)
|
||||
[](https://gitter.im/kimai2/support)
|
||||
[](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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
102
UPGRADING.md
102
UPGRADING.md
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
1348
assets/css/emails.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
70
assets/js/plugins/KimaiConfirmationLink.js
Normal file
70
assets/js/plugins/KimaiConfirmationLink.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
29
assets/js/plugins/KimaiDateUtils.js
Normal file
29
assets/js/plugins/KimaiDateUtils.js
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
35
assets/js/plugins/KimaiForm.js
Normal file
35
assets/js/plugins/KimaiForm.js
Normal 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);
|
||||
}
|
||||
}
|
||||
168
assets/js/plugins/KimaiFormSelect.js
Normal file
168
assets/js/plugins/KimaiFormSelect.js
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
73
assets/js/plugins/KimaiMultiUpdateTable.js
Normal file
73
assets/js/plugins/KimaiMultiUpdateTable.js
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
97
assets/js/widgets/KimaiPaginatedBoxWidget.js
Normal file
97
assets/js/widgets/KimaiPaginatedBoxWidget.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
73
assets/js/widgets/KimaiReloadPageWidget.js
Normal file
73
assets/js/widgets/KimaiReloadPageWidget.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
10
assets/sass/admin-lte.scss
Normal file
10
assets/sass/admin-lte.scss
Normal 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';
|
||||
@@ -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';
|
||||
|
||||
52
assets/sass/content-header.scss
Normal file
52
assets/sass/content-header.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
23
assets/sass/selectpicker.scss
Normal file
23
assets/sass/selectpicker.scss
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
*/
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
21
bin/console
21
bin/console
@@ -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');
|
||||
}
|
||||
|
||||
111
composer.json
111
composer.json
@@ -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
6323
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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],
|
||||
];
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
packages:
|
||||
avatars:
|
||||
base_path: 'avatars'
|
||||
|
||||
@@ -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
|
||||
3
config/packages/dev/mailer.yaml
Normal file
3
config/packages/dev/mailer.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: 'null://null'
|
||||
@@ -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']
|
||||
@@ -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:
|
||||
|
||||
119
config/packages/doctrine/mysql.yaml
Normal file
119
config/packages/doctrine/mysql.yaml
Normal 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
|
||||
56
config/packages/doctrine/sqlite.yaml
Normal file
56
config/packages/doctrine/sqlite.yaml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
3
config/packages/mailer.yaml
Normal file
3
config/packages/mailer.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: '%env(MAILER_URL)%'
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
swiftmailer:
|
||||
url: '%env(MAILER_URL)%'
|
||||
spool: { type: 'memory' }
|
||||
3
config/packages/test/mailer.yaml
Normal file
3
config/packages/test/mailer.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
mailer:
|
||||
dsn: 'null://null'
|
||||
@@ -1,2 +0,0 @@
|
||||
swiftmailer:
|
||||
disable_delivery: true
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
controllers:
|
||||
resource: '../src/Controller/'
|
||||
resource: '../../src/Controller/'
|
||||
type: annotation
|
||||
prefix: /{_locale}
|
||||
requirements:
|
||||
|
||||
@@ -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%'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
32
config/services-saml.yaml
Normal 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
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ~
|
||||
|
||||
36
package.json
36
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
10
phpstan.neon
10
phpstan.neon
@@ -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
|
||||
|
||||
@@ -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
0
public/avatars/.gitignore
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/build/0.f7fae2b9.js
Normal file
1
public/build/0.f7fae2b9.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
public/build/1.c24a9c6f.js
Normal file
1
public/build/1.c24a9c6f.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
public/build/2.a47ad919.js
Normal file
1
public/build/2.a47ad919.js
Normal file
File diff suppressed because one or more lines are too long
1
public/build/app.2bde4971.css
Normal file
1
public/build/app.2bde4971.css
Normal file
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
Reference in New Issue
Block a user