Compare commits

..

3 Commits

Author SHA1 Message Date
Kevin Papst
8a5bc3bfc1 merge base 2024-10-04 11:17:24 +02:00
Kevin Papst
0c2236d7f6 Merge branch 'refs/heads/main' into docker-ips
# Conflicts:
#	.docker/Dockerfile.kimai-base
2024-10-04 11:16:17 +02:00
Kevin Papst
a629e3d83e pass proxy values for Apache webserver 2024-09-22 16:22:03 +02:00
1942 changed files with 36839 additions and 65342 deletions

View File

@@ -8,6 +8,20 @@
PassEnv DATABASE_URL PassEnv DATABASE_URL
PassEnv MAILER_URL PassEnv MAILER_URL
PassEnv TRUSTED_PROXIES PassEnv TRUSTED_PROXIES
PassEnv TRUSTED_HOSTS
SetEnvIf X-Forwarded-Proto https HTTPS=on
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
RemoteIPHeader X-Forwarded-For
# Define a macro to set the RemoteIPTrustedProxy directive
<Macro SetTrustedProxy PROXY>
RemoteIPTrustedProxy ${PROXY}
</Macro>
# Use the macro, passing the environment variable TRUSTED_PROXIES
Use SetTrustedProxy %{TRUSTED_PROXIES}e
<Directory "/opt/kimai/public"> <Directory "/opt/kimai/public">
Require all granted Require all granted

View File

@@ -13,19 +13,20 @@ try {
]); ]);
} catch(\Exception $ex) { } catch(\Exception $ex) {
switch ($ex->getCode()) { switch ($ex->getCode()) {
// we can immediately stop startup here and show the error message
case 1045: case 1045:
// we can immediately stop here and show the error message
echo 'Access denied (1045)'; echo 'Access denied (1045)';
die(1); die(1);
// we can immediately stop startup here and show the error message
case 1049: case 1049:
// error "Unknown database (1049)" can be ignored, the database will be created by Kimai echo 'Unknown database (1049)';
return; die(2);
// a lot of errors share the same meaningless error code zero // a lot of errors share the same meaningless error code zero
case 0: case 0:
// this error includes the database name, so we can only search for the static part of the error message // this error includes the database name, so we can only search for the static part of the error message
if (stripos($ex->getMessage(), 'SQLSTATE[HY000] [1049] Unknown database') !== false) { if (stripos($ex->getMessage(), 'SQLSTATE[HY000] [1049] Unknown database') !== false) {
// error "Unknown database (1049)" can be ignored, the database will be created by Kimai echo 'Unknown database (0-1049)';
return; die(3);
} }
switch ($ex->getMessage()) { switch ($ex->getMessage()) {
// eg. no response (fw) - the startup script should retry it a couple of times // eg. no response (fw) - the startup script should retry it a couple of times

View File

@@ -76,6 +76,7 @@ function handleStartup() {
function prepareKimai() { function prepareKimai() {
# These are idempotent, so we can run them on every start-up # These are idempotent, so we can run them on every start-up
/opt/kimai/bin/console -n kimai:install /opt/kimai/bin/console -n kimai:install
/opt/kimai/bin/console -n kimai:update
if [ ! -z "$ADMINPASS" ] && [ ! -a "$ADMINMAIL" ]; then if [ ! -z "$ADMINPASS" ] && [ ! -a "$ADMINMAIL" ]; then
/opt/kimai/bin/console kimai:user:create admin "$ADMINMAIL" ROLE_SUPER_ADMIN "$ADMINPASS" /opt/kimai/bin/console kimai:user:create admin "$ADMINMAIL" ROLE_SUPER_ADMIN "$ADMINPASS"
fi fi

View File

@@ -1,17 +1,23 @@
# editorconfig.org ; top-most EditorConfig file
root = true root = true
; Unix-style newlines
[*] [*]
charset = utf-8
end_of_line = lf end_of_line = lf
indent_size = 4 charset = utf-8
[*.php]
indent_style = space indent_style = space
insert_final_newline = true indent_size = 4
trim_trailing_whitespace = true
[{compose.yaml,compose.*.yaml}] [*.twig]
indent_size = 2 indent_style = space
indent_size = 4
[*.md] [*.yaml]
trim_trailing_whitespace = false indent_style = space
indent_size = 4
[*.yml]
indent_style = space
indent_size = 4

View File

@@ -1,18 +1,7 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
# #
# * .env contains default values for the environment variables needed by the app # You should NOT use this file in production, but instead move the environment variables to your webserver configuration.
# * .env.local uncommitted file with local overrides # The .env file is only existing to simplify the initial setup!
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
# #
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
#================================================================================ #================================================================================
# Configure your database connection and set the correct server version. # Configure your database connection and set the correct server version.
@@ -27,34 +16,32 @@
# For MySQL that would be "serverVersion=5.7" as in: # For MySQL that would be "serverVersion=5.7" as in:
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7 # DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
# #
# For MariaDB it would be "serverVersion=10.11.15-MariaDB": # For MariaDB it would be "serverVersion=10.5.8-MariaDB":
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.11.15-MariaDB # DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
# #
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.11.15-MariaDB DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
# Running behind reverse proxies? Try these: #================================================================================
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2 # The full documentation can be found at https://www.kimai.org/documentation/emails.html
# TRUSTED_HOSTS=localhost|example.com #
# Email will be sent with this address as sender:
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###
###> symfony/mailer ###
# Documentation at https://www.kimai.org/documentation/emails.html
MAILER_FROM=kimai@example.com MAILER_FROM=kimai@example.com
# Email connection (disabled by default) - see documentation for the format # Email connection (disabled by default) - see documentation for the format
MAILER_URL=null://null MAILER_URL=null://null
###< symfony/mailer ###
###> nelmio/cors-bundle ### #================================================================================
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' # do not change, unless you are developing for Kimai
###< nelmio/cors-bundle ### APP_ENV=prod
###> symfony/routing ### #================================================================================
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands. # should be changed to a unique character sequence, used for hashing cookies
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands APP_SECRET=change_this_to_something_unique
DEFAULT_URI=http://localhost
###< symfony/routing ### #================================================================================
# Running behind reverse proxies? Try these:
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
# TRUSTED_HOSTS=localhost,example.com
#================================================================================
# unlikely, that you need to change this one
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$

11
.eslintrc.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
env: {
"browser": true,
"node": true,
"es6": true,
"amd": true,
},
parser: '@babel/eslint-parser',
extends: ['eslint:recommended'],
ignorePatterns: ["assets/*.js"],
}

20
.gitattributes vendored
View File

@@ -1,20 +0,0 @@
.docker export-ignore
.github export-ignore
assets export-ignore
tests export-ignore
.codecov.yml export-ignore
.editorconfig export-ignore
eslint.config.js export-ignore
eslint.config.mjs export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.php-cs-fixer.dist.php export-ignore
php-cs-fixer.dist.php export-ignore
babel.config.js export-ignore
package.json export-ignore
php-cs-fixer.sh export-ignore
phpstan.neon export-ignore
phpstan.sh export-ignore
phpunit.xml.dist export-ignore
webpack.config.js export-ignore
yarn.lock export-ignore

1
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,2 @@
github: [kevinpapst] github: [kevinpapst]
open_collective: kimai
custom: ["https://www.kimai.org/", "https://www.kimai.cloud/", "https://www.kevinpapst.de/"] custom: ["https://www.kimai.org/", "https://www.kimai.cloud/", "https://www.kevinpapst.de/"]

View File

@@ -53,12 +53,12 @@ body:
attributes: attributes:
label: Which PHP version are you using? label: Which PHP version are you using?
options: options:
- "8.5"
- "8.4"
- "8.2"
- "8.3"
- "8.1" - "8.1"
- Unknown - Unknown
- "8.2"
- "8.0"
- "7.4"
- "7.3"
- Other (please mention below) - Other (please mention below)
validations: validations:
required: true required: true

View File

@@ -7,7 +7,6 @@ exclude-labels:
- 'release' - 'release'
exclude-contributors: exclude-contributors:
- 'dependabot' - 'dependabot'
- 'weblate'
change-template: '- $TITLE (#$NUMBER)' change-template: '- $TITLE (#$NUMBER)'
change-title-escapes: '\<*_&`#@' change-title-escapes: '\<*_&`#@'
version-resolver: version-resolver:
@@ -22,8 +21,8 @@ version-resolver:
- 'translation' - 'translation'
default: patch default: patch
template: | template: |
**Compatible with PHP 8.1 to 8.5** **Compatible with PHP 8.1 to 8.3**
$CHANGES $CHANGES
Involved in this release: $CONTRIBUTORS Involved in this release: $CONTRIBUTORS - thank you!

View File

@@ -1,11 +1,6 @@
name: 'Docker Build' name: 'Docker Build'
on: on:
workflow_dispatch:
inputs:
kimai_tag:
description: 'Kimai tag to build'
required: true
release: release:
types: [released] types: [released]
@@ -18,9 +13,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -28,24 +20,7 @@ jobs:
password: ${{secrets.DOCKERHUB_PASSWORD}} password: ${{secrets.DOCKERHUB_PASSWORD}}
- name: Determine version - name: Determine version
run: | run: echo "kimai_version=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
input="${{ github.event.inputs.kimai_tag }}"
# Determine between manual trigger and release event
if [ -z "$input" ]; then
echo "Using release tag: ${{ github.event.release.tag_name }}"
version="${{ github.event.release.tag_name }}"
else
echo "Using tag provided: $input"
version="$input"
fi
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
echo "Invalid version number: $version"
exit 1
fi
echo "kimai_version=$version" >> $GITHUB_ENV
- name: FPM image - name: FPM image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -60,6 +35,7 @@ jobs:
tags: | tags: |
kimai/kimai2:latest kimai/kimai2:latest
kimai/kimai2:fpm kimai/kimai2:fpm
kimai/kimai2:fpm-${{ env.kimai_version }}
push: true push: true
- name: Apache image - name: Apache image

View File

@@ -1,21 +1,23 @@
name: Check .lock files name: Check .lock files
on: on:
pull_request_target: null pull_request: null
push: push:
branches: branches:
- main - main
permissions:
pull-requests: read
jobs: jobs:
lockfiles: lockfiles:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Verify lock file integrity name: Verify lock file integrity
steps: steps:
- name: Clone Kimai
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Prevent file change - name: Prevent file change
uses: xalvarez/prevent-file-change-action@v3 uses: xalvarez/prevent-file-change-action@v1
with: with:
githubToken: ${{ secrets.GITHUB_TOKEN }} githubToken: ${{ secrets.GITHUB_TOKEN }}
pattern: .*\.lock$|^\.github\/.*$ pattern: .*\.lock$
trustedAuthors: kevinpapst, dependabot trustedAuthors: kevinpapst, dependabot

View File

@@ -19,7 +19,7 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy: strategy:
matrix: matrix:
php: ['8.4', '8.5'] php: ['8.1', '8.2', '8.3']
name: Integration (${{ matrix.php }}) name: Integration (${{ matrix.php }})
steps: steps:
@@ -62,7 +62,7 @@ jobs:
run: APP_ENV=dev bin/console kimai:reload -n run: APP_ENV=dev bin/console kimai:reload -n
- name: Check codestyles - name: Check codestyles
run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none --format=checkstyle | cs2pr run: PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none --format=checkstyle | cs2pr
- name: Run PHPStan for application - name: Run PHPStan for application
run: vendor/bin/phpstan analyse -c phpstan.neon --no-progress --error-format=checkstyle | cs2pr run: vendor/bin/phpstan analyse -c phpstan.neon --no-progress --error-format=checkstyle | cs2pr
@@ -87,7 +87,7 @@ jobs:
MAILER_URL: null://localhost MAILER_URL: null://localhost
- name: Full test-suite - name: Full test-suite
if: matrix.php != '8.5' if: matrix.php != '8.2'
run: vendor/bin/phpunit tests/ run: vendor/bin/phpunit tests/
env: env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35 DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
@@ -95,7 +95,7 @@ jobs:
MAILER_URL: null://localhost MAILER_URL: null://localhost
- name: Full test-suite with coverage - name: Full test-suite with coverage
if: matrix.php == '8.5' if: matrix.php == '8.2'
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
env: env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35 DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
@@ -103,8 +103,8 @@ jobs:
MAILER_URL: null://localhost MAILER_URL: null://localhost
- name: Upload code coverage - name: Upload code coverage
if: matrix.php == '8.5' if: matrix.php == '8.2'
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml files: ./coverage.xml

12
.gitignore vendored
View File

@@ -1,9 +1,8 @@
# some hosters require a htaccess to change the PHP version # some hosters require a htaccess to change the PHP version
/public/.htaccess /public/.htaccess
/.env-*
/.idea/ /.idea/
.DS_Store .DS_Store
var/templates/
# custom apache rules e.g. to deactivate ioncube loader # custom apache rules e.g. to deactivate ioncube loader
/public/.user.ini /public/.user.ini
@@ -15,15 +14,12 @@ var/templates/
# YARN 2 # YARN 2
/.yarnrc.yml /.yarnrc.yml
/.yarn /.yarn
/.pnp.*
# for keeping empty directories # for keeping empty directories
/config/packages/local.yaml /config/packages/local.yaml
/config/bundles-local.php /config/bundles-local.php
/var/dev/*
/var/data/* /var/data/*
/var/cache/* /var/cache/*
/var/share/*
/var/invoices* /var/invoices*
/var/export* /var/export*
/var/log/* /var/log/*
@@ -37,6 +33,7 @@ var/templates/
/.env.local.php /.env.local.php
/.env.*.local /.env.*.local
/config/secrets/prod/prod.decrypt.private.php /config/secrets/prod/prod.decrypt.private.php
.env
/public/bundles/ /public/bundles/
/vendor/ /vendor/
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
@@ -47,8 +44,9 @@ var/templates/
###< phpunit/phpunit ### ###< phpunit/phpunit ###
###> friendsofphp/php-cs-fixer ### ###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php .php_cs
/.php-cs-fixer.cache .php_cs.cache
.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ### ###< friendsofphp/php-cs-fixer ###
###> symfony/phpunit-bridge ### ###> symfony/phpunit-bridge ###

View File

@@ -8,7 +8,6 @@ file that was distributed with this source code.
COMMENT; COMMENT;
$fixer = new PhpCsFixer\Config(); $fixer = new PhpCsFixer\Config();
$fixer->setUnsupportedPhpVersionAllowed(true);
$fixer $fixer
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setRiskyAllowed(true) ->setRiskyAllowed(true)
@@ -44,7 +43,6 @@ $fixer
'single_line_after_imports' => true, 'single_line_after_imports' => true,
'switch_case_semicolon_to_colon' => true, 'switch_case_semicolon_to_colon' => true,
'switch_case_space' => true, 'switch_case_space' => true,
'php_unit_method_casing' => true,
'array_syntax' => [ 'array_syntax' => [
'syntax' => 'short' 'syntax' => 'short'
], ],
@@ -143,7 +141,7 @@ $fixer
'whitespace_after_comma_in_array' => true, 'whitespace_after_comma_in_array' => true,
'yoda_style' => false, 'yoda_style' => false,
'ternary_to_null_coalescing' => true, 'ternary_to_null_coalescing' => true,
'modifier_keywords' => ['elements' => [ 'visibility_required' => ['elements' => [
'const', 'const',
'method', 'method',
'property', 'property',

View File

@@ -1,16 +1,15 @@
# Contributing # Contributing
Kimai is an open source project, contributions made by the community are welcome. Kimai is an open source project, contributions made by the community are welcome.
But we can only accept contributions with a signed CLA (Contributor License Agreement) to prevent issues in the future (you will see a link when opening a PR).
Send your ideas, code reviews, pull requests and feature requests to help to improve this project. Send your ideas, code reviews, pull requests and feature requests to help to improve this project.
## Pull request rules ## Pull request rules
- Make your changes in a new git branch, based on the latest code in `main`
- Apply our code-style by running `composer codestyle-fix` - Apply our code-style by running `composer codestyle-fix`
- Run the static code analysis with `composer phpstan` - Run the static code analysis with `composer phpstan`
- Verify everything still works with `composer tests` - Verify everything still works with `composer tests-unit` and `composer tests-integration`
- Add tests for your changes - Add tests for your changes
- When sending in a PR, you must accept that your contributions/code will be published under MIT license (see the [LICENSE](LICENSE) file as well), otherwise your PR will be closed
- If one of the PR checks/builds fails, fix it before asking for a review
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html). Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).

View File

@@ -13,8 +13,8 @@
# #
# docker build --no-cache -t kimai-fpm --build-arg BASE=fpm . # docker build --no-cache -t kimai-fpm --build-arg BASE=fpm .
# docker build --no-cache -t kimai-apache --build-arg BASE=apache . # docker build --no-cache -t kimai-apache --build-arg BASE=apache .
# docker run -d --name kimai-apache-app kimai-apache # docker run -d --name kimai-apache
# docker exec -ti kimai-apache-app /bin/bash # docker exec -ti kimai-apache /bin/bash
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Official PHP images: https://hub.docker.com/_/php/ # Official PHP images: https://hub.docker.com/_/php/
# https://github.com/docker-library/docs/blob/master/php/README.md#supported-tags-and-respective-dockerfile-links # https://github.com/docker-library/docs/blob/master/php/README.md#supported-tags-and-respective-dockerfile-links
@@ -78,8 +78,8 @@ RUN apk add --no-cache \
# apache debian php extension base # apache debian php extension base
FROM php:8.3-apache-bookworm AS apache-php-ext-base FROM php:8.3-apache-bookworm AS apache-php-ext-base
RUN apt-get update && \ RUN apt-get update
apt-get install -y \ RUN apt-get install -y \
libldap2-dev \ libldap2-dev \
libicu-dev \ libicu-dev \
libpng-dev \ libpng-dev \
@@ -165,10 +165,12 @@ RUN apt-get update && \
libpng16-16 \ libpng16-16 \
libzip4 \ libzip4 \
libxslt1.1 \ libxslt1.1 \
libfreetype6 \ libfreetype6 && \
unzip && \
echo "Listen 8001" > /etc/apache2/ports.conf && \ echo "Listen 8001" > /etc/apache2/ports.conf && \
a2enmod rewrite && \ a2enmod rewrite && \
a2enmod headers && \
a2enmod remoteip && \
a2enmod macro && \
touch /use_apache touch /use_apache
COPY .docker/000-default.conf /etc/apache2/sites-available/000-default.conf COPY .docker/000-default.conf /etc/apache2/sites-available/000-default.conf
@@ -218,18 +220,20 @@ COPY --from=php-ext-intl /usr/local/lib/php/extensions/no-debug-non-zts-20230831
COPY --from=php-ext-opcache /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini COPY --from=php-ext-opcache /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
########################### ###########################
# fetch Kimai sources # Shared tools
########################### ###########################
FROM alpine:latest AS git-prod # full kimai source
FROM alpine:latest AS git-dev
ARG KIMAI ARG KIMAI
ARG TIMEZONE ARG TIMEZONE
# the convention in the Kimai repository is: tags are always version numbers, branch names always start with a letter RUN apk add --no-cache git && \
# if the KIMAI variable starts with a number (e.g. 2.24.0) we assume its a tag, otherwise its a branch git clone --depth 1 --branch ${KIMAI} https://github.com/kimai/kimai.git /opt/kimai
RUN [[ $KIMAI =~ ^[0-9] ]] && export REF='tags' || export REF='heads' && \
wget -O "/opt/kimai.tar.gz" "https://github.com/kimai/kimai/archive/refs/${REF}/${KIMAI}.tar.gz" && \ # production kimai source
tar -xpzf /opt/kimai.tar.gz -C /opt/ && \ FROM git-dev AS git-prod
mv /opt/kimai-${KIMAI} /opt/kimai WORKDIR /opt/kimai
RUN rm -r tests
########################### ###########################
# global base build # global base build
@@ -257,12 +261,13 @@ RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} >
# copy startup script & DB checking script # copy startup script & DB checking script
COPY .docker/dbtest.php /dbtest.php COPY .docker/dbtest.php /dbtest.php
COPY .docker/entrypoint.sh /entrypoint.sh COPY .docker/startup.sh /startup.sh
ENV DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=8.3" ENV DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=8.3"
ENV APP_SECRET=change_this_to_something_unique ENV APP_SECRET=change_this_to_something_unique
# The default container name for nginx is nginx # The default container name for nginx is nginx
ENV TRUSTED_PROXIES=nginx,localhost,127.0.0.1 ENV TRUSTED_PROXIES=nginx,localhost,127.0.0.1
ENV TRUSTED_HOSTS=nginx,localhost,127.0.0.1
ENV MAILER_FROM=kimai@example.com ENV MAILER_FROM=kimai@example.com
ENV MAILER_URL=null://localhost ENV MAILER_URL=null://localhost
ENV ADMINPASS= ENV ADMINPASS=
@@ -275,7 +280,7 @@ ENV COMPOSER_ALLOW_SUPERUSER=1
VOLUME [ "/opt/kimai/var" ] VOLUME [ "/opt/kimai/var" ]
CMD [ "/entrypoint.sh" ] CMD [ "/startup.sh" ]
########################### ###########################
# final builds # final builds
@@ -284,9 +289,10 @@ CMD [ "/entrypoint.sh" ]
# development build # development build
FROM base AS dev FROM base AS dev
# copy kimai develop source # copy kimai develop source
COPY --from=git-prod --chown=www-data:www-data /opt/kimai /opt/kimai COPY --from=git-dev --chown=www-data:www-data /opt/kimai /opt/kimai
COPY .docker /assets COPY .docker /assets
# do the composer deps installation # do the composer deps installation
RUN echo \$PATH
RUN \ RUN \
export COMPOSER_HOME=/composer && \ export COMPOSER_HOME=/composer && \
composer --no-ansi install --working-dir=/opt/kimai --optimize-autoloader && \ composer --no-ansi install --working-dir=/opt/kimai --optimize-autoloader && \
@@ -302,7 +308,7 @@ ENV APP_ENV=dev
ENV DATABASE_URL= ENV DATABASE_URL=
ENV memory_limit=512M ENV memory_limit=512M
# the "prod" stage (production build) is configured as last stage in the file, as this is the default target in BuildKit # production build
FROM base AS prod FROM base AS prod
# copy kimai production source # copy kimai production source
COPY --from=git-prod --chown=www-data:www-data /opt/kimai /opt/kimai COPY --from=git-prod --chown=www-data:www-data /opt/kimai /opt/kimai
@@ -312,7 +318,7 @@ RUN \
export COMPOSER_HOME=/composer && \ export COMPOSER_HOME=/composer && \
composer --no-ansi install --working-dir=/opt/kimai --no-dev --optimize-autoloader && \ composer --no-ansi install --working-dir=/opt/kimai --no-dev --optimize-autoloader && \
composer --no-ansi clearcache && \ composer --no-ansi clearcache && \
composer --no-ansi require --update-no-dev --working-dir=/opt/kimai laminas/laminas-ldap && \ composer --no-ansi require --working-dir=/opt/kimai laminas/laminas-ldap && \
cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini && \ cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini && \
sed -i "s/expose_php = On/expose_php = Off/g" /usr/local/etc/php/php.ini && \ sed -i "s/expose_php = On/expose_php = Off/g" /usr/local/etc/php/php.ini && \
sed -i "s/;opcache.enable=1/opcache.enable=1/g" /usr/local/etc/php/php.ini && \ sed -i "s/;opcache.enable=1/opcache.enable=1/g" /usr/local/etc/php/php.ini && \

View File

@@ -21,6 +21,13 @@ authentication via SAML/LDAP/Database, two-factor authentication (2FA) with TOTP
user/customer/project specific rates, advanced search & filtering, money and time budgets, advanced reporting, support for [plugins](https://www.kimai.org/store/) user/customer/project specific rates, advanced search & filtering, money and time budgets, advanced reporting, support for [plugins](https://www.kimai.org/store/)
and so much more. and so much more.
### Versions
There are two [versions](https://www.kimai.org/documentation/versions.html) of Kimai existing:
- [Version 2](https://github.com/kimai/kimai) — the current stable release (PHP 8.1+)
- [Version 1](https://github.com/kimai/kimai/tree/1.x) — do **NOT** use, EOL since mid of 2023 (PHP 7.4)
### Links ### Links
- [Home](https://www.kimai.org) — Kimai project homepage - [Home](https://www.kimai.org) — Kimai project homepage
@@ -29,7 +36,7 @@ and so much more.
### Requirements ### Requirements
- PHP 8.1.3 minimum with support for 8.2, 8.3, 8.4, 8.5 - PHP 8.1.3 minimum (support for PHP 8.2 and 8.3)
- MariaDB or MySQL - MariaDB or MySQL
- A webserver and subdomain (subdirectory is not supported) - A webserver and subdomain (subdirectory is not supported)
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip` - PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
@@ -44,7 +51,7 @@ and so much more.
There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html). There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html).
And if you don't want to host Kimai, you can use the [Cloud version](https://www.kimai.cloud/) of it. And if you don't want to host Kimai, you can use [the Cloud version](https://www.kimai.cloud/) of it.
### Updating Kimai ### Updating Kimai

View File

@@ -7,4 +7,5 @@ As announced in the [README](README.md) security fixes will only be added to the
| main branch | :white_check_mark: | | main branch | :white_check_mark: |
| older releases | :x: | | older releases | :x: |
You find all information in our [Bughunter documentation](https://www.kimai.org/documentation/bughunter.html). You find all information in our [Bughunter documentation](https://www.kimai.org/documentation/bughunter.html).

View File

@@ -1,50 +0,0 @@
# Upgrading Kimai - Version 2.x
_Make sure to create a backup before you start!_
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
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.
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
**!! This release requires minimum PHP version to 8.1 !!**
### Breaking changes
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
- removed: configuring the `dashboard` is not supported any longer
- removed: custom translation files via `theme.branding.translation`
- removed: changing the plugin directory via `kimai.plugin_dir`
### Developer
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
- Role names are forced to be uppercase
- Removed unused `public/avatars/` directory
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
- `date_full` => `date_time`
- `duration_decimal` => `duration(true)`
- `currency` => `currency_name`
- `country` => `country_name`
- `language` => `language_name`
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
- `composer require symfony/amazon-mailer`
- `composer require symfony/google-mailer`
- `composer require symfony/mailchimp-mailer`
- `composer require symfony/mailgun-mailer`
- `composer require symfony/postmark-mailer`
- `composer require symfony/sendgrid-mailer`

View File

@@ -1,37 +0,0 @@
# Upgrading Kimai - Version 3.x
_Make sure to create a backup before you start!_
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how 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.
## 3.0
**!! This release requires minimum PHP version 8.4 !!**
### Rename .env
Rename your file `.env` to `.env.local` or even better: move all variables to your webserver/container environment.
### Developer
Do not use method chaining: all fluent interface, especially in Entities, are no longer supported.
- Require PHP 8.4
- Bump to Symfony 7.4
- Removed old API token `X-AUTH-USER` and `X-AUTH-TOKEN`
- Removed `TimesheetConstraint` - use a normal `Constraint` as base class and attach the `#[App\Validator\Attribute\TimesheetConstraint]` attribute
- Removed `ProjectConstraint` - use a `FormExtension` and attach your custom constraints
- Interface `MetaTableTypeInterface` has new methods: `getSection()`, `setSection()`
- Interface `ExportRendererInterface` has new methods: `getType()`, `isInternal()`
- Interface `ExportableItem` has new methods: `getTags()`, `getBreak()`
- Removed and renamed translations, most important `action.edit` => `edit`, `my.profile` => `user_profile`
- Removed `User::isExportDecimal()`
- Use duration format `HH:mm` in default PDF exports
- Replace Twig `AppVariable` with custom implementation
- You need to adjust your templates if you access anything else then `app.locale`, `app.user`. `app.current_route`.
- Most often used:
- Replace `app.request.locale` with `app.locale`
- Replace `app.request.attributes.get('_route')` with `app.current_route`

View File

@@ -1,3 +1,50 @@
# Upgrading Kimai # Upgrading Kimai - Version2.x
The most recent version is Kimai 3, please read the changelog at [UPGRADING-3.md](UPGRADING-3.md). _Make sure to create a backup before you start!_
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
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.
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
**!! This release requires minimum PHP version to 8.1 !!**
### Breaking changes
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
- removed: configuring the `dashboard` is not supported any longer
- removed: custom translation files via `theme.branding.translation`
- removed: changing the plugin directory via `kimai.plugin_dir`
### Developer
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
- Role names are forced to be uppercase
- Removed unused `public/avatars/` directory
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
- `date_full` => `date_time`
- `duration_decimal` => `duration(true)`
- `currency` => `currency_name`
- `country` => `country_name`
- `language` => `language_name`
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
- `composer require symfony/amazon-mailer`
- `composer require symfony/google-mailer`
- `composer require symfony/mailchimp-mailer`
- `composer require symfony/mailgun-mailer`
- `composer require symfony/postmark-mailer`
- `composer require symfony/sendgrid-mailer`

View File

@@ -1,5 +0,0 @@
require('highlight.js/styles/github-dark.css');
const hljs = require('highlight.js/lib/common');
global.hljs = hljs;

View File

@@ -1,4 +1,2 @@
/**
* @deprecated use invoice-pdf instead
*/
require('./sass/_invoice.scss'); require('./sass/_invoice.scss');

View File

@@ -43,7 +43,6 @@ import KimaiHotkeys from "./plugins/KimaiHotkeys";
import KimaiRemoteModal from "./plugins/KimaiRemoteModal"; import KimaiRemoteModal from "./plugins/KimaiRemoteModal";
import KimaiUser from "./plugins/KimaiUser"; import KimaiUser from "./plugins/KimaiUser";
import KimaiAutocompleteTags from "./forms/KimaiAutocompleteTags"; import KimaiAutocompleteTags from "./forms/KimaiAutocompleteTags";
import KimaiMonthPicker from "./forms/KimaiMonthPicker";
export default class KimaiLoader { export default class KimaiLoader {
@@ -77,7 +76,6 @@ export default class KimaiLoader {
kimai.registerPlugin(new KimaiTeamForm()); kimai.registerPlugin(new KimaiTeamForm());
kimai.registerPlugin(new KimaiCopyDataForm()); kimai.registerPlugin(new KimaiCopyDataForm());
kimai.registerPlugin(new KimaiDateNowForm()); kimai.registerPlugin(new KimaiDateNowForm());
kimai.registerPlugin(new KimaiMonthPicker());
kimai.registerPlugin(new KimaiForm()); kimai.registerPlugin(new KimaiForm());
kimai.registerPlugin(new KimaiHotkeys()); kimai.registerPlugin(new KimaiHotkeys());

View File

@@ -154,7 +154,7 @@ export default class KimaiPlugin {
const width = Math.max( const width = Math.max(
document.documentElement.clientWidth, document.documentElement.clientWidth,
window.innerWidth || 0 window.innerWidth || 0
); )
return width < 576; return width < 576;
} }

View File

@@ -29,7 +29,7 @@ export default class KimaiCopyDataForm extends KimaiFormPlugin {
/** /**
* @param {HTMLFormElement} form * @param {HTMLFormElement} form
*/ */
activateForm(form) activateForm(form) // eslint-disable-line no-unused-vars
{ {
if (this._eventHandler === undefined) { if (this._eventHandler === undefined) {
this._eventHandler = (event) => { this._eventHandler = (event) => {
@@ -48,10 +48,10 @@ export default class KimaiCopyDataForm extends KimaiFormPlugin {
if (element.dataset.event !== undefined) { if (element.dataset.event !== undefined) {
for (const event of element.dataset.event.split(' ')) { for (const event of element.dataset.event.split(' ')) {
target.dispatchEvent(new Event(event)); target.dispatchEvent(new Event(event));
const form = target.closest('form');
if (form !== null) {
form.dispatchEvent(new Event(event));
} }
} else if (element.dataset.eventBubbles !== undefined) {
for (const event of element.dataset.eventBubbles.split(' ')) {
target.dispatchEvent(new Event(event, {bubbles: true}));
} }
} }
event.preventDefault(); event.preventDefault();
@@ -63,7 +63,7 @@ export default class KimaiCopyDataForm extends KimaiFormPlugin {
/** /**
* @param {HTMLFormElement} form * @param {HTMLFormElement} form
*/ */
destroyForm(form) destroyForm(form) // eslint-disable-line no-unused-vars
{ {
form.removeEventListener('click', this._eventHandler); form.removeEventListener('click', this._eventHandler);
} }

View File

@@ -43,10 +43,7 @@ export default class KimaiDateNowForm extends KimaiFormPlugin {
const formElement = document.getElementById(linkTarget.dataset.target); const formElement = document.getElementById(linkTarget.dataset.target);
if (!formElement.disabled) { if (!formElement.disabled) {
formElement.value = this.getDateUtils().format(linkTarget.dataset.format, null); formElement.value = this.getDateUtils().format(linkTarget.dataset.format, null);
// this should usually work
formElement.dispatchEvent(new Event('change', {bubbles: true})); formElement.dispatchEvent(new Event('change', {bubbles: true}));
// this is required for Litepicker to pick the new date (with autoRefresh option)
formElement.dispatchEvent(new Event('keyup', {bubbles: true}));
} }
event.preventDefault(); event.preventDefault();

View File

@@ -62,10 +62,13 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
plugins.push('remove_button'); plugins.push('remove_button');
} }
if (node.dataset['order'] !== undefined && node.dataset['order'] === '1') { /*
//plugins.push('caret_position'); const isOrdering = false;
if (isOrdering) {
plugins.push('caret_position');
plugins.push('drag_drop'); plugins.push('drag_drop');
} }
*/
let options = { let options = {
// see https://github.com/orchidjs/tom-select/issues/543#issuecomment-1664342257 // see https://github.com/orchidjs/tom-select/issues/543#issuecomment-1664342257
@@ -80,11 +83,8 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
// if there are more than X entries, the other ones are hidden and can only be found // if there are more than X entries, the other ones are hidden and can only be found
// by typing some characters to trigger the internal option search // by typing some characters to trigger the internal option search
// see App\Form\Type\TagsType::MAX_AMOUNT_SELECT // see App\Form\Type\TagsType::MAX_AMOUNT_SELECT
// TODO make this value configurable with a data attribute
maxOptions: 500, maxOptions: 500,
sortField:[{field: '$order'}, {field: '$score'}], sortField:[{field: '$order'}, {field: '$score'}],
// required so it works in table.responsive, but requires z-index 1056, because bootstrap modal would otherwise hide it
dropdownParent: 'body',
}; };
let render = { let render = {
@@ -236,13 +236,8 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
options.push(optGroup); options.push(optGroup);
} }
// log the one with a group name first (e.g. non-global activities)
options.forEach(child => node.appendChild(child)); options.forEach(child => node.appendChild(child));
emptyOpts.forEach(child => node.appendChild(child));
// append the ones with no parent at the end (e.g. global activities)
const optGroupEmpty = this._createOptgroup('');
emptyOpts.forEach(child => optGroupEmpty.appendChild(child));
node.appendChild(optGroupEmpty);
// if available, re-select the previous selected option (mostly usable for global activities) // if available, re-select the previous selected option (mostly usable for global activities)
node.value = selectedValue; node.value = selectedValue;
@@ -440,7 +435,7 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
targetSelect.dataset['reloading'] = '0'; targetSelect.dataset['reloading'] = '0';
targetSelect.disabled = false; targetSelect.disabled = false;
}); });
}; }
document.addEventListener('change', this._eventHandlerApiSelects); document.addEventListener('change', this._eventHandlerApiSelects);
} }
@@ -476,14 +471,14 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
newValue = [...targetField.selectedOptions].map(o => o.value); newValue = [...targetField.selectedOptions].map(o => o.value);
} else if (newValue !== '') { } else if (newValue !== '') {
if (targetField.type === 'date') { if (targetField.type === 'date') {
const timeId = targetField.id.replace('_date', '_time'); const timeId = targetField.id.replace('_date', '_time')
const timeElement = document.getElementById(timeId); const timeElement = document.getElementById(timeId);
const time = timeElement === null ? '12:00:00' : timeElement.value; const time = timeElement === null ? '12:00:00' : timeElement.value;
// using 12:00 as fallback, because timezone handling might change the date if we use 00:00 // using 12:00 as fallback, because timezone handling might change the date if we use 00:00
const newDate = this.getDateUtils().fromHtml5Input(newValue, time); const newDate = this.getDateUtils().fromHtml5Input(newValue, time);
newValue = this.getDateUtils().formatForAPI(newDate, false); newValue = this.getDateUtils().formatForAPI(newDate, false);
} else if (targetField.type === 'text' && targetField.name.includes('date')) { } else if (targetField.type === 'text' && targetField.name.includes('date')) {
const timeId = targetField.id.replace('_date', '_time'); const timeId = targetField.id.replace('_date', '_time')
const timeElement = document.getElementById(timeId); const timeElement = document.getElementById(timeId);
// using 12:00 as fallback, because timezone handling might change the date if we use 00:00 // using 12:00 as fallback, because timezone handling might change the date if we use 00:00
let time = '12:00:00'; let time = '12:00:00';

View File

@@ -6,7 +6,7 @@
*/ */
/*! /*!
* [KIMAI] KimaiFormTomselectPlugin: base form plugin for everyone using tomselect * [KIMAI] KimaiFormPlugin: base class for all none ID plugin that handle forms
*/ */
import KimaiFormPlugin from './KimaiFormPlugin'; import KimaiFormPlugin from './KimaiFormPlugin';
@@ -28,12 +28,12 @@ export default class KimaiFormTomselectPlugin extends KimaiFormPlugin {
return null; return null;
} }
const tpl = this.translate('select.search.create'); const tpl = this.translate('select.search.create');
const tplReplaced = tpl.replace('%input%', '<strong>' + name + '</strong>'); const tplReplaced = tpl.replace('%input%', '<strong>' + name + '</strong>')
return '<div class="create">' + tplReplaced + '</div>'; return '<div class="create">' + tplReplaced + '</div>';
}, },
no_results: (data, escape) => { no_results: (data, escape) => {
const tpl = this.translate('select.search.notfound'); const tpl = this.translate('select.search.notfound');
const tplReplaced = tpl.replace('%input%', '<strong>' + escape(data.input) + '</strong>'); const tplReplaced = tpl.replace('%input%', '<strong>' + escape(data.input) + '</strong>')
return '<div class="no-results">' + tplReplaced + '</div>'; return '<div class="no-results">' + tplReplaced + '</div>';
}, },
}; };

View File

@@ -1,44 +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] KimaiDatePicker: single date selects (currently unused)
*/
import KimaiFormPlugin from "./KimaiFormPlugin";
export default class KimaiMonthPicker extends KimaiFormPlugin {
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form) // eslint-disable-line no-unused-vars
{
return true;
}
/**
* @param {HTMLFormElement} form
*/
activateForm(form)
{
const input = document.createElement('input');
input.setAttribute('type','month');
const notADateValue = 'not-a-month';
input.setAttribute('value', notADateValue);
if (input.value === notADateValue) {
const polyfills = form.querySelectorAll('a.input-month-polyfill');
polyfills.forEach(i => i.classList.toggle('d-none'));
const inputs = form.querySelectorAll('input[type="month"]');
inputs.forEach(i => i.classList.toggle('d-none'));
}
}
}

View File

@@ -40,27 +40,17 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
if (this._beginTime !== undefined) { if (this._beginTime !== undefined) {
this._beginTime.removeEventListener('change', this._beginListener); this._beginTime.removeEventListener('change', this._beginListener);
delete this._beginListener;
this._beginTime.removeEventListener('blur', this._beginBlurListener);
delete this._beginBlurListener;
delete this._beginTime; delete this._beginTime;
} }
if (this._endTime !== undefined) { if (this._endTime !== undefined) {
this._endTime.removeEventListener('change', this._endListener); this._endTime.removeEventListener('change', this._endListener);
delete this._endListener;
this._endTime.removeEventListener('blur', this._endBlurListener);
delete this._endBlurListener;
delete this._endTime; delete this._endTime;
} }
if (this._duration !== undefined) { if (this._duration !== undefined) {
this._duration.removeEventListener('change', this._durationListener); this._duration.removeEventListener('change', this._durationListener);
delete this._durationListener; delete this._durationListener;
this._duration.removeEventListener('keydown', this._durationKeyListener);
delete this._durationKeyListener;
this._duration.removeEventListener('blur', this._durationBlurListener);
delete this._durationBlurListener;
delete this._duration; delete this._duration;
} }
@@ -118,21 +108,13 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
} }
this._beginListener = () => this._changedBegin(); this._beginListener = () => this._changedBegin();
this._beginBlurListener = () => this._parseBeginTime();
this._endListener = () => this._changedEnd(); this._endListener = () => this._changedEnd();
this._endBlurListener = () => this._parseEndTime();
this._durationListener = () => this._changedDuration(); this._durationListener = () => this._changedDuration();
this._durationKeyListener = (event) => this._changeDurationOnKeypress(event);
this._durationBlurListener = () => this._parseDuration();
this._beginDate.addEventListener('change', this._beginListener); this._beginDate.addEventListener('change', this._beginListener);
this._beginTime.addEventListener('change', this._beginListener); this._beginTime.addEventListener('change', this._beginListener);
this._beginTime.addEventListener('blur', this._beginBlurListener);
this._endTime.addEventListener('change', this._endListener); this._endTime.addEventListener('change', this._endListener);
this._endTime.addEventListener('blur', this._endBlurListener);
this._duration.addEventListener('change', this._durationListener); this._duration.addEventListener('change', this._durationListener);
this._duration.addEventListener('keydown', this._durationKeyListener);
this._duration.addEventListener('blur', this._durationBlurListener);
if (this._duration !== null && this._durationToggle !== null) { if (this._duration !== null && this._durationToggle !== null) {
this._durationToggleListener = () => { this._durationToggleListener = () => {
@@ -142,159 +124,6 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
} }
} }
_parseBeginTime()
{
if (this._beginTime.value === '') {
return;
}
let newBeginTime = this._formatTimeForParsing(this._beginTime.value, this._beginTime.dataset['format']);
if (newBeginTime !== this._beginTime.value) {
this._beginTime.value = newBeginTime;
this._changedBegin();
}
}
_parseEndTime()
{
if (this._endTime.value === '') {
return;
}
let newEndTime = this._formatTimeForParsing(this._endTime.value, this._endTime.dataset['format']);
if (newEndTime !== this._endTime.value) {
this._endTime.value = newEndTime;
this._changedEnd();
}
}
_parseDuration()
{
if (this._duration.value === '') {
return;
}
this._setDurationAsString(this._getParsedDuration());
}
/**
* Receives a time, written by a human, probably in an invalid format.
* This method supports 12-hour or 24-hour format, the format string contains an uppercase "A" in case of the 12-hour format.
* If it is 12-hour format, then always en-US locallized with AM/PM.
*
* Ruleset:
* - Some locales use a dot instead of a colon, always replace the dot in HH.mm with a colon as in HH:mm
* - If there is an "am" or "pm", always uppercase them
* - Split the string into time and prefix: if AM/PM is included remove it and remember for later
* - If the time is a 1 or 2 character long number: use as hours
* - If the time now is 3 character long: use the 1 char as hour and the 2 and 3 char as minute
* - If the time now is 4 character long: use the 1 and 2 char as hour and the 3 and 4 char as minutes
* - If the format is 12-hour: try to identify the correct time and suffix
* - If the format is 12-hour and misses the AM/PM: try to detect whether it
* - If the time contains AM or PM, make sure that it is always prefixed by a space character
*
* @param {string} time
* @param {string} format
* @returns {string}
* @private
*/
_formatTimeForParsing(time, format)
{
let formatted = time.trim();
// replace invalid separators with colon
formatted = formatted.replace(/\.|;|,/g, ':');
// uppercase 12-hour format
formatted = formatted.replace(/am/i, 'AM');
formatted = formatted.replace(/pm/i, 'PM');
// Split time and AM/PM suffix if present
let suffix = '';
let hour = 0;
let minute = 0;
let timePart = formatted;
const ampmMatch = formatted.match(/\s*(AM|PM)$/i);
if (ampmMatch) {
suffix = ampmMatch[1].toUpperCase();
timePart = formatted.replace(/\s*(AM|PM)$/i, '').trim();
}
if (timePart.indexOf(':') !== -1) {
const match = timePart.match(/(?:(\d+):)?(\d+)/);
hour = parseInt(match?.[1] || 0, 10);
minute = parseInt(match?.[2] || 0, 10);
} else {
timePart = timePart.replace(/:/, '');
if (/^\d{1,2}$/.test(timePart)) {
hour = timePart;
}
if (/^\d{3}$/.test(timePart)) {
hour = timePart.slice(0, 1);
minute = timePart.slice(1);
}
if (/^\d{4}$/.test(timePart)) {
hour = timePart.slice(0, 2);
minute = timePart.slice(2);
}
}
hour = parseInt(hour);
minute = parseInt(minute);
// just in case a person entered a wrong time like 35 hours
hour = hour % 24;
minute = minute % 60;
// format is 12-hour
if (format.toUpperCase().indexOf('A') !== -1) {
// time entered in 24-hour: convert to 12-hour format
if (hour > 12 && hour < 24) {
hour = hour - 12;
suffix = 'PM';
}
// if the person forgot to add a suffix, calculate it and convert time
if (suffix === '') {
if (hour === 0) {
hour = 12;
suffix = 'AM';
} else if (hour === 12) {
suffix = 'PM';
} else {
suffix = 'AM';
}
}
if (suffix === 'PM' && hour === 0) {
hour = 12;
}
} else {
// this is the 34-hour format branch
// check if the person entered time in 12-hour format and convert it
if (suffix === 'AM' && hour === 12) {
hour = 0;
} else if (suffix === 'PM' && hour !== 12) {
hour = (hour + 12) % 24;
}
// make sure we have no suffix
suffix = '';
}
formatted = hour + ':' + minute.toString().padStart(2, '0');
if (suffix !== '') {
formatted = formatted + ' ' + suffix.trim();
}
return formatted;
}
_isDurationConnected() _isDurationConnected()
{ {
if (this._duration === null && this._durationToggle === null) { if (this._duration === null && this._durationToggle === null) {
@@ -458,14 +287,14 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
/** /**
* Ruleset: * Ruleset:
* - invalid or empty duration => skip * - invalid duration => skip
* - if begin and end are empty: set begin to now and end to duration * - if begin and end are empty: set begin to now and end to duration
* - if begin is empty and end is not empty: set begin to end minus duration * - if begin is empty and end is not empty: set begin to end minus duration
* - if begin is not empty and end is empty and duration is > 0 (running records = 0): set end to begin plus duration * - if begin is not empty and end is empty and duration is > 0 (running records = 0): set end to begin plus duration
*/ */
_changedDuration() _changedDuration()
{ {
if (!this._isDurationConnected() || this._duration.value === '') { if (!this._isDurationConnected()) {
return; return;
} }
@@ -486,11 +315,11 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
if (begin === null && end === null) { if (begin === null && end === null) {
const newBegin = DateTime.now(); const newBegin = DateTime.now();
this._applyDateToField(newBegin, this._beginDate, this._beginTime); this._applyDateToField(newBegin, this._beginDate, this._beginTime);
this._addSecondsToEndDate(newBegin, seconds); this._applyDateToField(newBegin.plus({seconds: seconds}), null, this._endTime);
} else if (begin === null && end !== null) { } else if (begin === null && end !== null) {
this._applyDateToField(end.minus({seconds: seconds}), this._beginDate, this._beginTime); this._applyDateToField(end.minus({seconds: seconds}), this._beginDate, this._beginTime);
} else if (begin !== null && seconds >= 0) { } else if (begin !== null && seconds >= 0) {
this._addSecondsToEndDate(begin, seconds); this._applyDateToField(begin.plus({seconds: seconds}), null, this._endTime);
} }
} }
@@ -538,23 +367,7 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
*/ */
_getParsedDuration() _getParsedDuration()
{ {
return this.getDateUtils().parseDuration(this._duration.value); return this.getDateUtils().parseDuration(this._duration.value.toUpperCase());
}
/**
* @param {DateTime} dateTime
* @param {int} seconds
* @private
*/
_addSecondsToEndDate(dateTime, seconds)
{
// if the duration is longer than one day, the end field should be empty
// so kimai can calculate it after submitting the data from start + duration
if (seconds < 86400) {
this._applyDateToField(dateTime.plus({seconds: seconds}), null, this._endTime);
} else {
this._endTime.value = '';
}
} }
/** /**
@@ -577,122 +390,4 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
timeField.value = this.getDateUtils().format(timeField.dataset['format'], dateTime); timeField.value = this.getDateUtils().format(timeField.dataset['format'], dateTime);
} }
/**
* @param {KeyboardEvent} event
* @private
*/
_changeDurationOnKeypress(event)
{
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'PageUp':
case 'PageDown':
case 'Home':
case 'End':
this._setDurationAsString(this._getParsedDuration());
break;
default:
return; // Ignore other keys
}
this._changeTimeOnKeypress(event, this._duration, 99999, this._durationListener);
}
/**
* This method helps the user to change a duration field with simple keyboard interaction:
* - Read the current duration from the given timeField input in format HH:MM (no seconds)
* - Change the duration based on the rules below
* - Write the new duration back to the field
* - If the field is empty or invalid it uses 00:00 as start-time
* - Duration cannot exceed maxtime (which is given in minutes)
* - Duration cannot drop below 00:00
* - Read the position of the cursor and decide whether to increase minutes or hours: if the cursor is in the hour section (before the colon) change hours, if the cursor is in the minute section (after the colon) change minutes
* - It reads the pressed key from the given KeyboardEvent and changes the duration accordingly to the rules below
*
* Rules to apply when a key is pressed:
* - ArrowUp key to increase the duration (either 5 minutes or 1 hour, depending on the cursor position)
* - ArrowDown key to decrease the duration (either 5 minutes or 1 hour, depending on the cursor position)
* - PageUp key to increase the duration by 1 hour
* - PageDown key to decrease the duration by 1 hour
* - Home key to set the duration to 08:00
* - End key to set the duration to 00:00
* - all other keys are ignored
*
* @param {KeyboardEvent} event
* @param {HTMLElement} timeField
* @param {int} maxTime
* @param {function} changeCallback
* @private
*/
_changeTimeOnKeypress(event, timeField, maxTime, changeCallback)
{
// Parse current value or default to 00:00
let value = timeField.value || '00:00';
let [hours, minutes] = value.split(':').map(Number);
if (isNaN(hours)) { hours = 0; }
if (isNaN(minutes)) { minutes = 0; }
// Cursor position: before or after colon
const cursorPos = timeField.selectionStart || 0;
const colonPos = value.indexOf(':');
const inHour = cursorPos <= colonPos;
// Helper to clamp values
const clamp = (h, m) => {
let total = h * 60 + m;
if (total < 0) { total = 0; }
if (total > maxTime) { total = maxTime; }
h = Math.floor(total / 60);
m = total % 60;
return [h, m];
};
switch (event.key) {
case 'ArrowUp':
if (inHour) {
[hours, minutes] = clamp(hours + 1, minutes);
} else {
[hours, minutes] = clamp(hours, minutes + 5);
}
break;
case 'ArrowDown':
if (inHour) {
[hours, minutes] = clamp(hours - 1, minutes);
} else {
[hours, minutes] = clamp(hours, minutes - 5);
}
break;
case 'PageUp':
[hours, minutes] = clamp(hours + 1, minutes);
event.preventDefault();
break;
case 'PageDown':
[hours, minutes] = clamp(hours - 1, minutes);
event.preventDefault();
break;
case 'Home':
// TODO this should use the configured working time for today
hours = 8;
minutes = 0;
event.preventDefault();
break;
case 'End':
hours = 0;
minutes = 0;
event.preventDefault();
break;
default:
return; // Ignore other keys
}
// Format and set value
timeField.value = `${hours}:${minutes.toString().padStart(2, '0')}`;
// trigger update of linked fields
changeCallback(timeField);
// Move cursor to original position if possible
setTimeout(() => {
timeField.setSelectionRange(cursorPos, cursorPos);
}, 0);
}
} }

View File

@@ -73,7 +73,6 @@ export default class KimaiAPILink extends KimaiPlugin {
const ALERT = this.getContainer().getPlugin('alert'); const ALERT = this.getContainer().getPlugin('alert');
const successHandle = () => { const successHandle = () => {
EVENTS.trigger(eventName); EVENTS.trigger(eventName);
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
if (attributes['msgSuccess'] !== undefined) { if (attributes['msgSuccess'] !== undefined) {
ALERT.success(attributes['msgSuccess']); ALERT.success(attributes['msgSuccess']);
} }
@@ -83,7 +82,6 @@ export default class KimaiAPILink extends KimaiPlugin {
if (attributes['msgError'] !== undefined) { if (attributes['msgError'] !== undefined) {
message = attributes['msgError']; message = attributes['msgError'];
} }
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
API.handleError(message, error); API.handleError(message, error);
}; };
@@ -92,8 +90,6 @@ export default class KimaiAPILink extends KimaiPlugin {
data = attributes['payload']; data = attributes['payload'];
} }
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
if (method === 'PATCH') { if (method === 'PATCH') {
API.patch(url, data, successHandle, errorHandle); API.patch(url, data, successHandle, errorHandle);
} else if (method === 'POST') { } else if (method === 'POST') {

View File

@@ -69,7 +69,7 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
_getModal() _getModal()
{ {
return Modal.getOrCreateInstance(this._getModalElement()); return Modal.getOrCreateInstance(this._getModalElement())
} }
/** /**
@@ -217,6 +217,8 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
const eventName = form.dataset['formEvent']; const eventName = form.dataset['formEvent'];
/** @type {KimaiEvent} alert */ /** @type {KimaiEvent} alert */
const events = this.getContainer().getPlugin('event'); const events = this.getContainer().getPlugin('event');
/** @type {KimaiAlert} alert */
const alert = this.getContainer().getPlugin('alert');
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -257,8 +259,15 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
} else { } else {
events.trigger(eventName); events.trigger(eventName);
// try to find form defined message first, but
let msg = form.dataset['msgSuccess'];
// if that is not available: use a generic fallback message
if (msg === null || msg === undefined || msg === '') {
msg = 'action.update.success';
}
this._isDirty = false; this._isDirty = false;
this._getModal().hide(); this._getModal().hide();
alert.success(msg);
} }
}); });
}) })
@@ -268,8 +277,6 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
message = 'action.update.error'; message = 'action.update.error';
} }
/** @type {KimaiAlert} alert */
const alert = this.getContainer().getPlugin('alert');
alert.error(message, error.message); alert.error(message, error.message);
// this is useful for changing form fields and retrying to save (and in development to test form changes) // this is useful for changing form fields and retrying to save (and in development to test form changes)

View File

@@ -208,7 +208,7 @@ export default class KimaiAlert extends KimaiPlugin {
const toast = new Toast(element); const toast = new Toast(element);
element.addEventListener('hidden.bs.toast', function () { element.addEventListener('hidden.bs.toast', function () {
container.removeChild(element); container.removeChild(element);
}); })
toast.show(); toast.show();
} }

View File

@@ -273,10 +273,7 @@ export default class KimaiDateUtils extends KimaiPlugin {
let luxonDuration = null; let luxonDuration = null;
if (duration.indexOf(':') !== -1) { if (duration.indexOf(':') !== -1) {
const match = duration.match(/(?:(\d+):)?(\d+)(?::(\d+))?/); const [, hours, minutes, seconds] = duration.match(/(\d+):(\d+)(?::(\d+))*/);
const hours = parseInt(match?.[1] || 0, 10);
const minutes = parseInt(match?.[2] || 0, 10);
const seconds = parseInt(match?.[3] || 0, 10);
luxonDuration = Duration.fromObject({hours: hours, minutes: minutes, seconds: seconds}); luxonDuration = Duration.fromObject({hours: hours, minutes: minutes, seconds: seconds});
} else if (duration.indexOf('.') !== -1 || duration.indexOf(',') !== -1) { } else if (duration.indexOf('.') !== -1 || duration.indexOf(',') !== -1) {
duration = duration.replace(/,/, '.'); duration = duration.replace(/,/, '.');

View File

@@ -36,7 +36,7 @@ export default class KimaiHotkeys extends KimaiPlugin {
elements[0].click(); elements[0].click();
} }
} }
}); })
} }
// adopted from Bootstrap 5.1.1, MIT // adopted from Bootstrap 5.1.1, MIT

View File

@@ -13,14 +13,9 @@ import KimaiPlugin from '../KimaiPlugin';
export default class KimaiMultiUpdateTable extends KimaiPlugin { export default class KimaiMultiUpdateTable extends KimaiPlugin {
getId()
{
return 'datatable-batch-action';
}
init() init()
{ {
if (document.getElementsByClassName('multi_update_all').length === 0) { if (document.getElementById('multi_update_all') === null) {
return; return;
} }
@@ -28,28 +23,34 @@ export default class KimaiMultiUpdateTable extends KimaiPlugin {
// via KimaiDatable and everything inside will be removed, including event listeners // via KimaiDatable and everything inside will be removed, including event listeners
const element = document.querySelector('div.page-body'); const element = document.querySelector('div.page-body');
element.addEventListener('change', (event) => { element.addEventListener('change', (event) => {
if (event.target.matches('.multi_update_all')) { if (event.target.matches('#multi_update_all')) {
this.toggle(event.target.checked, event.target.closest('table')); // the "check all" checkbox in the upper start corner of the table
const checked = event.target.checked;
for (const element of document.querySelectorAll('.multi_update_single')) {
element.checked = checked;
}
this._toggleForm();
event.stopPropagation(); event.stopPropagation();
} else if (event.target.matches('.multi_update_single')) { } else if (event.target.matches('.multi_update_single')) {
// single checkboxes in front of each row // single checkboxes in front of each row
this._toggleDatatable(event.target.closest('table')); this._toggleForm();
event.stopPropagation(); event.stopPropagation();
} }
}); });
element.addEventListener('click', (event) => { element.addEventListener('click', (event) => {
if (event.target.matches('.multi_update_table_action')) { if (event.target.matches('.multi_update_table_action')) {
const selectedButton = event.target; const selectedItem = event.target;
const form = selectedButton.form; const ids = this._getSelectedIds();
const ids = form.querySelector('.multi_update_ids').value.split(','); const form = document.getElementById('multi_update_form');
const question = form.dataset['question'].replace(/%action%/, selectedButton.textContent).replace(/%count%/, ids.length.toString()); const question = form.dataset['question'].replace(/%action%/, selectedItem.textContent).replace(/%count%/, ids.length.toString());
/** @type {KimaiAlert} ALERT */ /** @type {KimaiAlert} ALERT */
const ALERT = this.getPlugin('alert'); const ALERT = this.getPlugin('alert');
ALERT.question(question, function(value) { ALERT.question(question, function(value) {
if (value) { if (value) {
form.action = selectedButton.dataset['href']; const form = document.getElementById('multi_update_form');
form.action = selectedItem.dataset['href'];
form.submit(); form.submit();
} }
}); });
@@ -57,72 +58,29 @@ export default class KimaiMultiUpdateTable extends KimaiPlugin {
}); });
} }
/** _getSelectedIds()
* @param {boolean} checked
* @param {HTMLTableElement} table
*/
toggle(checked, table)
{ {
for (const element of table.querySelectorAll('.multi_update_single')) {
element.checked = checked;
}
this._toggleDatatable(table);
}
/**
* @param {boolean} checked
*/
toggleAll(checked)
{
for (const element of document.querySelectorAll('.multi_update_all')) {
this._toggleAll(checked, element);
}
}
/**
* @param {boolean} checked
* @param {string} name
*/
toggleByName(checked, name)
{
for (const element of document.querySelectorAll('#multi_update_all_' + name)) {
this._toggleAll(checked, element);
}
}
/**
* @param {boolean} checked
* @param {Element} name
*/
_toggleAll(checked, element)
{
element.checked = checked;
this.toggle(checked, element.closest('table'));
}
/**
* @param {HTMLTableElement} table
* @private
*/
_toggleDatatable(table)
{
const card = table.closest('div.card.data_table');
let ids = []; let ids = [];
for (const box of table.querySelectorAll('input.multi_update_single:checked')) { for (const box of document.querySelectorAll('input.multi_update_single:checked')) {
ids.push(box.value); ids.push(box.value);
} }
card.querySelector('.multi_update_ids').value = ids.join(','); return ids;
}
_toggleForm()
{
const ids = this._getSelectedIds();
document.getElementById('multi_update_table_entities').value = ids.join(',');
if (ids.length > 0) { if (ids.length > 0) {
for (const element of card.querySelectorAll('.multi_update_form_hide')) { for (const element of document.getElementsByClassName('multi_update_form_hide')) {
element.style.setProperty('display', 'none', 'important'); element.style.setProperty('display', 'none', 'important');
} }
card.querySelector('form.multi_update_form').style.display = null;//'block'; document.getElementById('multi_update_form').style.display = null;//'block';
} else { } else {
card.querySelector('form.multi_update_form').style.setProperty('display', 'none', 'important'); document.getElementById('multi_update_form').style.setProperty('display', 'none', 'important');
for (const element of card.querySelectorAll('.multi_update_form_hide')) { for (const element of document.getElementsByClassName('multi_update_form_hide')) {
element.style.display = null; element.style.display = null;
} }
} }

View File

@@ -43,7 +43,7 @@ export default class KimaiNotification extends KimaiPlugin {
callback(false); callback(false);
} }
}); });
} catch (e) { // eslint-disable-line no-unused-vars } catch (e) {
Notification.requestPermission((permission) => { Notification.requestPermission((permission) => {
if (permission === "granted") { if (permission === "granted") {
callback(true); callback(true);

View File

@@ -6,17 +6,12 @@
*/ */
/*! /*!
* [KIMAI] KimaiRemoteModal: load remote content (without forms) into a modal * [KIMAI] KimaiRecentActivities: responsible to reload the users recent activities
*/ */
import KimaiPlugin from '../KimaiPlugin'; import KimaiPlugin from '../KimaiPlugin';
import { Modal } from 'bootstrap'; import { Modal } from 'bootstrap';
/**
* Use like this:
* <a href="{{ path('your-route') }}" class="remote-modal-load" data-modal-id="remote_modal" data-modal-class="p-0" data-modal-title="Some title" title="Some title">Modal</a>
* <a href="{{ path('your-route') }}" class="remote-modal-load" data-modal-title="Some title" title="Some title">Modal</a>
*/
export default class KimaiRemoteModal extends KimaiPlugin { export default class KimaiRemoteModal extends KimaiPlugin {
constructor() constructor()
@@ -39,11 +34,43 @@ export default class KimaiRemoteModal extends KimaiPlugin {
this._showModal(event.currentTarget); this._showModal(event.currentTarget);
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
}; }
for (let link of document.querySelectorAll(this._selector)) { for (let link of document.querySelectorAll(this._selector)) {
link.addEventListener('click', this.handle); link.addEventListener('click', this.handle);
} }
document.addEventListener('kimai.closeRemoteModal', () => { this._hide(); });
}
/**
* @param {HTMLElement} element
* @private
*/
_initElement(element)
{
for (let link of element.querySelectorAll('a.remote-modal-reload')) {
link.addEventListener('click', this.handle);
}
}
_hide()
{
this._getModal().hide();
}
_getModalElement()
{
return document.getElementById('remote_modal');
}
/**
* @returns {Modal}
* @private
*/
_getModal()
{
return Modal.getOrCreateInstance(this._getModalElement());
} }
/** /**
@@ -58,33 +85,21 @@ export default class KimaiRemoteModal extends KimaiPlugin {
return; return;
} }
let modalSelector = 'remote_modal';
if (element.dataset['modalId'] !== undefined) {
modalSelector = element.dataset['modalId'];
}
const modalElement = document.getElementById(modalSelector);
if (modalElement === null) {
console.log('Could not find modal with ID: ' + modalSelector);
}
return response.text().then(html => { return response.text().then(html => {
const modalBody = document.createElement('div'); const newFormHtml = document.createElement('div');
modalBody.classList.add('modal-body'); newFormHtml.classList.add('modal-body');
if (element.dataset['modalClass'] !== undefined) { newFormHtml.classList.add('p-0');
modalBody.classList.add(element.dataset['modalClass']); newFormHtml.innerHTML = html;
}
modalBody.innerHTML = html;
for (let link of modalBody.querySelectorAll('a.remote-modal-reload')) { this._initElement(newFormHtml);
link.addEventListener('click', this.handle);
}
modalElement.querySelector('.modal-body').replaceWith(modalBody); const modal = this._getModalElement();
modal.querySelector('.modal-body').replaceWith(newFormHtml);
if (element.dataset['modalTitle'] !== undefined) { if (element.dataset['modalTitle'] !== undefined) {
modalElement.querySelector('.modal-title').textContent = element.dataset['modalTitle']; modal.querySelector('.modal-title').textContent = element.dataset['modalTitle'];
} }
Modal.getOrCreateInstance(modalElement).show(); this._getModal().show();
}); });
}) })
.catch((reason) => { .catch((reason) => {

View File

@@ -42,13 +42,13 @@ export default class KimaiThemeInitializer extends KimaiPlugin {
} }
// at which element we append the loading screen // at which element we append the loading screen
let container = 'div.page-wrapper'; let container = 'body';
if (event.detail !== undefined && event.detail !== null) { if (event.detail !== undefined && event.detail !== null) {
container = event.detail; container = event.detail;
} }
const temp = document.createElement('div'); const temp = document.createElement('div');
temp.innerHTML = '<div class="overlay"><div class="progress progress-sm"><div class="progress-bar progress-bar-indeterminate"></div></div></div>'; temp.innerHTML = '<div class="overlay"><div class="fas fa-sync fa-spin"></div></div>';
this.overlay = temp.firstElementChild; this.overlay = temp.firstElementChild;
document.querySelector(container).append(this.overlay); document.querySelector(container).append(this.overlay);
}); });

View File

@@ -49,11 +49,4 @@ export default class KimaiUser extends KimaiPlugin {
return this.user.superAdmin; return this.user.superAdmin;
} }
/**
* @returns {array}
*/
getRoles() {
return this.user.roles;
}
} }

View File

@@ -14,7 +14,7 @@ import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid'; import timeGridPlugin from '@fullcalendar/timegrid';
import bootstrap5Plugin, { BootstrapTheme } from '@fullcalendar/bootstrap5'; import bootstrap5Plugin, { BootstrapTheme } from '@fullcalendar/bootstrap5';
import googlePlugin from '@fullcalendar/google-calendar'; import googlePlugin from '@fullcalendar/google-calendar';
import iCalendarPlugin from '@fullcalendar/icalendar'; import iCalendarPlugin from '@fullcalendar/icalendar'
import interactionPlugin, { Draggable } from '@fullcalendar/interaction'; import interactionPlugin, { Draggable } from '@fullcalendar/interaction';
import arLocale from '@fullcalendar/core/locales/ar'; import arLocale from '@fullcalendar/core/locales/ar';
import csLocale from '@fullcalendar/core/locales/cs'; import csLocale from '@fullcalendar/core/locales/cs';
@@ -49,7 +49,6 @@ import enGbLocale from '@fullcalendar/core/locales/en-gb';
import enUsLocale from '@fullcalendar/core/locales/en-gb'; import enUsLocale from '@fullcalendar/core/locales/en-gb';
import KimaiColor from './KimaiColor'; import KimaiColor from './KimaiColor';
import KimaiContextMenu from "./KimaiContextMenu"; import KimaiContextMenu from "./KimaiContextMenu";
import { DateTime } from 'luxon';
export default class KimaiCalendar { export default class KimaiCalendar {
@@ -70,6 +69,23 @@ export default class KimaiCalendar {
const DATES = this.kimai.getPlugin('date'); const DATES = this.kimai.getPlugin('date');
/** @type {KimaiAjaxModalForm} MODAL */ /** @type {KimaiAjaxModalForm} MODAL */
const MODAL = this.kimai.getPlugin('modal'); const MODAL = this.kimai.getPlugin('modal');
/** @type {KimaiAlert} ALERT */
const ALERT = this.kimai.getPlugin('alert');
let initialView = 'dayGridMonth';
switch (options['initialView']) {
case 'month':
initialView = 'dayGridMonth';
break;
case 'agendaWeek':
case 'week':
initialView = 'timeGridWeek';
break;
case 'agendaDay':
case 'day':
initialView = 'timeGridDay';
break;
}
// Instead of using "buttonIcons" the theme needs to be adjusted directly // Instead of using "buttonIcons" the theme needs to be adjusted directly
// https://fullcalendar.io/docs/buttonIcons // https://fullcalendar.io/docs/buttonIcons
@@ -103,8 +119,7 @@ export default class KimaiCalendar {
esLocale, euLocale, faLocale, fiLocale, frLocale, heLocale, hrLocale, huLocale, itLocale, jaLocale, koLocale, esLocale, euLocale, faLocale, fiLocale, frLocale, heLocale, hrLocale, huLocale, itLocale, jaLocale, koLocale,
nbLocale, nlLocale, plLocale, ptLocale, ptBrLocale, roLocale, ruLocale, skLocale, svLocale, trLocale, zhLocale, viLocale ], nbLocale, nlLocale, plLocale, ptLocale, ptBrLocale, roLocale, ruLocale, skLocale, svLocale, trLocale, zhLocale, viLocale ],
plugins: [ bootstrap5Plugin, dayGridPlugin, timeGridPlugin, googlePlugin, iCalendarPlugin, interactionPlugin ], plugins: [ bootstrap5Plugin, dayGridPlugin, timeGridPlugin, googlePlugin, iCalendarPlugin, interactionPlugin ],
initialView: this.toInternalViewName(this.options['initialView']), initialView: initialView,
initialDate: this.options['initialDate'],
// https://fullcalendar.io/docs/theming // https://fullcalendar.io/docs/theming
themeSystem: 'bootstrap5', themeSystem: 'bootstrap5',
// https://fullcalendar.io/docs/headerToolbar // https://fullcalendar.io/docs/headerToolbar
@@ -139,9 +154,8 @@ export default class KimaiCalendar {
slotMinTime: this.options['timeframeBegin'] + ':00', slotMinTime: this.options['timeframeBegin'] + ':00',
slotMaxTime: this.options['timeframeEnd'] === '23:59' ? '24:00:00' : (this.options['timeframeEnd'] + ':59'), slotMaxTime: this.options['timeframeEnd'] === '23:59' ? '24:00:00' : (this.options['timeframeEnd'] + ':59'),
// deactivate for auto calculation, which does a good job. // auto calculation seems to do the better job, therefor deactivated
// but 1h seems to be a "normal distance" for calendar apps (like Google and Apple) //slotLabelInterval: this.options['slotDuration'],
slotLabelInterval: '1:00',
// how long should entries look like when they don't have an end // how long should entries look like when they don't have an end
defaultTimedEventDuration: this.options['slotDuration'], defaultTimedEventDuration: this.options['slotDuration'],
@@ -157,20 +171,14 @@ export default class KimaiCalendar {
// once we can configure working days // once we can configure working days
// hiddenDays: [ 2, 4 ] // hiddenDays: [ 2, 4 ]
// when we support holidays and other full day events
// allDaySlot: false,
// dropAccept // dropAccept
dayMaxEventRows: true, dayMaxEventRows: true,
eventMaxStack: this.options['dayLimit'], eventMaxStack: this.options['dayLimit'],
dayMaxEvents: this.options['dayLimit'], dayMaxEvents: this.options['dayLimit'],
// the callbacks "viewDidMount" and "viewWillUnmount" are only called when switching between month and others, not between week and day
datesSet: (dateInfo) => {
document.dispatchEvent(new CustomEvent('kimai.calendar.changeDate', {detail: {
view: this.toExternalViewName(dateInfo.view.type),
date: dateInfo.start.toISOString().split('T')[0],
}}));
},
views: { views: {
dayGrid: { dayGrid: {
dayMaxEventRows: this.options['dayLimit'] dayMaxEventRows: this.options['dayLimit']
@@ -256,14 +264,8 @@ export default class KimaiCalendar {
contextMenu.createFromApi(jsEvent, result); contextMenu.createFromApi(jsEvent, result);
}, (e) => { console.log('Failed to load actions for context menu', e); }); }, (e) => { console.log('Failed to load actions for context menu', e); });
} }
}); })
}, },
// called after all events of one source were set, so this can
// and will be called multiple times before the calendar is initialized
eventsSet: (events) => {
this._renderDayAndWeekSum(this.getCalendar().getCurrentData().viewSpec.type, events);
}
}; };
// ============= DRAG & DROP ============= // ============= DRAG & DROP =============
@@ -280,7 +282,6 @@ export default class KimaiCalendar {
droppable: true, droppable: true,
// drop function handles external draggable events // drop function handles external draggable events
drop: (dropInfo) => { drop: (dropInfo) => {
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
const entry = dropInfo.draggedEl; const entry = dropInfo.draggedEl;
const source = entry.parentElement; const source = entry.parentElement;
let data = JSON.parse(entry.dataset.entry); let data = JSON.parse(entry.dataset.entry);
@@ -323,7 +324,7 @@ export default class KimaiCalendar {
(result) => { (result) => {
const newItem = this.convertSourceForCalendar(result); const newItem = this.convertSourceForCalendar(result);
this.getCalendar().addEvent(newItem, true); this.getCalendar().addEvent(newItem, true);
document.dispatchEvent(new CustomEvent('kimai.reloadedContent')); ALERT.success('action.update.success');
} }
); );
} else { } else {
@@ -333,7 +334,7 @@ export default class KimaiCalendar {
(result) => { (result) => {
const newItem = this.convertSourceForCalendar(result); const newItem = this.convertSourceForCalendar(result);
this.getCalendar().addEvent(newItem, true); this.getCalendar().addEvent(newItem, true);
document.dispatchEvent(new CustomEvent('kimai.reloadedContent')); ALERT.success('action.update.success');
} }
); );
} }
@@ -408,13 +409,13 @@ export default class KimaiCalendar {
this.hidePopover(info.el); this.hidePopover(info.el);
}, },
eventDrop: (eventDropInfo) => { eventDrop: (eventDropInfo) => {
this.changeHandler(eventDropInfo); this.changeHandler(eventDropInfo)
}, },
eventResizeStart: (info) => { eventResizeStart: (info) => {
this.hidePopover(info.el); this.hidePopover(info.el);
}, },
eventResize: (eventResizeInfo) => { eventResize: (eventResizeInfo) => {
this.changeHandler(eventResizeInfo); this.changeHandler(eventResizeInfo)
}, },
}}; }};
} }
@@ -527,44 +528,6 @@ export default class KimaiCalendar {
return (event.source.id.indexOf('kimai-') === 0); return (event.source.id.indexOf('kimai-') === 0);
} }
/**
* @param {string} viewName
* @returns {string}
*/
toExternalViewName(viewName) {
switch(viewName) {
case 'timeGridDay':
return 'day';
case 'timeGridWeek':
return 'week';
case 'dayGridMonth':
default:
return 'month';
}
}
/**
* @param {string} viewName
* @returns {string}
*/
toInternalViewName(viewName) {
switch(viewName) {
case 'day':
case 'agendaDay':
case 'timeGridDay':
return 'timeGridDay';
case 'week':
case 'agendaWeek':
case 'timeGridWeek':
return 'timeGridWeek';
case 'month':
case 'agendaMonth':
case 'dayGridMonth':
default:
return 'dayGridMonth';
}
}
/** /**
* @param {string} name * @param {string} name
* @return {boolean} * @return {boolean}
@@ -605,7 +568,7 @@ export default class KimaiCalendar {
color = apiItem.project.customer.color; color = apiItem.project.customer.color;
} }
} }
if (color === null) { if (color == null) {
color = defaultColor; color = defaultColor;
} }
@@ -702,6 +665,8 @@ export default class KimaiCalendar {
/** @type {KimaiAPI} API */ /** @type {KimaiAPI} API */
const API = this.kimai.getPlugin('api'); const API = this.kimai.getPlugin('api');
/** @type {KimaiAlert} ALERT */
const ALERT = this.kimai.getPlugin('alert');
/** @type {KimaiDateUtils} DATE */ /** @type {KimaiDateUtils} DATE */
const DATES = this.kimai.getPlugin('date'); const DATES = this.kimai.getPlugin('date');
@@ -713,85 +678,13 @@ export default class KimaiCalendar {
payload.end = null; payload.end = null;
} }
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
const updateUrl = this.options.url.update(event.id); const updateUrl = this.options.url.update(event.id);
API.patch(updateUrl, JSON.stringify(payload), () => { API.patch(updateUrl, JSON.stringify(payload), () => {
document.dispatchEvent(new CustomEvent('kimai.reloadedContent')); ALERT.success('action.update.success');
}, (error) => { }, (error) => {
eventArg.revert(); eventArg.revert();
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
API.handleError('action.update.error', error); API.handleError('action.update.error', error);
}); });
} }
/**
* @param {string} view
* @param {EventApi[]} events
* @private
*/
_renderDayAndWeekSum(view, events) {
if (view === 'dayGridMonth') {
// currently we do not display totals in month view
return;
}
/** @type {KimaiDateUtils} DATES */
const DATES = this.kimai.getPlugin('date');
const durations = {};
if (view === 'timeGridWeek') {
// make sure we have an entry for every day of the week, even days without timesheets
document.querySelectorAll(`th.fc-col-header-cell[data-date]`).forEach(cell => {
durations[cell.dataset.date] = 0;
});
}
events.forEach(item => {
const start = DateTime.fromJSDate(item.start).toUTC();
const dateStr = start.toFormat('yyyy-MM-dd');
if (!durations[dateStr]) {
durations[dateStr] = 0;
}
// absences or public holidays are all day
if (item.end !== null) {
const end = DateTime.fromJSDate(item.end).toUTC();
const duration = end.diff(start, 'hours').as('seconds');
durations[dateStr] += duration;
}
});
const dailyTotals = document.querySelectorAll('.fc-dailytotal');
dailyTotals.forEach(element => element.remove());
for (const dateValue in durations) {
const durationValue = durations[dateValue];
if (view === 'timeGridWeek') { // this is the week view
const headerCells = document.querySelectorAll(`th.fc-col-header-cell[data-date="${dateValue}"]`);
headerCells.forEach(cell => {
const newElement = document.createElement('div');
newElement.classList.add('fc-dailytotal');
newElement.textContent = DATES.formatSeconds(durationValue);
cell.appendChild(newElement);
});
}
}
// this is the day view
if (view === 'timeGridDay') {
const dayEl = document.querySelector('th.fc-day');
const dayDate = dayEl.dataset.date;
const dayTotal = document.querySelectorAll('.fc-dailytotal');
dayTotal.forEach(element => element.remove());
const newElement = document.createElement('div');
newElement.classList.add('fc-dailytotal');
newElement.textContent = DATES.formatSeconds(durations[dayDate]);
dayEl.appendChild(newElement);
}
}
} }

View File

@@ -89,7 +89,7 @@ export default class KimaiContextMenu {
} }
dropdownElement.removeEventListener('click', dropdownListener); dropdownElement.removeEventListener('click', dropdownListener);
document.removeEventListener('click', dropdownListener); document.removeEventListener('click', dropdownListener);
}; }
dropdownElement.addEventListener('click', dropdownListener); dropdownElement.addEventListener('click', dropdownListener);
document.addEventListener('click', dropdownListener); document.addEventListener('click', dropdownListener);

View File

@@ -16,6 +16,7 @@ export default class KimaiPaginatedBoxWidget {
constructor(boxId) { constructor(boxId) {
this.selector = boxId; this.selector = boxId;
const widget = document.querySelector(this.selector); const widget = document.querySelector(this.selector);
this.href = widget.dataset['href'];
if (widget.dataset['reload'] !== undefined) { if (widget.dataset['reload'] !== undefined) {
this.events = widget.dataset['reload'].split(' '); this.events = widget.dataset['reload'].split(' ');
@@ -60,7 +61,7 @@ export default class KimaiPaginatedBoxWidget {
// and this event will hide it afterwards // and this event will hide it afterwards
const hideOverlay = () => { const hideOverlay = () => {
document.dispatchEvent(new Event('kimai.reloadedContent')); document.dispatchEvent(new Event('kimai.reloadedContent'));
}; }
window.kimai.getPlugin('fetch').fetch(url) window.kimai.getPlugin('fetch').fetch(url)
.then(response => { .then(response => {

View File

@@ -5,20 +5,13 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
$modal-backdrop-opacity: 0.7 !default; $modal-backdrop-opacity: 0.6 !default;
@import "~@tabler/core/scss/config";
/* /*
THIS DOES NOT WORK AS EXPECTED, AS TABLER IS NOT REGENERATED HERE. THIS DOES NOT WORK AS EXPECTED, AS TABLER IS NOT REGENERATED HERE.
SO WE CANNOT CHANGE VARIABLES, LIKE THE ABOVE... SO WE CANNOT CHANGE VARIABLES, LIKE THE ABOVE...
*/ */
@import '~@tabler/core/dist/css/tabler.rtl.css'; @import '~@tabler/core/dist/css/tabler.rtl.css';
@import '~@tabler/core/dist/css/tabler-themes.rtl.css';
@import '~tom-select/dist/scss/tom-select';
@import '~tom-select/dist/scss/tom-select.bootstrap5';
@import '~@tabler/core/dist/css/tabler-vendors.rtl.css'; @import '~@tabler/core/dist/css/tabler-vendors.rtl.css';
/** /**
@@ -30,6 +23,21 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts/";
@import '~@fortawesome/fontawesome-free/scss/solid'; @import '~@fortawesome/fontawesome-free/scss/solid';
@import '~@fortawesome/fontawesome-free/scss/brands'; @import '~@fortawesome/fontawesome-free/scss/brands';
/**
* We need that, to compile the rest of the file
*/
@import "bootstrap/scss/functions";
@import "@tabler/core/src/scss/variables";
@import "@tabler/core/src/scss/variables-dark";
@import "@tabler/core/src/scss/mixins";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
/** /**
* Kimai related stuff * Kimai related stuff
*/ */

View File

@@ -5,14 +5,11 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
$modal-backdrop-opacity: 0.7 !default; $modal-backdrop-opacity: 0.6 !default;
@import '~@tabler/core/scss/tabler'; @import '~@tabler/core/src/scss/tabler';
@import '~@tabler/core/scss/tabler-themes'; @import '~@tabler/core/src/scss/vendor/litepicker';
@import '~@tabler/core/scss/vendor/litepicker'; @import '~@tabler/core/src/scss/vendor/tom-select';
@import '~tom-select/dist/scss/tom-select.bootstrap5';
@import '~@tabler/core/scss/vendor/tom-select';
/** /**
* Everyone needs icons * Everyone needs icons

View File

@@ -5,30 +5,111 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
body { font-family: sans-serif; font-size: 10pt; margin: 4px; padding: 0; } body {
table, tr, td, th, p, h1, h2, h3, h4, h5 { padding: 0; margin: 0;} font-family: sans-serif;
th { border: none; text-align: left; } font-size: 10pt;
td { vertical-align: top; } margin: 4px;
p { margin-bottom: 12px; } padding: 0;
.text-small { font-size: 7pt; } }
.padding-left { padding-left: 15px; } table, tr, td, th, p, h1, h2, h3, h4, h5 {
.text-center { text-align: center; } padding: 0;
.text-left, .text-begin { text-align: left; } margin: 0;
.text-right, .text-end { text-align: right; } }
.text-nowrap { white-space: nowrap; } th {
.date { font-size: 80%; text-align: right; padding-top: 6px; } border: none;
table { width: 100%; border-spacing: 0; } text-align: left;
table.addresses, p { line-height: 14pt; } }
table.addresses { margin-bottom: 30px; } td {
table.header { padding-bottom: 4px; border-bottom: 1px solid #999; } vertical-align: top;
table.header .title { font-weight: normal; font-size: 140%; } }
table.footer { border-top: 1px solid #999; font-size: 80%; padding-top: 8px; line-height: 14px; } p {
table.footer td { vertical-align: top; } margin-bottom: 12px;
table.items { border-collapse: collapse; margin-top: 50px; margin-bottom: 60px; } }
table.items thead th, table.items tbody td { border-bottom: 1px solid #ccc; } .text-small {
table.items thead th.first, table.items tbody td.first { padding-left: 0; } font-size: 7pt;
table.items thead th.last, table.items tbody td.last, table.items tfoot td.last { padding-right: 0; } }
table.items td, table.items th { padding: 8px 6px; } .padding-left {
table.items tfoot td, table.items tfoot th { padding: 20px 0 0 0; line-height: 1px; } padding-left: 15px;
table.items tfoot td.last { padding-left: 10px; } }
table.items tr.odd { background-color: #f5f5f5; } .text-center {
text-align: center;
}
.text-left, .text-begin {
text-align: left;
}
.text-right, .text-end {
text-align: right;
}
.text-nowrap {
white-space: nowrap;
}
.date {
font-size: 80%;
text-align: right;
padding-top: 6px;
}
table {
width: 100%;
border-spacing: 0;
}
table.addresses, p {
line-height: 14pt;
}
/* The address table */
table.addresses {
margin-bottom: 30px;
}
/* The PDF header */
table.header {
padding-bottom: 4px;
border-bottom: 1px solid #999;
}
table.header .title {
font-weight: normal;
font-size: 140%;
}
/* The PDF footer */
table.footer {
border-top: 1px solid #999;
font-size: 80%;
padding-top: 8px;
line-height: 14px;
}
table.footer td {
vertical-align: top;
}
/* The invoice items list */
table.items {
border-collapse: collapse;
margin-top: 50px;
margin-bottom: 60px;
}
table.items thead th,
table.items tbody td {
border-bottom: 1px solid #ccc;
}
table.items thead th.first,
table.items tbody td.first {
padding-left: 0;
}
table.items thead th.last,
table.items tbody td.last,
table.items tfoot td.last {
padding-right: 0;
}
table.items td,
table.items th {
padding: 8px 6px;
}
table.items tfoot td,
table.items tfoot th {
padding: 20px 0 0 0;
line-height: 1px;
}
table.items tfoot td.last {
padding-left: 10px;
}
table.items tr.odd {
background-color: #f5f5f5;
}

View File

@@ -11,7 +11,8 @@
@import "modal"; @import "modal";
@import "progressbar"; @import "progressbar";
@import "avatar"; @import "avatar";
@import "weekly-hours"; @import "theme-dark";
@import "pages";
@import "tabler-fixes"; @import "tabler-fixes";
@import "help"; @import "help";
@import "rtl"; @import "rtl";

View File

@@ -52,13 +52,6 @@ table.dataTable thead > tr > th.hw-min {
width: 1%; width: 1%;
white-space: normal; white-space: normal;
} }
@include media-breakpoint-up(sm) {
th.w-sm-min,
td.w-sm-min {
width: 1%;
white-space: nowrap;
}
}
/* If a table column contains ONLY avatar <img> it will collapse, so make it a defined width */ /* If a table column contains ONLY avatar <img> it will collapse, so make it a defined width */
.w-avatar { .w-avatar {
width: 40px; width: 40px;
@@ -93,19 +86,3 @@ table.dataTable thead > tr > th.hw-min {
width: 30%; width: 30%;
} }
} }
.markdown {
blockquote {
p {
/* Parsedown wraps content in a <p> like <blockquote><p> */
margin: 0;
}
/* Tabler doesn't style blockquotes */
border-left: 15px var(--tblr-border-style) var(--tblr-border-color);
background-color: var(--tblr-bg-surface-secondary);
}
pre code.hljs {
padding: 0;
background-color: var(--tblr-bg-surface-dark);
}
}

View File

@@ -56,18 +56,3 @@ fieldset > .mb-3.row:last-child {
.form-fieldset-light:last-child { .form-fieldset-light:last-child {
border:none; border:none;
} }
.dropdown-item {
.dropdown-action {
visibility: hidden;
opacity: 0.5;
}
&:hover {
.dropdown-action {
visibility: visible;
&:hover {
opacity: 1.0;
}
}
}
}

View File

@@ -12,6 +12,14 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 1021; z-index: 1021;
/*background-color: var(--tblr-backdrop-bg);*/ background: rgba(255, 255, 255, 0.7);
opacity: $modal-backdrop-opacity; > .fas {
position: absolute;
top: 50vh;
left: 50vw;
margin-left: -15px;
margin-top: -15px;
color: #000;
font-size: 30px;
}
} }

39
assets/sass/pages.scss Normal file
View File

@@ -0,0 +1,39 @@
/*
* 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.
*/
/*
----------- This file contains rules that only page to certain pages -----------
*/
/* weekly working times AKA quick-entries form */
section.quick-entry-page {
#quick_entry_form {
display: none;
}
/* shrinks the dropdown elements for project and activity to fit into the table, instead of adjusting to the longest option element */
.ts-wrapper {
display: table;
table-layout: fixed;
}
/* make the duration field and dropdown smaller */
#quick_entry_box {
.duration-widget {
.input-group {
min-width: 85px;
}
.btn-duration-preset {
padding: 7px 8px 7px 5px;
}
input {
padding: 7px 3px 7px 7px;
}
}
}
}

View File

@@ -15,11 +15,12 @@
.content-wrapper { .content-wrapper {
margin: 0!important; margin: 0!important;
} }
section.content {
margin: 0;
padding: 0;
}
span i.dot { span i.dot {
display: none; display: none;
margin-right: 0!important; margin-right: 0!important;
} }
.float-help {
display: none;
}
} }

View File

@@ -1,11 +1,7 @@
/* /* bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that */
1. bootstrap thead.sticky-top has z-index 1020 and tomselect dropdowns often hide behind that: use 1021
2. bootstrap modal uses 1055, tomselect with dropdownParent: 'body' (required for use in table.responsive, e.g quick.entry on small devices) needs 1056, so it works in modals
*/
.ts-dropdown { .ts-dropdown {
z-index: 1056; z-index: 1021;
} }
/* Tabler defines a min-width of 7rem, what makes many input fields become a two-line field /* Tabler defines a min-width of 7rem, what makes many input fields become a two-line field

View File

@@ -17,6 +17,16 @@ fieldset.form-fieldset > legend {
--tblr-dropdown-divider-margin-y: .5rem; --tblr-dropdown-divider-margin-y: .5rem;
} }
/* fix translucent scroll border (e.g. timesheet modal duration dropdown) - see https://github.com/tabler/tabler/issues/1606 */
.dropdown-menu {
background-clip: border-box;
}
/* hide empty fieldset - see https://github.com/tabler/tabler/issues/1650 */
fieldset:empty {
display: none;
}
:host,:root,[data-bs-theme=light] { :host,:root,[data-bs-theme=light] {
/* remove the shadow from all form element groups */ /* remove the shadow from all form element groups */
--tblr-box-shadow-input: none; --tblr-box-shadow-input: none;
@@ -32,28 +42,3 @@ fieldset.form-fieldset > legend {
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
} }
/* fixes contrast of batch-update checkboxes - https://github.com/kimai/kimai/issues/5146 */
.multiupdater[type=checkbox] {
--tblr-border-color-translucent: rgba(4, 32, 69, .2);
}
/* fixes height for long tags https://github.com/kimai/kimai/issues/5169 */
.tag {
--tblr-tag-height: unset;
}
/* Page title (e.g. Dashboard) does not work properly in navbar, just outside */
.navbar {
.page-title {
color: var(--tblr-body-color);
}
}
/* Highlighted text is not visible - https://github.com/tabler/tabler/issues/2603 */
[data-bs-theme=dark] {
::selection,
.text-selected {
background-color: var(--#{$prefix}primary);
}
}

View File

@@ -17,18 +17,22 @@ table.dataTable {
position: relative; position: relative;
} }
thead .sorting_asc,
thead .sorting_desc {
font-weight: bold;
}
thead .sorting:after, thead .sorting:after,
thead .sorting_asc:after, thead .sorting_asc:after,
thead .sorting_desc:after { thead .sorting_desc:after {
padding-left: 5px; padding-left: 5px;
font-family: 'Font Awesome\ 6 Free'; font-family: 'Font Awesome 6 Free';
opacity: 0.5; opacity: 0.5;
font-size: 12px; font-size: 12px;
font-weight: 700
} }
thead .sorting:after { thead .sorting:after {
opacity: 0.2; opacity: 0.2;
content: "\f0dc"; content: "\f0dc"; /* sort */
} }
thead .sorting_asc:after { thead .sorting_asc:after {
content: "\f077"; content: "\f077";
@@ -38,7 +42,7 @@ table.dataTable {
} }
thead > tr > th { thead > tr > th {
vertical-align: middle; vertical-align: top;
white-space: nowrap; white-space: nowrap;
} }
@@ -88,13 +92,13 @@ table.dataTable {
&.summary td { &.summary td {
font-weight: bold; font-weight: bold;
border: 0; border: 0;
background-color: var(--tblr-secondary-lt); background-color: var(--tblr-bg-surface-secondary);
} }
th.multiCheckbox { th.multiCheckbox {
width: 15px; width: 15px;
} }
&.overlapping { &.overlapping {
border-top: 2px solid rgba(214,57,57,.2); border-top: 2px solid rgba(214,57,57,.1);
} }
&.exported { &.exported {
opacity: 0.7; opacity: 0.7;
@@ -131,6 +135,14 @@ table.dataTable {
} }
} }
/* Quick entry form */
.form-dataTable {
table.dataTable {
.form-group {
margin-bottom: 0;
}
}
}
table.table-hover { table.table-hover {
tr { tr {

View File

@@ -0,0 +1,42 @@
@include color-mode(dark, true) {
$dark-font-opacity: 0.6;
/*
--tblr-body-bg: #1e222b;
--tblr-bg-surface-tertiary: #1e222b;
--tblr-bg-surface: #1e222b;
--tblr-bg-surface-secondary: #17202b;
//--tblr-muted: #d9dcdf;
*/
.table thead th {
color: inherit;
opacity: $dark-font-opacity;
}
table.dataTable {
thead .sorting:after {
opacity: $dark-font-opacity;
}
}
.overlay {
background: rgba(0, 0, 0, 0.5);
> .fas {
color: var(--tblr-white);
}
}
/* Fix tabs pills (e.g. user profile) */
.nav {
--tblr-nav-link-color: inherit;
opacity: $dark-font-opacity;
}
.datagrid-title {
color: inherit;
opacity: $dark-font-opacity;
}
.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
color: inherit;
}
}

View File

@@ -1,178 +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.
*/
/* This file contains rules for the "weekly-hours" AKA "quick-entries" form */
.quick-entry-page {
.form-dataTable {
table.dataTable {
.form-group {
margin-bottom: 0;
}
}
}
#quick_entry_form {
display: none;
}
.ts-wrapper {
min-height: 36px;
max-height: 36px;
max-width: 150px;
.ts-control {
white-space: nowrap;
.item {
overflow: hidden;
}
}
}
.table tbody td:not(.total) {
padding: 3px;
}
.duration-widget input.duration-input {
min-width: 40px;
}
/* make the duration field and dropdown smaller */
#quick_entry_box {
.duration-widget {
.input-group {
max-width: 50px;
min-width: 50px;
}
.btn-duration-preset {
display: none;
min-width: 30px;
}
input.duration-input {
padding: 7px 5px;
max-width: 50px;
border-bottom-right-radius: var(--tblr-border-radius);
border-top-right-radius: var(--tblr-border-radius);
}
}
}
/* make sure dropdown never hides behind table responsive */
.dropdown-menu {
z-index: 1056;
}
}
@media (min-width: 900px) {
.quick-entry-page {
.ts-wrapper {
max-width: 170px;
}
}
}
@media (min-width: 992px) {
.quick-entry-page {
.ts-wrapper {
max-width: 100px;
}
}
}
@media (min-width: 1100px) {
.quick-entry-page {
.ts-wrapper {
max-width: 160px;
}
}
}
@media (min-width: 1200px) {
.quick-entry-page {
.ts-wrapper {
max-width: 165px;
}
.duration-widget {
max-width: 100%;
}
}
}
@media (min-width: 1250px) {
.quick-entry-page {
#quick_entry_box {
.duration-widget {
.input-group {
max-width: 85px;
min-width: 75px;
}
.btn-duration-preset {
display: flex;
}
input.duration-input {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
}
}
}
}
@media (min-width: 1300px) {
.quick-entry-page {
#quick_entry_box {
.duration-widget {
input.duration-input {
max-width: 100%;
}
}
}
.ts-wrapper {
max-width: 190px;
}
}
}
@media (min-width: 1400px) {
.quick-entry-page {
.ts-wrapper {
max-width: 235px;
}
}
}
@media (min-width: 1500px) {
.quick-entry-page {
.ts-wrapper {
max-width: 290px;
}
}
}
@media (min-width: 1600px) {
.quick-entry-page {
.ts-wrapper {
max-width: 340px;
}
}
}
@media (min-width: 1800px) {
.quick-entry-page {
.ts-wrapper {
max-width: 450px;
}
#quick_entry_box {
.duration-widget {
.input-group {
max-width: 100%;
}
}
}
}
}

17
babel.config.js Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
"sourceType": "unambiguous",
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {},
"useBuiltIns": "usage",
"corejs": 3
}
]
],
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
}

View File

@@ -6,8 +6,8 @@ use App\ConsoleApplication;
set_time_limit(0); set_time_limit(0);
if (!is_dir(dirname(__DIR__).'/vendor')) { if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Dependencies are missing. Try running "composer install".'); throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
} }
require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

View File

@@ -14,7 +14,7 @@
} }
], ],
"require": { "require": {
"php": "8.4.*||8.5.*", "php": "8.1.*||8.2.*||8.3.*",
"ext-gd": "*", "ext-gd": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
@@ -28,55 +28,49 @@
"azuyalabs/yasumi": "^2.6", "azuyalabs/yasumi": "^2.6",
"composer/semver": "^3.3", "composer/semver": "^3.3",
"doctrine/doctrine-bundle": "^2.7", "doctrine/doctrine-bundle": "^2.7",
"doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^3.0", "doctrine/orm": "^2.8",
"easybill/zugferd-php": "^2.1", "endroid/qr-code": "^4.8",
"endroid/qr-code": "^6.0", "erusev/parsedown": "^1.6",
"friendsofsymfony/rest-bundle": "3.9.0-beta1", "friendsofsymfony/rest-bundle": "^3.0",
"gedmo/doctrine-extensions": "^3.6", "gedmo/doctrine-extensions": "^3.6",
"horstoeko/zugferd": "^1.0",
"horstoeko/zugferdublbridge": "^1.0",
"jms/serializer-bundle": "^5.0", "jms/serializer-bundle": "^5.0",
"kevinpapst/tabler-bundle": "^2.0", "kevinpapst/tabler-bundle": "^1.4",
"league/csv": "^9.4", "league/csv": "^9.4",
"mpdf/mpdf": "^8.0", "mpdf/mpdf": "^8.0",
"nelmio/api-doc-bundle": "^5.0", "nelmio/api-doc-bundle": "^4.0",
"nelmio/cors-bundle": "^2.0", "nelmio/cors-bundle": "^2.0",
"onelogin/php-saml": "^4.0", "onelogin/php-saml": "^4.0",
"openspout/openspout": "^4.0", "pagerfanta/pagerfanta": "^3.0",
"pagerfanta/pagerfanta": "^4.0",
"parsedown/parsedown": "^1.6",
"phpoffice/phpspreadsheet": "^2.0", "phpoffice/phpspreadsheet": "^2.0",
"phpoffice/phpword": "^1.0", "phpoffice/phpword": "^1.0",
"psr/container": "^2.0", "psr/container": "^2.0",
"psr/log": "^3.0", "psr/log": "^3.0",
"scheb/2fa-backup-code": "^7.11", "scheb/2fa-backup-code": "^6.2",
"scheb/2fa-bundle": "^7.11", "scheb/2fa-bundle": "^6.2",
"scheb/2fa-totp": "^7.11", "scheb/2fa-totp": "^6.2",
"symfony/asset": "^7.0", "symfony/asset": "^6.0",
"symfony/console": "^7.0", "symfony/console": "^6.0",
"symfony/dependency-injection": "^7.0", "symfony/dotenv": "^6.0",
"symfony/dotenv": "^7.0", "symfony/expression-language": "^6.0",
"symfony/expression-language": "^7.0",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "^7.0", "symfony/form": "^6.0",
"symfony/framework-bundle": "^7.0", "symfony/framework-bundle": "^6.0",
"symfony/http-client": "^7.0", "symfony/http-client": "^6.0",
"symfony/intl": "^7.0", "symfony/intl": "^6.0",
"symfony/mailer": "^7.0", "symfony/mailer": "^6.0",
"symfony/mime": "^7.0", "symfony/mime": "^6.0",
"symfony/monolog-bundle": "^3.4", "symfony/monolog-bundle": "^3.4",
"symfony/process": "^7.0", "symfony/rate-limiter": "^6.0",
"symfony/rate-limiter": "^7.0", "symfony/runtime": "^6.0",
"symfony/runtime": "^7.0", "symfony/security-bundle": "^6.0",
"symfony/security-bundle": "^7.0", "symfony/security-csrf": "^6.0",
"symfony/security-csrf": "^7.0", "symfony/serializer": "^6.0",
"symfony/serializer": "^7.0", "symfony/translation": "^6.0",
"symfony/translation": "^7.0", "symfony/twig-bundle": "^6.0",
"symfony/twig-bundle": "^7.0", "symfony/validator": "^6.0",
"symfony/validator": "^7.0",
"symfony/webpack-encore-bundle": "^2.0", "symfony/webpack-encore-bundle": "^2.0",
"symfony/yaml": "^7.0", "symfony/yaml": "^6.0",
"twig/cssinliner-extra": "^3.0", "twig/cssinliner-extra": "^3.0",
"twig/extra-bundle": "^3.0", "twig/extra-bundle": "^3.0",
"twig/inky-extra": "^3.0", "twig/inky-extra": "^3.0",
@@ -89,21 +83,21 @@
"doctrine/doctrine-fixtures-bundle": "^3.2", "doctrine/doctrine-fixtures-bundle": "^3.2",
"fakerphp/faker": "^1.15", "fakerphp/faker": "^1.15",
"friendsofphp/php-cs-fixer": "^3.3", "friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^2.0", "phpstan/phpstan": "^1.0",
"phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-doctrine": "^2.0", "phpstan/phpstan-doctrine": "^1.0",
"phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^2.0", "phpstan/phpstan-strict-rules": "^1.0",
"phpstan/phpstan-symfony": "^2.0", "phpstan/phpstan-symfony": "^1.0",
"phpunit/phpunit": "^10.0", "phpunit/phpunit": "9.5.*",
"symfony/browser-kit": "^7.0", "symfony/browser-kit": "^6.0",
"symfony/css-selector": "^7.0", "symfony/css-selector": "^6.0",
"symfony/debug-bundle": "^7.0", "symfony/debug-bundle": "^6.0",
"symfony/dom-crawler": "^7.0", "symfony/dom-crawler": "^6.0",
"symfony/phpunit-bridge": "^7.0", "symfony/phpunit-bridge": "^6.0",
"symfony/stopwatch": "^7.0", "symfony/stopwatch": "^6.0",
"symfony/var-dumper": "^7.0", "symfony/var-dumper": "^6.0",
"symfony/web-profiler-bundle": "^7.0" "symfony/web-profiler-bundle": "^6.0"
}, },
"repositories": [ "repositories": [
{ {
@@ -118,7 +112,7 @@
}, },
"optimize-autoloader": true, "optimize-autoloader": true,
"platform": { "platform": {
"php": "8.4.4" "php": "8.1.3"
}, },
"preferred-install": { "preferred-install": {
"*": "dist" "*": "dist"
@@ -140,14 +134,7 @@
"symfony/polyfill-ctype": "*", "symfony/polyfill-ctype": "*",
"symfony/polyfill-mbstring": "*", "symfony/polyfill-mbstring": "*",
"symfony/polyfill-intl": "*", "symfony/polyfill-intl": "*",
"symfony/polyfill-intl-icu": "*",
"symfony/polyfill-intl-grapheme": "*",
"symfony/polyfill-intl-idn": "*",
"symfony/polyfill-intl-normalizer": "*",
"symfony/polyfill-iconv": "*", "symfony/polyfill-iconv": "*",
"symfony/polyfill-php84": "*",
"symfony/polyfill-php83": "*",
"symfony/polyfill-php82": "*",
"symfony/polyfill-php81": "*", "symfony/polyfill-php81": "*",
"symfony/polyfill-php80": "*", "symfony/polyfill-php80": "*",
"symfony/polyfill-php74": "*", "symfony/polyfill-php74": "*",
@@ -213,7 +200,7 @@
"symfony": { "symfony": {
"id": "01C3FWRDJJEX9K6Y3A4XDFXPBR", "id": "01C3FWRDJJEX9K6Y3A4XDFXPBR",
"allow-contrib": true, "allow-contrib": true,
"require": "7.4.*" "require": "6.4.*"
} }
} }
} }

6628
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ return [
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
FOS\RestBundle\FOSRestBundle::class => ['all' => true], FOS\RestBundle\FOSRestBundle::class => ['all' => true],

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,17 @@
framework:
cache:
#app: cache.adapter.redis
#default_redis_provider: redis://127.0.0.1:6379
#app: cache.adapter.memcached
#default_memcached_provider: 'memcached://localhost'
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@@ -1,5 +0,0 @@
when@test:
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true

View File

@@ -1,5 +0,0 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

View File

@@ -10,31 +10,29 @@ doctrine:
default_connection: default default_connection: default
connections: connections:
default: default:
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
# existing migrations will fail if the schema filter is activated # existing migrations will fail if the schema filter is activated
#schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~ #schema_filter: ~^(?!(bundle_migration_|kimai2_sessions))~
url: '%env(resolve:DATABASE_URL)%' url: '%env(DATABASE_URL)%'
driver: 'pdo_mysql' driver: 'pdo_mysql'
charset: utf8mb4 charset: utf8mb4
default_table_options: default_table_options:
charset: utf8mb4 charset: utf8mb4
collation: utf8mb4_unicode_ci collation: utf8mb4_unicode_ci
schema_manager_factory: doctrine.dbal.default_schema_manager_factory schema_manager_factory: doctrine.dbal.default_schema_manager_factory
types: types:
datetime: App\Doctrine\UTCDateTimeType datetime: App\Doctrine\UTCDateTimeType
datetime_immutable: App\Doctrine\UTCDateTimeImmutableType datetime_immutable: App\Doctrine\UTCDateTimeImmutableType
orm: orm:
controller_resolver: controller_resolver:
auto_mapping: false # FIXME this is causing a deprecation and needs to be changed
auto_generate_proxy_classes: true # auto_mapping: false
auto_mapping: true
auto_generate_proxy_classes: '%kernel.debug%'
default_entity_manager: default default_entity_manager: default
enable_native_lazy_objects: true enable_lazy_ghost_objects: true
entity_managers: entity_managers:
default: default:
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
validate_xml_mapping: true
report_fields_where_declared: true report_fields_where_declared: true
connection: default connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
@@ -58,23 +56,17 @@ when@test:
connections: connections:
default: default:
logging: false logging: false
use_savepoints: true
when@prod: when@prod:
doctrine: doctrine:
orm: orm:
auto_generate_proxy_classes: false metadata_cache_driver:
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' type: pool
pool: doctrine.system_cache_pool
query_cache_driver: query_cache_driver:
type: pool type: pool
pool: doctrine.system_cache_pool pool: doctrine.system_cache_pool
result_cache_driver: result_cache_driver:
type: pool type: pool
pool: doctrine.result_cache_pool pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@@ -1,11 +1,8 @@
doctrine_migrations: doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false
transactional: false transactional: false
storage: storage:
table_storage: table_storage:
table_name: 'migration_versions' table_name: 'migration_versions'
migrations_paths:
'DoctrineMigrations': '%kernel.project_dir%/migrations'
custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt' custom_template: '%kernel.project_dir%/migrations/MigrationTemplate.txt'

View File

@@ -1,4 +1,3 @@
# Read the documentation: https://fosrestbundle.readthedocs.io/en/3.x/
fos_rest: fos_rest:
param_fetcher_listener: param_fetcher_listener:
enabled: true enabled: true
@@ -40,5 +39,3 @@ fos_rest:
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] } - { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] }
zone: zone:
- { path: ^/api/* } - { path: ^/api/* }
service:
view_handler: App\API\ViewHandler

View File

@@ -1,7 +1,6 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html # see https://symfony.com/doc/current/reference/configuration/framework.html
parameters: parameters:
env(TRUSTED_PROXIES): '' env(TRUSTED_PROXIES): ''
env(TRUSTED_HOSTS): ''
framework: framework:
secret: '%env(APP_SECRET)%' secret: '%env(APP_SECRET)%'
@@ -15,7 +14,6 @@ framework:
enable_attributes: true enable_attributes: true
trusted_proxies: '%env(string:TRUSTED_PROXIES)%' trusted_proxies: '%env(string:TRUSTED_PROXIES)%'
trusted_hosts: '%env(string:TRUSTED_HOSTS)%'
exceptions: exceptions:
App\Validator\ValidationFailedException: App\Validator\ValidationFailedException:
@@ -27,7 +25,6 @@ framework:
# Enables session support. Note that the session will ONLY be started if you read or write from it. # Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support. # Remove or comment this section to explicitly disable session support.
session: session:
name: KIMAI_SESSION
handler_id: App\Security\SessionHandler handler_id: App\Security\SessionHandler
cookie_secure: auto cookie_secure: auto
cookie_samesite: lax cookie_samesite: lax
@@ -37,9 +34,6 @@ framework:
php_errors: php_errors:
log: true log: true
property_info:
with_constructor_extractor: true
mailer: mailer:
dsn: '%env(MAILER_URL)%' dsn: '%env(MAILER_URL)%'

View File

@@ -3,6 +3,10 @@ jms_serializer:
datetime: datetime:
default_format: 'Y-m-d\TH:i:sO' # DATE_ISO8601 default_format: 'Y-m-d\TH:i:sO' # DATE_ISO8601
visitors: visitors:
json_serialization:
options:
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION
xml_serialization: xml_serialization:
format_output: '%kernel.debug%' format_output: '%kernel.debug%'
metadata: metadata:
@@ -14,20 +18,3 @@ jms_serializer:
excluded: [] excluded: []
property_naming: property_naming:
id: 'jms_serializer.identical_property_naming_strategy' id: 'jms_serializer.identical_property_naming_strategy'
when@prod:
jms_serializer:
visitors:
json_serialization:
options:
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION
when@dev:
jms_serializer:
visitors:
json_serialization:
options:
- JSON_PRETTY_PRINT
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION

View File

@@ -119,8 +119,8 @@ kimai:
roles: roles:
ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting'] ROLE_USER: ['view_team_member','time_team_project','create_tag','view_reporting']
ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile'] ROLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member','hours_other_profile']
ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile','create_export_template'] ROLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data','contract_other_profile','hours_other_profile']
ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile','create_export_template'] ROLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','supervisor_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data','contract_other_profile','hours_other_profile']
# -------------------------------------------------------------------------------- # --------------------------------------------------------------------------------

View File

@@ -1,50 +1,5 @@
monolog: monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: fingers_crossed
action_level: notice
handler: file_log
excluded_http_codes:
- {400: ['^/api/']}
- {401: ['^/api/']}
- 403
- 404
- 405
channels: ["!event", "!deprecation"]
file_log:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
formatter: monolog.formatter.kimai
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!deprecation"]
deprecation:
type: stream
channels: ["deprecation"] channels: ["deprecation"]
path: "%kernel.logs_dir%/deprecations.log"
formatter: monolog.formatter.deprecation
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
formatter: monolog.formatter.kimai
when@prod: when@prod:
monolog: monolog:
@@ -69,3 +24,42 @@ when@prod:
type: console type: console
process_psr_3_messages: false process_psr_3_messages: false
channels: ["!event", "!doctrine", "!deprecation"] channels: ["!event", "!doctrine", "!deprecation"]
when@dev:
monolog:
handlers:
main:
type: fingers_crossed
action_level: notice
handler: file_log
excluded_http_codes:
- {400: ['^/api/']}
- {401: ['^/api/']}
- 403
- 404
- {405: ['/homepage$', '/login_check$', '/export/data$']}
channels: ["!event", "!deprecation"]
file_log:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
formatter: monolog.formatter.kimai
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!deprecation"]
deprecation:
type: stream
channels: ["deprecation"]
path: "%kernel.logs_dir%/deprecations.log"
formatter: monolog.formatter.deprecation
when@test:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: info
channels: ["!event"]
formatter: monolog.formatter.kimai

View File

@@ -1,22 +1,21 @@
nelmio_api_doc: nelmio_api_doc:
operation_id_generation: conditionally_prepend
models: models:
use_jms: true use_jms: true
names: names:
- { alias: CustomerEditForm, type: App\Form\API\CustomerApiEditForm, groups: [Default, Entity, Customer, Customer_Entity] } - { alias: CustomerEditForm, type: App\Form\API\CustomerApiEditForm, groups: [Default, Entity, Customer] }
- { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer, Customer_Entity, Not_Expanded] } - { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer, Customer_Entity, Not_Expanded] }
- { alias: Customer, type: App\Entity\Customer, groups: [Default, Not_Expanded] } - { alias: Customer, type: App\Entity\Customer, groups: [Default, Not_Expanded] }
- { alias: CustomerRate, type: App\Entity\CustomerRate, groups: [Default, Entity, Customer_Rate] } - { alias: CustomerRate, type: App\Entity\CustomerRate, groups: [Default, Entity, Customer_Rate] }
- { alias: CustomerRateForm, type: App\Form\API\CustomerRateApiForm, groups: [Default, Entity, Customer_Rate] } - { alias: CustomerRateForm, type: App\Form\API\CustomerRateApiForm, groups: [Default, Entity, Customer_Rate] }
- { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] } - { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] }
- { alias: ProjectEditForm, type: App\Form\API\ProjectApiEditForm, groups: [Default, Entity, Project, Project_Entity] } - { alias: ProjectEditForm, type: App\Form\API\ProjectApiEditForm, groups: [Default, Entity, Project] }
- { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project, Project_Entity, Not_Expanded] } - { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project, Project_Entity, Not_Expanded] }
- { alias: Project, type: App\Entity\Project, groups: [Default, Not_Expanded] } - { alias: Project, type: App\Entity\Project, groups: [Default, Not_Expanded] }
- { alias: ProjectExpanded, type: App\Entity\Project, groups: [Default, Expanded] } - { alias: ProjectExpanded, type: App\Entity\Project, groups: [Default, Expanded] }
- { alias: ProjectRate, type: App\Entity\ProjectRate, groups: [Default, Entity, Project_Rate] } - { alias: ProjectRate, type: App\Entity\ProjectRate, groups: [Default, Entity, Project_Rate] }
- { alias: ProjectRateForm, type: App\Form\API\ProjectRateApiForm, groups: [Default, Entity, Project_Rate] } - { alias: ProjectRateForm, type: App\Form\API\ProjectRateApiForm, groups: [Default, Entity, Project_Rate] }
- { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] } - { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] }
- { alias: ActivityEditForm, type: App\Form\API\ActivityApiEditForm, groups: [Default, Entity, Activity, Activity_Entity] } - { alias: ActivityEditForm, type: App\Form\API\ActivityApiEditForm, groups: [Default, Entity, Activity] }
- { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity, Activity_Entity, Not_Expanded] } - { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity, Activity_Entity, Not_Expanded] }
- { alias: Activity, type: App\Entity\Activity, groups: [Default, Not_Expanded] } - { alias: Activity, type: App\Entity\Activity, groups: [Default, Not_Expanded] }
- { alias: ActivityExpanded, type: App\Entity\Activity, groups: [Default, Expanded] } - { alias: ActivityExpanded, type: App\Entity\Activity, groups: [Default, Expanded] }
@@ -29,7 +28,7 @@ nelmio_api_doc:
- { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Not_Expanded] } - { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Not_Expanded] }
- { alias: TimesheetExpanded, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Expanded] } - { alias: TimesheetExpanded, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet, Timesheet_Entity, Expanded] }
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Not_Expanded] } - { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Not_Expanded] }
- { alias: TimesheetCollectionExpanded, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Expanded] } - { alias: TimesheetCollectionExpanded, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet, Subresource, Expanded] }
- { alias: UserCreateForm, type: App\Form\API\UserApiCreateForm, groups: [Default, Entity, User, User_Entity] } - { 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: UserEditForm, type: App\Form\API\UserApiEditForm, groups: [Default, Entity, User, User_Entity] }
- { alias: User, type: App\Entity\User, groups: [Default] } - { alias: User, type: App\Entity\User, groups: [Default] }
@@ -44,28 +43,20 @@ nelmio_api_doc:
- { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] } - { alias: Invoice, type: App\Entity\Invoice, groups: [Default, Entity, Invoice, Invoice_Entity] }
- { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] } - { alias: InvoiceCollection, type: App\Entity\Invoice, groups: [Default, Collection, Invoice] }
areas: areas:
default:
path_patterns: path_patterns:
- ^/api(?!/doc) - ^/api(?!/doc)
security:
bearer:
type: 'http'
description: 'API Token'
bearerFormat: 'KIMAI'
scheme: 'bearer'
documentation: documentation:
info: info:
title: Kimai - API title: Kimai - API
description: | description: |
JSON API for the Kimai time-tracking software. Find more infos in our [API documentation](https://www.kimai.org/documentation/rest-api.html). JSON API for the Kimai time-tracking software. Read our [API documentation](https://www.kimai.org/documentation/rest-api.html) and download the [Open API definition](doc.json) to import into your API client.
version: '1.1' version: '1.0'
components:
securitySchemes:
bearer:
type: http
scheme: bearer
bearerFormat: KIMAI
description: API Token
security: security:
- bearer: [] - bearer: []
html_config:
# assets_mode: cdn
# https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
swagger_ui_config: []
# https://redocly.com/docs/redoc/config/
redocly_config: []
# https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options
stoplight_config: { basePath: '/api/doc', router: 'memory', logo: '/touch-icon-192x192.png', hideInternal: true }

View File

@@ -17,7 +17,7 @@ nelmio_cors:
'^/api/': '^/api/':
# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] # allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_origin: ['*'] allow_origin: ['*']
allow_headers: ['Content-Type', 'Authorization'] allow_headers: ['Content-Type', 'Authorization', 'X-AUTH-USER', 'X-AUTH-TOKEN']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
# expose_headers: ['Link'] # expose_headers: ['Link']
max_age: 3600 max_age: 3600

View File

@@ -1,6 +1,3 @@
parameters:
env(default_uri): 'http://localhost'
framework: framework:
router: router:
utf8: true utf8: true
@@ -8,7 +5,7 @@ framework:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands. # Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(DEFAULT_URI)%' #default_uri: http://localhost
when@test: when@test:
framework: framework:

View File

@@ -1,8 +1,7 @@
security: security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers: password_hashers:
App\Entity\User: auto App\Entity\User: auto
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers: providers:
chain_provider: chain_provider:
chain: chain:
@@ -12,11 +11,12 @@ security:
class: App\Entity\User class: App\Entity\User
kimai_ldap: kimai_ldap:
id: App\Ldap\LdapUserProvider id: App\Ldap\LdapUserProvider
firewalls: firewalls:
dev: dev:
# Ensure dev tools and static assets are always allowed
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
api: api:
access_token: access_token:
token_handler: App\API\Authentication\AccessTokenHandler token_handler: App\API\Authentication\AccessTokenHandler
@@ -27,52 +27,65 @@ security:
stateless: true stateless: true
remember_me: false remember_me: false
provider: chain_provider provider: chain_provider
custom_authenticators:
- App\API\Authentication\TokenAuthenticator
secured_area: secured_area:
kimai_ldap: ~ kimai_ldap: ~
pattern: ^/ pattern: ^/
user_checker: App\Security\UserChecker user_checker: App\Security\UserChecker
stateless: false stateless: false
entry_point: form_login entry_point: form_login
custom_authenticators: custom_authenticators:
- App\Saml\SamlAuthenticator - App\Saml\SamlAuthenticator
remember_me: remember_me:
name: KIMAI_REMEMBER
secret: '%kernel.secret%' secret: '%kernel.secret%'
lifetime: 604800 lifetime: 604800
path: / path: /
always_remember_me: true always_remember_me: true
# activate all configured user provider # activate all configured user provider
provider: chain_provider provider: chain_provider
form_login: form_login:
check_path: security_check check_path: security_check
login_path: login login_path: login
enable_csrf: true enable_csrf: true
two_factor: two_factor:
auth_form_path: 2fa_login auth_form_path: 2fa_login
check_path: 2fa_login_check check_path: 2fa_login_check
remember_me_sets_trusted: true remember_me_sets_trusted: true
logout: logout:
path: logout path: logout
target: homepage target: homepage
enable_csrf: false enable_csrf: false
login_throttling: login_throttling:
max_attempts: 5 max_attempts: 5
interval: '5 minutes' interval: '5 minutes'
login_link: login_link:
check_route: link_login_check check_route: link_login_check
signature_properties: ['id'] signature_properties: ['id']
lifetime: 900 lifetime: 900
max_uses: 3 max_uses: 3
access_decision_manager: access_decision_manager:
# only grants access if there is no voter denying access # only grants access if there is no voter denying access
strategy: unanimous strategy: unanimous
allow_if_all_abstain: false allow_if_all_abstain: false
role_hierarchy: role_hierarchy:
ROLE_USER: ~ ROLE_USER: ~
ROLE_TEAMLEAD: ROLE_USER ROLE_TEAMLEAD: ROLE_USER
ROLE_ADMIN: ROLE_TEAMLEAD ROLE_ADMIN: ROLE_TEAMLEAD
ROLE_SUPER_ADMIN: ROLE_ADMIN ROLE_SUPER_ADMIN: ROLE_ADMIN
# Note: Only the *first* matching rule is applied
access_control: access_control:
- { path: '^/auth/2fa', role: IS_AUTHENTICATED_2FA_IN_PROGRESS } - { path: '^/auth/2fa', role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
- { path: '^/auth', roles: PUBLIC_ACCESS } - { path: '^/auth', roles: PUBLIC_ACCESS }
@@ -85,12 +98,13 @@ security:
- { path: '^/api', roles: IS_AUTHENTICATED } - { path: '^/api', roles: IS_AUTHENTICATED }
when@test: when@test:
# this configuration simplifies testing URLs protected by the security mechanism
# See https://symfony.com/doc/current/cookbook/testing/http_authentication.html
security: security:
password_hashers: password_hashers:
# Password hashers are resource-intensive by design to ensure security.
# In tests, it's safe to reduce their cost to improve performance.
App\Entity\User: App\Entity\User:
algorithm: auto algorithm: auto
# see https://github.com/symfony/recipes/pull/1026
cost: 4 # Lowest possible value for bcrypt cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon memory_cost: 10 # Lowest possible value for argon

View File

@@ -6,18 +6,20 @@
# #
tabler: tabler:
options: options:
theme_auto: true # dark or normal layout
dark_mode: false dark_mode: false
# condensed = one nav-bar, false = two nav-bars
navbar_condensed: true navbar_condensed: true
navbar_overlap: false # overlap = https://preview.tabler.io/layout-navbar-overlap.html
navbar_dark: true navbar_overlap: true
# boxed layout (true) or full-screen (false)
boxed_layout: false boxed_layout: false
# right to left = true
rtl_mode: false rtl_mode: false
user_menu_condensed: false # true = only avatar, false = show username and title as well
user_menu_condensed: true
# url to your company logo
logo_url: "touch-icon-192x192.png" logo_url: "touch-icon-192x192.png"
theme_base: "neutral"
theme_radius: 0.5
theme_primary: "blue"
knp_menu: knp_menu:
enable: false enable: false
@@ -45,7 +47,6 @@ tabler:
barcode: fas fa-barcode barcode: fas fa-barcode
bookmark: far fa-star bookmark: far fa-star
bookmarked: fas fa-star bookmarked: fas fa-star
break: fas fa-utensils
calendar: far fa-calendar-alt calendar: far fa-calendar-alt
cancel: fas fa-times cancel: fas fa-times
clock: far fa-clock clock: far fa-clock
@@ -67,10 +68,10 @@ tabler:
documentation: fas fa-book documentation: fas fa-book
dot: fas fa-circle dot: fas fa-circle
download: fas fa-download download: fas fa-download
duration: fas fa-stopwatch duration: far fa-hourglass
edit: far fa-edit edit: far fa-edit
end: fas fa-hourglass-end end: fas fa-stopwatch
export: fas fa-download export: fas fa-file-export
failure: fas fa-times failure: fas fa-times
fax: fas fa-fax fax: fas fa-fax
filter: fas fa-filter filter: fas fa-filter

View File

@@ -1,13 +1,11 @@
twig: twig:
file_name_pattern: '*.twig' debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
form_themes: form_themes:
- 'form/vertical.html.twig' - 'form/vertical.html.twig'
exception_controller: null
paths: paths:
# used in templates/emails/layout.html.twig
'%kernel.project_dir%/assets/css': css
'%kernel.project_dir%/templates/bundles/TablerBundle': theme '%kernel.project_dir%/templates/bundles/TablerBundle': theme
'%kernel.project_dir%/vendor/kevinpapst/tabler-bundle/templates': theme '%kernel.project_dir%/vendor/kevinpapst/tabler-bundle/templates': theme
globals:
app: '@App\Twig\AppVariable'
when@test:
twig:
strict_variables: true

View File

@@ -1,13 +1,15 @@
when@dev: when@dev:
web_profiler: web_profiler:
toolbar: true toolbar: true
intercept_redirects: false
framework: framework:
profiler: profiler: { only_exceptions: false }
collect_serializer_data: true
when@test: when@test:
web_profiler:
toolbar: false
intercept_redirects: false
framework: framework:
profiler: profiler: { collect: false }
collect: false
collect_serializer_data: true

View File

@@ -1,45 +1,17 @@
webpack_encore: webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath() # The path where Encore is building the assets.
# This should match Encore.setOutputPath() in webpack.config.js.
output_path: '%kernel.project_dir%/public/build' output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build: # If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false # output_path: false
# Set attributes that will be rendered on all script and link tags # if using Encore.enableIntegrityHashes() specify the crossorigin attribute value (default: false, or use 'anonymous' or 'use-credentials')
script_attributes:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous' # crossorigin: 'anonymous'
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# If you have multiple builds:
# builds:
# frontend: '%kernel.project_dir%/public/frontend/build'
# pass the build name as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
when@prod:
webpack_encore:
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Available in version 1.2 # Available in version 1.2
cache: true cache: '%kernel.debug%'
#when@test: when@dev:
# webpack_encore: webpack_encore:
# strict_mode: false cache: false

View File

@@ -1,14 +1,4 @@
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
# This file is the entry point to configure the routes of your app.
# Methods with the #[Route] attribute are automatically imported.
# See also https://symfony.com/doc/current/routing.html
# To list all registered routes, run the following command:
# bin/console debug:router
controllers: controllers:
# resource: routing.controllers
resource: ../src/Controller/ resource: ../src/Controller/
type: attribute type: attribute
prefix: /{_locale} prefix: /{_locale}
@@ -18,8 +8,12 @@ controllers:
api.swagger_ui: api.swagger_ui:
path: /api/doc path: /api/doc
methods: GET methods: GET
defaults: defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
_controller: nelmio_api_doc.controller.stoplight
api.swagger:
path: /api/doc.json
methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger }
api: api:
resource: ../src/API/ resource: ../src/API/

View File

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

View File

@@ -0,0 +1,7 @@
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

View File

@@ -1,6 +0,0 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /{_locale}/_error
defaults:
_locale: '%locale%'

View File

@@ -1,3 +0,0 @@
_security_logout:
resource: security.route_loader.logout
type: service

View File

@@ -1,8 +0,0 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler

View File

@@ -1,22 +1,16 @@
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters: parameters:
locale: en locale: en
# can be regenerated with: bin/console kimai:reset:locales # can be regenerated with: bin/console kimai:reset:locales
kimai_locales: ['ar', 'bg', 'cs', 'da', 'de', 'de_CH', 'el', 'en', 'eo', 'es', 'eu', 'fa', 'fi', 'fo', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'nb_NO', 'nl', 'pa', 'pl', 'pt', 'pt_BR', 'ro', 'ru', 'sk', 'sl', 'sv', 'ta', 'tr', 'uk', 'vi', 'zh_CN', 'zh_Hant', 'zh_Hant_TW', 'cs_CZ', 'da_DK', 'da_GL', 'de_AT', 'de_BE', 'de_CH', 'de_DE', 'de_IT', 'de_LI', 'de_LU', 'el_CY', 'el_GR', 'en_AE', 'en_AG', 'en_AI', 'en_AS', 'en_AT', 'en_AU', 'en_BB', 'en_BE', 'en_BI', 'en_BM', 'en_BS', 'en_BW', 'en_BZ', 'en_CA', 'en_CC', 'en_CH', 'en_CK', 'en_CM', 'en_CX', 'en_CY', 'en_CZ', 'en_DE', 'en_DG', 'en_DK', 'en_DM', 'en_ER', 'en_ES', 'en_FI', 'en_FJ', 'en_FK', 'en_FM', 'en_FR', 'en_GB', 'en_GD', 'en_GG', 'en_GH', 'en_GI', 'en_GM', 'en_GS', 'en_GU', 'en_GY', 'en_HK', 'en_HU', 'en_ID', 'en_IE', 'en_IL', 'en_IM', 'en_IN', 'en_IO', 'en_IT', 'en_JE', 'en_JM', 'en_KE', 'en_KI', 'en_KN', 'en_KY', 'en_LC', 'en_LR', 'en_LS', 'en_MG', 'en_MH', 'en_MO', 'en_MP', 'en_MS', 'en_MT', 'en_MU', 'en_MV', 'en_MW', 'en_MY', 'en_NA', 'en_NF', 'en_NG', 'en_NH', 'en_NL', 'en_NO', 'en_NR', 'en_NU', 'en_NZ', 'en_PG', 'en_PH', 'en_PK', 'en_PL', 'en_PN', 'en_PR', 'en_PT', 'en_PW', 'en_RH', 'en_RO', 'en_RW', 'en_SB', 'en_SC', 'en_SD', 'en_SE', 'en_SG', 'en_SH', 'en_SI', 'en_SK', 'en_SL', 'en_SS', 'en_SX', 'en_SZ', 'en_TC', 'en_TK', 'en_TO', 'en_TT', 'en_TV', 'en_TZ', 'en_UG', 'en_UM', 'en_US', 'en_VC', 'en_VG', 'en_VI', 'en_VU', 'en_WS', 'en_ZA', 'en_ZM', 'en_ZW', 'es_AR', 'es_BO', 'es_BR', 'es_BZ', 'es_CL', 'es_CO', 'es_CR', 'es_CU', 'es_DO', 'es_EA', 'es_EC', 'es_ES', 'es_GQ', 'es_GT', 'es_HN', 'es_IC', 'es_MX', 'es_NI', 'es_PA', 'es_PE', 'es_PH', 'es_PR', 'es_PY', 'es_SV', 'es_US', 'es_UY', 'es_VE', 'eu_ES', 'fa_AF', 'fa_IR', 'fi_FI', 'fo_DK', 'fo_FO', 'fr_BE', 'fr_BF', 'fr_BI', 'fr_BJ', 'fr_BL', 'fr_CA', 'fr_CD', 'fr_CF', 'fr_CG', 'fr_CH', 'fr_CI', 'fr_CM', 'fr_DJ', 'fr_DZ', 'fr_FR', 'fr_GA', 'fr_GF', 'fr_GN', 'fr_GP', 'fr_GQ', 'fr_HT', 'fr_KM', 'fr_LU', 'fr_MA', 'fr_MC', 'fr_MF', 'fr_MG', 'fr_ML', 'fr_MQ', 'fr_MR', 'fr_MU', 'fr_NC', 'fr_NE', 'fr_PF', 'fr_PM', 'fr_RE', 'fr_RW', 'fr_SC', 'fr_SN', 'fr_SY', 'fr_TD', 'fr_TG', 'fr_TN', 'fr_VU', 'fr_WF', 'fr_YT', 'he_IL', 'hr_BA', 'hr_HR', 'hu_HU', 'it_CH', 'it_IT', 'it_SM', 'it_VA', 'ja_JP', 'ko_CN', 'ko_KP', 'ko_KR', 'nl_AW', 'nl_BE', 'nl_BQ', 'nl_CW', 'nl_NL', 'nl_SR', 'nl_SX', 'pl_PL', 'pt_AO', 'pt_BR', 'pt_CH', 'pt_CV', 'pt_GQ', 'pt_GW', 'pt_LU', 'pt_MO', 'pt_MZ', 'pt_PT', 'pt_ST', 'pt_TL', 'ro_MD', 'ro_RO', 'ru_BY', 'ru_KG', 'ru_KZ', 'ru_MD', 'ru_RU', 'ru_UA', 'sk_SK', 'sv_AX', 'sv_FI', 'sv_SE', 'tr_CY', 'tr_TR', 'uk_UA', 'vi_VN'] kimai_locales: ['ar', 'ca', 'cs', 'da', 'de', 'de_CH', 'el', 'en', 'eo', 'es', 'eu', 'fa', 'fi', 'fo', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'nb_NO', 'nl', 'pa', 'pl', 'pt', 'pt_BR', 'ro', 'ru', 'sk', 'sl', 'sv', 'tr', 'uk', 'vi', 'zh_CN', 'zh_Hant', 'zh_Hant_TW', 'ca_AD', 'ca_ES', 'ca_FR', 'ca_IT', 'cs_CZ', 'da_DK', 'da_GL', 'de_AT', 'de_BE', 'de_CH', 'de_DE', 'de_IT', 'de_LI', 'de_LU', 'el_CY', 'el_GR', 'en_AE', 'en_AG', 'en_AI', 'en_AS', 'en_AT', 'en_AU', 'en_BB', 'en_BE', 'en_BI', 'en_BM', 'en_BS', 'en_BW', 'en_BZ', 'en_CA', 'en_CC', 'en_CH', 'en_CK', 'en_CM', 'en_CX', 'en_CY', 'en_DE', 'en_DG', 'en_DK', 'en_DM', 'en_ER', 'en_FI', 'en_FJ', 'en_FK', 'en_FM', 'en_GB', 'en_GD', 'en_GG', 'en_GH', 'en_GI', 'en_GM', 'en_GU', 'en_GY', 'en_HK', 'en_ID', 'en_IE', 'en_IL', 'en_IM', 'en_IN', 'en_IO', 'en_JE', 'en_JM', 'en_KE', 'en_KI', 'en_KN', 'en_KY', 'en_LC', 'en_LR', 'en_LS', 'en_MG', 'en_MH', 'en_MO', 'en_MP', 'en_MS', 'en_MT', 'en_MU', 'en_MV', 'en_MW', 'en_MY', 'en_NA', 'en_NF', 'en_NG', 'en_NH', 'en_NL', 'en_NR', 'en_NU', 'en_NZ', 'en_PG', 'en_PH', 'en_PK', 'en_PN', 'en_PR', 'en_PW', 'en_RH', 'en_RW', 'en_SB', 'en_SC', 'en_SD', 'en_SE', 'en_SG', 'en_SH', 'en_SI', 'en_SL', 'en_SS', 'en_SX', 'en_SZ', 'en_TC', 'en_TK', 'en_TO', 'en_TT', 'en_TV', 'en_TZ', 'en_UG', 'en_UM', 'en_US', 'en_VC', 'en_VG', 'en_VI', 'en_VU', 'en_WS', 'en_ZA', 'en_ZM', 'en_ZW', 'es_AR', 'es_BO', 'es_BR', 'es_BZ', 'es_CL', 'es_CO', 'es_CR', 'es_CU', 'es_DO', 'es_EA', 'es_EC', 'es_ES', 'es_GQ', 'es_GT', 'es_HN', 'es_IC', 'es_MX', 'es_NI', 'es_PA', 'es_PE', 'es_PH', 'es_PR', 'es_PY', 'es_SV', 'es_US', 'es_UY', 'es_VE', 'eu_ES', 'fa_AF', 'fa_IR', 'fi_FI', 'fo_DK', 'fo_FO', 'fr_BE', 'fr_BF', 'fr_BI', 'fr_BJ', 'fr_BL', 'fr_CA', 'fr_CD', 'fr_CF', 'fr_CG', 'fr_CH', 'fr_CI', 'fr_CM', 'fr_DJ', 'fr_DZ', 'fr_FR', 'fr_GA', 'fr_GF', 'fr_GN', 'fr_GP', 'fr_GQ', 'fr_HT', 'fr_KM', 'fr_LU', 'fr_MA', 'fr_MC', 'fr_MF', 'fr_MG', 'fr_ML', 'fr_MQ', 'fr_MR', 'fr_MU', 'fr_NC', 'fr_NE', 'fr_PF', 'fr_PM', 'fr_RE', 'fr_RW', 'fr_SC', 'fr_SN', 'fr_SY', 'fr_TD', 'fr_TG', 'fr_TN', 'fr_VU', 'fr_WF', 'fr_YT', 'he_IL', 'hr_BA', 'hr_HR', 'hu_HU', 'it_CH', 'it_IT', 'it_SM', 'it_VA', 'ja_JP', 'ko_CN', 'ko_KP', 'ko_KR', 'nl_AW', 'nl_BE', 'nl_BQ', 'nl_CW', 'nl_NL', 'nl_SR', 'nl_SX', 'pl_PL', 'pt_AO', 'pt_BR', 'pt_CH', 'pt_CV', 'pt_GQ', 'pt_GW', 'pt_LU', 'pt_MO', 'pt_MZ', 'pt_PT', 'pt_ST', 'pt_TL', 'ro_MD', 'ro_RO', 'ru_BY', 'ru_KG', 'ru_KZ', 'ru_MD', 'ru_RU', 'ru_UA', 'sk_SK', 'sv_AX', 'sv_FI', 'sv_SE', 'tr_CY', 'tr_TR', 'uk_UA', 'vi_VN']
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
_defaults: _defaults:
autowire: true # Automatically injects dependencies in your services. autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
bind: bind:
$projectDirectory: '%kernel.project_dir%' $projectDirectory: '%kernel.project_dir%'
$kernelEnvironment: '%kernel.environment%' $kernelEnvironment: '%kernel.environment%'
@@ -24,14 +18,13 @@ services:
# makes classes in src/ available to be used as services # makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name # this creates a service per class whose id is the fully-qualified class name
App\: App\:
resource: '../src/' resource: '../src/*'
exclude: exclude:
- '../src/API/Model/' - '../src/API/Model/'
- '../src/DependencyInjection/' - '../src/DependencyInjection/'
- '../src/Doctrine/Extensions/' - '../src/Doctrine/Extensions/'
- '../src/Entity/'
- '../src/Event/' - '../src/Event/'
- '../src/Export/Package/'
- '../src/Export/Base/'
- '../src/Form/Model/' - '../src/Form/Model/'
- '../src/Model/' - '../src/Model/'
- '../src/Repository/Loader/' - '../src/Repository/Loader/'
@@ -42,9 +35,6 @@ services:
- '../src/Kernel.php' - '../src/Kernel.php'
- '../src/Constants.php' - '../src/Constants.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
# controllers are imported separately to make sure services can be injected # controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class # as action arguments even if you don't extend any base controller class
App\Controller\: App\Controller\:
@@ -96,9 +86,6 @@ services:
App\Timesheet\RateService: App\Timesheet\RateService:
arguments: ['%kimai.timesheet.rates%'] arguments: ['%kimai.timesheet.rates%']
App\Timesheet\RateCalculator\RateCalculatorMode:
factory: ['@App\Timesheet\RateCalculator\RateCalculatorFactory', getRateCalculatorMode]
# ================================================================================ # ================================================================================
# SECURITY & VOTER # SECURITY & VOTER
# ================================================================================ # ================================================================================
@@ -218,11 +205,6 @@ services:
factory: ['@doctrine.orm.entity_manager', getRepository] factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\AccessToken'] arguments: ['App\Entity\AccessToken']
App\Repository\ExportTemplateRepository:
class: App\Repository\ExportTemplateRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\ExportTemplate']
monolog.formatter.kimai: monolog.formatter.kimai:
class: Monolog\Formatter\LineFormatter class: Monolog\Formatter\LineFormatter
arguments: arguments:
@@ -234,16 +216,3 @@ services:
arguments: arguments:
- "[%%datetime%%] %%message%% %%context%%\n" - "[%%datetime%%] %%message%% %%context%%\n"
- "Y-m-d H:i:s" - "Y-m-d H:i:s"
App\API\ViewHandler:
arguments: ['@fos_rest.view_handler.default']
App\Validator\Constraints\TimesheetAllValidator:
arguments: ['%kimai.validator_timesheet%']
App\Validator\Constraints\QuickEntryTimesheetValidator:
arguments: ['%kimai.validator_timesheet%']
App\Twig\AppVariable:
tags:
- {name: twig.global, alias: app}

25
config/services_test.yaml Normal file
View File

@@ -0,0 +1,25 @@
parameters:
locale: en
kimai_locales: ['ar', 'cs', 'de', 'de_CH', 'da', 'en', 'eo', 'es', 'eu', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pl', 'pt_BR', 'ro', 'ru', 'sk', 'sv', 'tr', 'vi', 'zh_CN']
services:
_defaults:
public: true
App\Configuration\SystemConfiguration:
arguments: ['@App\Configuration\ConfigurationService', "%kimai.config%"]
# required for the importer command test
App\Repository\UserRepository:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: ['App\Entity\User']
# required for the importer command test
App\Importer\ImporterService:
public: true
arguments: ['@App\Customer\CustomerService', '@App\Project\ProjectService']
App\User\UserService:
public: true
arguments: ['@App\Repository\UserRepository', '@event_dispatcher', '@validator', '@App\Configuration\SystemConfiguration', '@security.user_password_hasher']

View File

@@ -1,22 +0,0 @@
import js from "@eslint/js";
import globals from "globals";
export default [
js.configs.recommended,
{
files: ["assets/js/**/*.js"],
ignores: ["assets/*.js"],
rules: {
eqeqeq: 1,
curly: "error",
semi: ["warn", "always"],
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...globals.amd,
}
}
},
];

117
kimai.sh
View File

@@ -1,117 +0,0 @@
#!/bin/bash
# --------------------------------------------------------------------------------
# This script was added with 2.24.0 and is in BETA status.
#
# To improve this script across platforms I need your feedback!
# --------------------------------------------------------------------------------
function update_kimai() {
if [[ "$1" =~ ^([0-9]+\.){2,3}[0-9]+$ ]]; then
export VERSION=$1
else
echo "You need to supply a full Kimai version like: \"2.24.0\""
exit 1
fi
git checkout -- composer.json
git checkout -- composer.lock
git checkout -- symfony.lock
if [[ -n $(git status --porcelain) ]]; then
echo "Cannot update: file changes detected. Run \"git status\" for details."
exit 1
fi
rm -rf var/sessions/ 2>&1
rm -rf var/cache/* 2>&1
git fetch --tags
git checkout "$VERSION"
$KIMAI_PHP "$KIMAI_COMPOSER" install --no-dev --optimize-autoloader
$KIMAI_PHP bin/console kimai:install
install_plugins
if [[ -z "${KIMAI_NO_PERMS}" ]]; then
set_permission
fi
}
function install_plugins() {
# detect if there are additional plugins that we need to install
packages="$($PHP bin/console kimai:plugin --composer)"
export PACKAGES=$packages
if [ -n "$PACKAGES" ]; then
$KIMAI_PHP "$KIMAI_COMPOSER" require "$PACKAGES"
$KIMAI_PHP bin/console kimai:plugins --install
fi
}
function set_permission() {
chown -R "$KIMAI_USER":"$KIMAI_GROUP" .
chmod -R g+r .
chmod -R g+rw var/
}
if [[ -z "${KIMAI_USER}" ]]; then
export KIMAI_USER=""
fi
if [[ -z "${KIMAI_GROUP}" ]]; then
export KIMAI_GROUP="www-data"
fi
if [[ -z "${KIMAI_PHP}" ]]; then
export KIMAI_PHP="php"
fi
if [[ -z "${KIMAI_COMPOSER}" ]]; then
export KIMAI_COMPOSER="composer"
fi
cd "$(dirname "$0")" || { echo "Cannot change working directory."; exit 1; }
# we need a few commands installed in order for this script to complete
command -v $KIMAI_COMPOSER >/dev/null 2>&1 || { echo >&2 "Update requires 'composer' but it's not installed."; exit 1; }
command -v git >/dev/null 2>&1 || { echo >&2 "Update requires 'git' but it's not installed."; exit 1; }
command -v $KIMAI_PHP >/dev/null 2>&1 || { echo >&2 "Update requires 'php' but it's not installed."; exit 1; }
if [[ -n $1 ]]; then
if [ "$1" == 'update' ]; then
update_kimai "$2"
exit
elif [ "$1" == 'permission' ]; then
set_permission
exit
elif [ "$1" == 'plugins' ]; then
install_plugins
exit
else
echo ""
echo ">> Unknown command: $1"
fi
fi
echo ""
echo "This script has the following sub-commands:"
echo ""
echo "$0 update <version> - Install Kimai version <version>"
echo "$0 permission - Fix file permissions"
echo "$0 plugins - Install plugins from var/packages/*.zip"
echo ""
echo "Use the following environment variables to customize the runtime:"
echo ""
echo "KIMAI_USER - Username of the webserver/php process that needs write access"
echo "KIMAI_GROUP - Group of the webserver/php process that needs write access"
echo "KIMAI_PHP - Full path to PHP executable in the correct version"
echo "KIMAI_COMPOSER - Path to composer executable or .phar file"
echo "KIMAI_NO_PERMS - Skip changing permissions"
echo ""
echo "Examples:"
echo ""
echo "$0 2.24.0"
echo "KIMAI_PHP=/usr/bin/php8.3 $0 2.24.0"
echo "KIMAI_PHP=/usr/bin/php8.3 KIMAI_COMPOSER=/tmp/composer.phar $0 2.24.0"
echo "KIMAI_PHP=php8.3 KIMAI_GROUP=httpd $0 2.24.0"
echo "KIMAI_NO_PERMS=1 KIMAI_PHP=/usr/bin/php8.3 $0 2.24.0"
echo ""

View File

@@ -23,9 +23,9 @@ final class Version20190219200020 extends AbstractMigration
{ {
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
$this->addSql("DELETE FROM kimai2_user_preferences WHERE name = 'theme.fixed_layout'"); $this->addSql('DELETE FROM kimai2_user_preferences WHERE name = "theme.fixed_layout"');
$this->addSql("DELETE FROM kimai2_user_preferences WHERE name = 'theme.boxed_layout'"); $this->addSql('DELETE FROM kimai2_user_preferences WHERE name = "theme.boxed_layout"');
$this->addSql("DELETE FROM kimai2_user_preferences WHERE name = 'theme.mini_sidebar'"); $this->addSql('DELETE FROM kimai2_user_preferences WHERE name = "theme.mini_sidebar"');
} }
public function down(Schema $schema): void public function down(Schema $schema): void

View File

@@ -28,13 +28,13 @@ class Version20190510205245 extends AbstractMigration
$timesheetTags->addColumn('tag_id', 'integer', ['length' => 11, 'notnull' => true]); $timesheetTags->addColumn('tag_id', 'integer', ['length' => 11, 'notnull' => true]);
$timesheetTags->addIndex(['timesheet_id'], 'IDX_E3284EFEABDD46BE'); $timesheetTags->addIndex(['timesheet_id'], 'IDX_E3284EFEABDD46BE');
$timesheetTags->addIndex(['tag_id'], 'IDX_E3284EFEBAD26311'); $timesheetTags->addIndex(['tag_id'], 'IDX_E3284EFEBAD26311');
$this->addPrimaryKeyConstraint($timesheetTags, ['timesheet_id', 'tag_id']); $timesheetTags->setPrimaryKey(['timesheet_id', 'tag_id']);
$tags = $schema->createTable('kimai2_tags'); $tags = $schema->createTable('kimai2_tags');
$tags->addColumn('id', 'integer', ['length' => 11, 'autoincrement' => true, 'notnull' => true]); $tags->addColumn('id', 'integer', ['length' => 11, 'autoincrement' => true, 'notnull' => true]);
$tags->addColumn('name', 'string', ['length' => 100, 'notnull' => true]); $tags->addColumn('name', 'string', ['length' => 100, 'notnull' => true]);
$tags->addUniqueIndex(['name'], 'UNIQ_27CAF54C5E237E06'); $tags->addUniqueIndex(['name'], 'UNIQ_27CAF54C5E237E06');
$this->addPrimaryKeyConstraint($tags, ['id']); $tags->setPrimaryKey(['id']);
} }
public function down(Schema $schema): void public function down(Schema $schema): void

View File

@@ -34,7 +34,7 @@ final class Version20190617100845 extends AbstractMigration
$timesheetMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]); $timesheetMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$timesheetMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]); $timesheetMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
$timesheetMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]); $timesheetMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
$this->addPrimaryKeyConstraint($timesheetMeta, ['id']); $timesheetMeta->setPrimaryKey(['id']);
$timesheetMeta->addIndex(['timesheet_id'], 'IDX_CB606CBAABDD46BE'); $timesheetMeta->addIndex(['timesheet_id'], 'IDX_CB606CBAABDD46BE');
$timesheetMeta->addUniqueIndex(['timesheet_id', 'name'], 'UNIQ_CB606CBAABDD46BE5E237E06'); $timesheetMeta->addUniqueIndex(['timesheet_id', 'name'], 'UNIQ_CB606CBAABDD46BE5E237E06');
$timesheetMeta->addForeignKeyConstraint('kimai2_timesheet', ['timesheet_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_CB606CBAABDD46BE'); $timesheetMeta->addForeignKeyConstraint('kimai2_timesheet', ['timesheet_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_CB606CBAABDD46BE');
@@ -45,7 +45,7 @@ final class Version20190617100845 extends AbstractMigration
$customerMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]); $customerMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$customerMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]); $customerMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
$customerMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]); $customerMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
$this->addPrimaryKeyConstraint($customerMeta, ['id']); $customerMeta->setPrimaryKey(['id']);
$customerMeta->addIndex(['customer_id'], 'IDX_A48A760F9395C3F3'); $customerMeta->addIndex(['customer_id'], 'IDX_A48A760F9395C3F3');
$customerMeta->addUniqueIndex(['customer_id', 'name'], 'UNIQ_A48A760F9395C3F35E237E06'); $customerMeta->addUniqueIndex(['customer_id', 'name'], 'UNIQ_A48A760F9395C3F35E237E06');
$customerMeta->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A48A760F9395C3F3'); $customerMeta->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A48A760F9395C3F3');
@@ -56,7 +56,7 @@ final class Version20190617100845 extends AbstractMigration
$projectMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]); $projectMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$projectMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]); $projectMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
$projectMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]); $projectMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
$this->addPrimaryKeyConstraint($projectMeta, ['id']); $projectMeta->setPrimaryKey(['id']);
$projectMeta->addIndex(['project_id'], 'IDX_50536EF2166D1F9C'); $projectMeta->addIndex(['project_id'], 'IDX_50536EF2166D1F9C');
$projectMeta->addUniqueIndex(['project_id', 'name'], 'UNIQ_50536EF2166D1F9C5E237E06'); $projectMeta->addUniqueIndex(['project_id', 'name'], 'UNIQ_50536EF2166D1F9C5E237E06');
$projectMeta->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50536EF2166D1F9C'); $projectMeta->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50536EF2166D1F9C');
@@ -67,7 +67,7 @@ final class Version20190617100845 extends AbstractMigration
$activityMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]); $activityMeta->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
$activityMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]); $activityMeta->addColumn('value', 'string', ['notnull' => false, 'length' => 255]);
$activityMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]); $activityMeta->addColumn('visible', 'boolean', ['notnull' => false, 'default' => false]);
$this->addPrimaryKeyConstraint($activityMeta, ['id']); $activityMeta->setPrimaryKey(['id']);
$activityMeta->addIndex(['activity_id'], 'IDX_A7C0A43D81C06096'); $activityMeta->addIndex(['activity_id'], 'IDX_A7C0A43D81C06096');
$activityMeta->addUniqueIndex(['activity_id', 'name'], 'UNIQ_A7C0A43D81C060965E237E06'); $activityMeta->addUniqueIndex(['activity_id', 'name'], 'UNIQ_A7C0A43D81C060965E237E06');
$activityMeta->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A7C0A43D81C06096'); $activityMeta->addForeignKeyConstraint('kimai2_activities', ['activity_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_A7C0A43D81C06096');

View File

@@ -32,7 +32,7 @@ final class Version20190730123324 extends AbstractMigration
$teams->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]); $teams->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
$teams->addColumn('name', 'string', ['notnull' => true, 'length' => 100]); $teams->addColumn('name', 'string', ['notnull' => true, 'length' => 100]);
$teams->addColumn('teamlead_id', 'integer', ['length' => 11, 'notnull' => true]); $teams->addColumn('teamlead_id', 'integer', ['length' => 11, 'notnull' => true]);
$this->addPrimaryKeyConstraint($teams, ['id']); $teams->setPrimaryKey(['id']);
$teams->addUniqueIndex(['name'], 'UNIQ_3BEDDC7F5E237E06'); $teams->addUniqueIndex(['name'], 'UNIQ_3BEDDC7F5E237E06');
$teams->addForeignKeyConstraint('kimai2_users', ['teamlead_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_3BEDDC7F8F7DE5D7'); $teams->addForeignKeyConstraint('kimai2_users', ['teamlead_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_3BEDDC7F8F7DE5D7');
@@ -41,21 +41,21 @@ final class Version20190730123324 extends AbstractMigration
$userTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]); $userTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
$userTeams->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8A76ED395'); $userTeams->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8A76ED395');
$userTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8296CD8AE'); $userTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8296CD8AE');
$this->addPrimaryKeyConstraint($userTeams, ['user_id', 'team_id']); $userTeams->setPrimaryKey(['user_id', 'team_id']);
$customerTeams = $schema->createTable('kimai2_customers_teams'); $customerTeams = $schema->createTable('kimai2_customers_teams');
$customerTeams->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => true]); $customerTeams->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => true]);
$customerTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]); $customerTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
$customerTeams->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD83889395C3F3'); $customerTeams->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD83889395C3F3');
$customerTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD8388296CD8AE'); $customerTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD8388296CD8AE');
$this->addPrimaryKeyConstraint($customerTeams, ['customer_id', 'team_id']); $customerTeams->setPrimaryKey(['customer_id', 'team_id']);
$projectTeams = $schema->createTable('kimai2_projects_teams'); $projectTeams = $schema->createTable('kimai2_projects_teams');
$projectTeams->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => true]); $projectTeams->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => true]);
$projectTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]); $projectTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
$projectTeams->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431166D1F9C'); $projectTeams->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431166D1F9C');
$projectTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431296CD8AE'); $projectTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431296CD8AE');
$this->addPrimaryKeyConstraint($projectTeams, ['project_id', 'team_id']); $projectTeams->setPrimaryKey(['project_id', 'team_id']);
} }
public function down(Schema $schema): void public function down(Schema $schema): void

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