Compare commits
5 Commits
webhooks
...
api-pagina
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233c0e792e | ||
|
|
896e8b42c1 | ||
|
|
bf288bf115 | ||
|
|
0f9b964b11 | ||
|
|
3e3cdc5e69 |
@@ -8,6 +8,7 @@
|
|||||||
PassEnv DATABASE_URL
|
PassEnv DATABASE_URL
|
||||||
PassEnv MAILER_URL
|
PassEnv MAILER_URL
|
||||||
PassEnv TRUSTED_PROXIES
|
PassEnv TRUSTED_PROXIES
|
||||||
|
PassEnv TRUSTED_HOSTS
|
||||||
|
|
||||||
<Directory "/opt/kimai/public">
|
<Directory "/opt/kimai/public">
|
||||||
Require all granted
|
Require all granted
|
||||||
|
|||||||
208
.docker/Dockerfile.kimai-base
Normal file
208
.docker/Dockerfile.kimai-base
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# Source base [fpm/apache]
|
||||||
|
ARG BASE="fpm"
|
||||||
|
ARG PHP_VER="8.2"
|
||||||
|
ARG COMPOSER_VER="latest"
|
||||||
|
ARG TIMEZONE="Europe/London"
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# Shared tools
|
||||||
|
###########################
|
||||||
|
|
||||||
|
# composer base image
|
||||||
|
FROM composer:${COMPOSER_VER} AS composer
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# PHP extensions
|
||||||
|
###########################
|
||||||
|
|
||||||
|
#fpm alpine php extension base
|
||||||
|
FROM php:${PHP_VER}-fpm-alpine AS fpm-php-ext-base
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
# build-tools
|
||||||
|
autoconf \
|
||||||
|
dpkg \
|
||||||
|
dpkg-dev \
|
||||||
|
file \
|
||||||
|
g++ \
|
||||||
|
gcc \
|
||||||
|
icu-dev \
|
||||||
|
libatomic \
|
||||||
|
libc-dev \
|
||||||
|
libgomp \
|
||||||
|
libmagic \
|
||||||
|
m4 \
|
||||||
|
make \
|
||||||
|
mpc1 \
|
||||||
|
mpfr4 \
|
||||||
|
musl-dev \
|
||||||
|
perl \
|
||||||
|
re2c \
|
||||||
|
# gd
|
||||||
|
freetype-dev \
|
||||||
|
libpng-dev \
|
||||||
|
# icu
|
||||||
|
icu-dev \
|
||||||
|
icu-data-full \
|
||||||
|
# ldap
|
||||||
|
openldap-dev \
|
||||||
|
libldap \
|
||||||
|
# zip
|
||||||
|
libzip-dev \
|
||||||
|
# xsl
|
||||||
|
libxslt-dev
|
||||||
|
|
||||||
|
|
||||||
|
# apache debian php extension base
|
||||||
|
FROM php:${PHP_VER}-apache-bookworm AS apache-php-ext-base
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y \
|
||||||
|
libldap2-dev \
|
||||||
|
libicu-dev \
|
||||||
|
libpng-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
libfreetype6-dev
|
||||||
|
|
||||||
|
|
||||||
|
# php extension gd - 13.86s
|
||||||
|
FROM ${BASE}-php-ext-base AS php-ext-gd
|
||||||
|
RUN docker-php-ext-configure gd \
|
||||||
|
--with-freetype && \
|
||||||
|
docker-php-ext-install -j$(nproc) gd
|
||||||
|
|
||||||
|
# php extension intl : 15.26s
|
||||||
|
FROM ${BASE}-php-ext-base AS php-ext-intl
|
||||||
|
RUN docker-php-ext-install -j$(nproc) intl
|
||||||
|
|
||||||
|
# php extension ldap : 8.45s
|
||||||
|
FROM ${BASE}-php-ext-base AS php-ext-ldap
|
||||||
|
RUN docker-php-ext-configure ldap && \
|
||||||
|
docker-php-ext-install -j$(nproc) ldap
|
||||||
|
|
||||||
|
# php extension pdo_mysql : 6.14s
|
||||||
|
FROM ${BASE}-php-ext-base AS php-ext-pdo_mysql
|
||||||
|
RUN docker-php-ext-install -j$(nproc) pdo_mysql
|
||||||
|
|
||||||
|
# php extension zip : 8.18s
|
||||||
|
FROM ${BASE}-php-ext-base AS php-ext-zip
|
||||||
|
RUN docker-php-ext-install -j$(nproc) zip
|
||||||
|
|
||||||
|
# php extension xsl : ?.?? s
|
||||||
|
FROM ${BASE}-php-ext-base AS php-ext-xsl
|
||||||
|
RUN docker-php-ext-install -j$(nproc) xsl
|
||||||
|
|
||||||
|
# php extension redis
|
||||||
|
FROM ${BASE}-php-ext-base AS php-ext-redis
|
||||||
|
RUN yes no | pecl install redis && \
|
||||||
|
docker-php-ext-enable redis
|
||||||
|
|
||||||
|
# php extension opcache
|
||||||
|
FROM ${BASE}-php-ext-base AS php-ext-opcache
|
||||||
|
RUN docker-php-ext-install -j$(nproc) opcache
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# fpm base build
|
||||||
|
###########################
|
||||||
|
|
||||||
|
# fpm base build
|
||||||
|
FROM php:${PHP_VER}-fpm-alpine AS fpm-base
|
||||||
|
ARG TIMEZONE
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
coreutils \
|
||||||
|
freetype \
|
||||||
|
haveged \
|
||||||
|
icu \
|
||||||
|
icu-data-full \
|
||||||
|
libldap \
|
||||||
|
libpng \
|
||||||
|
libzip \
|
||||||
|
libxslt-dev \
|
||||||
|
fcgi \
|
||||||
|
tzdata && \
|
||||||
|
touch /use_fpm
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||||
|
CMD \
|
||||||
|
SCRIPT_NAME=/ping \
|
||||||
|
SCRIPT_FILENAME=/ping \
|
||||||
|
REQUEST_METHOD=GET \
|
||||||
|
cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# apache base build
|
||||||
|
###########################
|
||||||
|
|
||||||
|
FROM php:${PHP_VER}-apache-bookworm AS apache-base
|
||||||
|
ARG TIMEZONE
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y \
|
||||||
|
bash \
|
||||||
|
haveged \
|
||||||
|
libicu72 \
|
||||||
|
libldap-common \
|
||||||
|
libpng16-16 \
|
||||||
|
libzip4 \
|
||||||
|
libxslt1.1 \
|
||||||
|
libfreetype6 && \
|
||||||
|
echo "Listen 8001" > /etc/apache2/ports.conf && \
|
||||||
|
a2enmod rewrite && \
|
||||||
|
touch /use_apache
|
||||||
|
|
||||||
|
EXPOSE 8001
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
||||||
|
CMD curl -f http://127.0.0.1:8001 || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# global base build
|
||||||
|
###########################
|
||||||
|
|
||||||
|
FROM ${BASE}-base AS base
|
||||||
|
ARG TIMEZONE
|
||||||
|
|
||||||
|
ENV TIMEZONE=${TIMEZONE}
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone && \
|
||||||
|
# make composer home dir
|
||||||
|
mkdir /composer && \
|
||||||
|
chown -R www-data:www-data /composer && \
|
||||||
|
echo "echo -e This imnage is not intended to be used stand alone. It's a base image\necho for the Kimai project. https://www.kimai.org\n" > /entrypoint.sh && \
|
||||||
|
chmod 777 /entrypoint.sh
|
||||||
|
# copy composer
|
||||||
|
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# copy php extensions
|
||||||
|
|
||||||
|
# PHP extension xsl
|
||||||
|
COPY --from=php-ext-xsl /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini
|
||||||
|
COPY --from=php-ext-xsl /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xsl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xsl.so
|
||||||
|
# PHP extension pdo_mysql
|
||||||
|
COPY --from=php-ext-pdo_mysql /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini
|
||||||
|
COPY --from=php-ext-pdo_mysql /usr/local/lib/php/extensions/no-debug-non-zts-20220829/pdo_mysql.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/pdo_mysql.so
|
||||||
|
# PHP extension zip
|
||||||
|
COPY --from=php-ext-zip /usr/local/etc/php/conf.d/docker-php-ext-zip.ini /usr/local/etc/php/conf.d/docker-php-ext-zip.ini
|
||||||
|
COPY --from=php-ext-zip /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so
|
||||||
|
# PHP extension ldap
|
||||||
|
COPY --from=php-ext-ldap /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini
|
||||||
|
COPY --from=php-ext-ldap /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ldap.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ldap.so
|
||||||
|
# PHP extension gd
|
||||||
|
COPY --from=php-ext-gd /usr/local/etc/php/conf.d/docker-php-ext-gd.ini /usr/local/etc/php/conf.d/docker-php-ext-gd.ini
|
||||||
|
COPY --from=php-ext-gd /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so
|
||||||
|
# PHP extension intl
|
||||||
|
COPY --from=php-ext-intl /usr/local/etc/php/conf.d/docker-php-ext-intl.ini /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
|
||||||
|
COPY --from=php-ext-intl /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so
|
||||||
|
# PHP extension redis
|
||||||
|
COPY --from=php-ext-redis /usr/local/etc/php/conf.d/docker-php-ext-redis.ini /usr/local/etc/php/conf.d/docker-php-ext-redis.ini
|
||||||
|
COPY --from=php-ext-redis /usr/local/lib/php/extensions/no-debug-non-zts-20220829/redis.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/redis.so
|
||||||
|
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
|
||||||
|
|
||||||
|
CMD [ "/entrypoint.sh" ]
|
||||||
|
|
||||||
|
# docker build -t kimai/kimai-base:fpm --build-arg BASE=fpm .
|
||||||
|
# docker build -t kimai/kimai-base:apache --build-arg BASE=apache .
|
||||||
@@ -1,31 +1,35 @@
|
|||||||
<?php
|
<?php
|
||||||
$DATABASE_HOST = urldecode($argv[1]);
|
$DB_HOST = urldecode($argv[1]);
|
||||||
$DATABASE_BASE = urldecode($argv[2]);
|
$DB_BASE = urldecode($argv[2]);
|
||||||
$DATABASE_PORT = $argv[3];
|
$DB_PORT = $argv[3];
|
||||||
$DATABASE_USER = urldecode($argv[4]);
|
$DB_USER = urldecode($argv[4]);
|
||||||
$DATABASE_PASS = urldecode($argv[5]);
|
$DB_PASS = urldecode($argv[5]);
|
||||||
|
|
||||||
echo "Testing DB:";
|
echo "Testing DB:";
|
||||||
|
echo "*";
|
||||||
|
echo "* new \PDO(mysql:host=$DB_HOST;dbname=$DB_BASE;port=$DB_PORT, $DB_USER, $DB_PASS, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION ]);";
|
||||||
|
echo "*";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = new \PDO("mysql:host=$DATABASE_HOST;dbname=$DATABASE_BASE;port=$DATABASE_PORT", "$DATABASE_USER", "$DATABASE_PASS", [
|
$pdo = new \PDO("mysql:host=$DB_HOST;dbname=$DB_BASE;port=$DB_PORT", "$DB_USER", "$DB_PASS", [
|
||||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
|
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
|
||||||
]);
|
]);
|
||||||
} 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
|
||||||
|
|||||||
59
.docker/self-test.sh
Executable file
59
.docker/self-test.sh
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
/startup.sh 2>&1
|
||||||
|
echo $$ > /tmp/startup.pid
|
||||||
|
echo "Waiting for kimai to install"
|
||||||
|
while [ ! -f /opt/kimai/var/installed ]; do
|
||||||
|
echo -n ". "
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Test FPM CGI
|
||||||
|
if [ -f /use_fpm ]; then
|
||||||
|
|
||||||
|
export SCRIPT_NAME=/opt/kimai/public/index.php
|
||||||
|
export SCRIPT_FILENAME=/opt/kimai/public/index.php
|
||||||
|
export REQUEST_METHOD=GET
|
||||||
|
export SERVER_ADDR=localhost
|
||||||
|
|
||||||
|
COUNT=0
|
||||||
|
until cgi-fcgi -bind -connect 127.0.0.1:9000 &> /dev/null
|
||||||
|
do
|
||||||
|
COUNT=$((COUNT+1))
|
||||||
|
echo "Waiting for FPM Server to start (${COUNT})"
|
||||||
|
sleep 3
|
||||||
|
if [ "$COUNT" -gt 5 ]; then
|
||||||
|
echo "FPM Failed to start."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test Apache/httpd
|
||||||
|
if [ -f /use_apache ]; then
|
||||||
|
|
||||||
|
COUNT=0
|
||||||
|
until curl -s -o /dev/null http://localhost:8001
|
||||||
|
do
|
||||||
|
COUNT=$((COUNT+1))
|
||||||
|
echo "Waiting for Apache/HTTP to start (${COUNT})" &> /dev/null
|
||||||
|
sleep 3
|
||||||
|
if [ "$COUNT" -gt 5 ]; then
|
||||||
|
echo "Apache/HTTP failed to start."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test PHP/Kimai
|
||||||
|
/opt/kimai/bin/console kimai:version
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
echo "PHP/Kimai not responding"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
kill $(cat /tmp/startup.pid)
|
||||||
|
|
||||||
|
|
||||||
61
.docker/service.sh
Executable file
61
.docker/service.sh
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash -x
|
||||||
|
|
||||||
|
function waitForDB() {
|
||||||
|
# Parse sql connection data
|
||||||
|
if [ ! -z "$DATABASE_URL" ]; then
|
||||||
|
DB_TYPE=$(awk -F '[/:@]' '{print $1}' <<< "$DATABASE_URL")
|
||||||
|
DB_USER=$(awk -F '[/:@]' '{print $4}' <<< "$DATABASE_URL")
|
||||||
|
DB_PASS=$(awk -F '[/:@]' '{print $5}' <<< "$DATABASE_URL")
|
||||||
|
DB_HOST=$(awk -F '[/:@]' '{print $6}' <<< "$DATABASE_URL")
|
||||||
|
DB_PORT=$(awk -F '[/:@]' '{print $7}' <<< "$DATABASE_URL")
|
||||||
|
DB_BASE=$(awk -F '[/?]' '{print $4}' <<< "$DATABASE_URL")
|
||||||
|
else
|
||||||
|
DB_TYPE=${DB_TYPE:mysql}
|
||||||
|
if [ "$DB_TYPE" == "mysql" ]; then
|
||||||
|
export DATABASE_URL="${DB_TYPE}://${DB_USER:=kimai}:${DB_PASS:=kimai}@${DB_HOST:=sqldb}:${DB_PORT:=3306}/${DB_BASE:=kimai}"
|
||||||
|
else
|
||||||
|
echo "Unknown database type, cannot proceed. Only 'mysql' is supported, received: [$DB_TYPE]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
re='^[0-9]+$'
|
||||||
|
if ! [[ $DB_PORT =~ $re ]] ; then
|
||||||
|
DB_PORT=3306
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Wait for MySQL DB connection ..."
|
||||||
|
until php /dbtest.php $DB_HOST $DB_BASE $DB_PORT $DB_USER $DB_PASS; do
|
||||||
|
echo Checking DB: $?
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
echo "Connection established"
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStartup() {
|
||||||
|
# These are idempotent, run them anyway
|
||||||
|
/opt/kimai/bin/console -n kimai:install
|
||||||
|
/opt/kimai/bin/console -n kimai:update
|
||||||
|
if [ ! -z "$ADMINPASS" ] && [ ! -a "$ADMINMAIL" ]; then
|
||||||
|
/opt/kimai/bin/console kimai:user:create superadmin "$ADMINMAIL" ROLE_SUPER_ADMIN "$ADMINPASS"
|
||||||
|
fi
|
||||||
|
echo "$KIMAI" > /opt/kimai/var/installed
|
||||||
|
echo "Kimai2 ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
function runServer() {
|
||||||
|
# Just while I'm fixing things
|
||||||
|
/opt/kimai/bin/console kimai:reload --env="$APP_ENV"
|
||||||
|
chown -R $USER_ID:$GROUP_ID /opt/kimai/var
|
||||||
|
if [ -e /use_apache ]; then
|
||||||
|
exec /usr/sbin/apache2 -D FOREGROUND
|
||||||
|
elif [ -e /use_fpm ]; then
|
||||||
|
exec php-fpm
|
||||||
|
else
|
||||||
|
echo "Error, unknown server type"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForDB
|
||||||
|
handleStartup
|
||||||
|
runServer
|
||||||
@@ -3,21 +3,39 @@
|
|||||||
KIMAI=$(cat /opt/kimai/version.txt)
|
KIMAI=$(cat /opt/kimai/version.txt)
|
||||||
echo $KIMAI
|
echo $KIMAI
|
||||||
|
|
||||||
function waitForDB() {
|
|
||||||
# Parse sql connection data
|
|
||||||
DATABASE_USER=$(awk -F '[/:@]' '{print $4}' <<< "$DATABASE_URL")
|
|
||||||
DATABASE_PASS=$(awk -F '[/:@]' '{print $5}' <<< "$DATABASE_URL")
|
|
||||||
DATABASE_HOST=$(awk -F '[/:@]' '{print $6}' <<< "$DATABASE_URL")
|
|
||||||
DATABASE_PORT=$(awk -F '[/:@]' '{print $7}' <<< "$DATABASE_URL")
|
|
||||||
DATABASE_BASE=$(awk -F '[/?]' '{print $4}' <<< "$DATABASE_URL")
|
|
||||||
|
|
||||||
re='^[0-9]+$'
|
function config() {
|
||||||
if ! [[ $DATABASE_PORT =~ $re ]] ; then
|
# set mem limits and copy in custom logger config
|
||||||
DATABASE_PORT=3306
|
if [ -z "$memory_limit" ]; then
|
||||||
|
memory_limit=256M
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Wait for database connection ..."
|
|
||||||
until php /dbtest.php "$DATABASE_HOST" "$DATABASE_BASE" "$DATABASE_PORT" "$DATABASE_USER" "$DATABASE_PASS"; do
|
# Parse sql connection data
|
||||||
|
if [ ! -z "$DATABASE_URL" ]; then
|
||||||
|
DB_TYPE=$(awk -F '[/:@]' '{print $1}' <<< "$DATABASE_URL")
|
||||||
|
DB_USER=$(awk -F '[/:@]' '{print $4}' <<< "$DATABASE_URL")
|
||||||
|
DB_PASS=$(awk -F '[/:@]' '{print $5}' <<< "$DATABASE_URL")
|
||||||
|
DB_HOST=$(awk -F '[/:@]' '{print $6}' <<< "$DATABASE_URL")
|
||||||
|
DB_PORT=$(awk -F '[/:@]' '{print $7}' <<< "$DATABASE_URL")
|
||||||
|
DB_BASE=$(awk -F '[/?]' '{print $4}' <<< "$DATABASE_URL")
|
||||||
|
else
|
||||||
|
DB_TYPE=${DB_TYPE:mysql}
|
||||||
|
if [ "$DB_TYPE" == "mysql" ]; then
|
||||||
|
export DATABASE_URL="${DB_TYPE}://${DB_USER:=kimai}:${DB_PASS:=kimai}@${DB_HOST:=sqldb}:${DB_PORT:=3306}/${DB_BASE:=kimai}"
|
||||||
|
else
|
||||||
|
echo "Unknown database type, cannot proceed. Only 'mysql' is supported, received: [$DB_TYPE]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
re='^[0-9]+$'
|
||||||
|
if ! [[ $DB_PORT =~ $re ]] ; then
|
||||||
|
DB_PORT=3306
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Wait for MySQL DB connection ..."
|
||||||
|
until php /dbtest.php $DB_HOST $DB_BASE $DB_PORT $DB_USER $DB_PASS; do
|
||||||
echo Checking DB: $?
|
echo Checking DB: $?
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
@@ -26,12 +44,11 @@ function waitForDB() {
|
|||||||
|
|
||||||
function handleStartup() {
|
function handleStartup() {
|
||||||
# set mem limits and copy in custom logger config
|
# set mem limits and copy in custom logger config
|
||||||
if [ -z "$memory_limit" ]; then
|
|
||||||
memory_limit=512M
|
|
||||||
fi
|
|
||||||
sed -i "s/memory_limit.*/memory_limit=$memory_limit/g" /usr/local/etc/php/php.ini
|
sed -i "s/memory_limit.*/memory_limit=$memory_limit/g" /usr/local/etc/php/php.ini
|
||||||
cp /assets/monolog.yaml /opt/kimai/config/packages/monolog.yaml
|
cp /assets/monolog.yaml /opt/kimai/config/packages/monolog.yaml
|
||||||
|
|
||||||
|
tar -zx -C /opt/kimai -f /var/tmp/public.tgz
|
||||||
|
|
||||||
if [ -z "$USER_ID" ]; then
|
if [ -z "$USER_ID" ]; then
|
||||||
USER_ID=$(id -u www-data)
|
USER_ID=$(id -u www-data)
|
||||||
fi
|
fi
|
||||||
@@ -73,30 +90,7 @@ function handleStartup() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareKimai() {
|
config
|
||||||
# These are idempotent, so we can run them on every start-up
|
|
||||||
/opt/kimai/bin/console -n kimai:install
|
|
||||||
if [ ! -z "$ADMINPASS" ] && [ ! -a "$ADMINMAIL" ]; then
|
|
||||||
/opt/kimai/bin/console kimai:user:create admin "$ADMINMAIL" ROLE_SUPER_ADMIN "$ADMINPASS"
|
|
||||||
fi
|
|
||||||
echo "$KIMAI" > /opt/kimai/var/installed
|
|
||||||
echo "Kimai is ready"
|
|
||||||
}
|
|
||||||
|
|
||||||
function runServer() {
|
|
||||||
# Just while I'm fixing things
|
|
||||||
/opt/kimai/bin/console kimai:reload --env="$APP_ENV"
|
|
||||||
chown -R $USER_ID:$GROUP_ID /opt/kimai/var
|
|
||||||
if [ -e /use_apache ]; then
|
|
||||||
exec /usr/sbin/apache2 -D FOREGROUND
|
|
||||||
elif [ -e /use_fpm ]; then
|
|
||||||
exec php-fpm
|
|
||||||
else
|
|
||||||
echo "Error, unknown server type"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForDB
|
|
||||||
handleStartup
|
handleStartup
|
||||||
prepareKimai
|
|
||||||
runServer
|
exec /service.sh
|
||||||
@@ -21,7 +21,3 @@ indent_size = 4
|
|||||||
[*.yml]
|
[*.yml]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
[*.xml]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
|
|||||||
@@ -40,12 +40,8 @@ APP_SECRET=change_this_to_something_unique
|
|||||||
#================================================================================
|
#================================================================================
|
||||||
# Running behind reverse proxies? Try these:
|
# Running behind reverse proxies? Try these:
|
||||||
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
||||||
# TRUSTED_HOSTS=localhost|example.com
|
# TRUSTED_HOSTS=localhost,example.com
|
||||||
|
|
||||||
#================================================================================
|
#================================================================================
|
||||||
# unlikely, that you need to change this one
|
# unlikely, that you need to change this one
|
||||||
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$
|
||||||
|
|
||||||
###> symfony/messenger ###
|
|
||||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
|
||||||
###< symfony/messenger ###
|
|
||||||
|
|||||||
11
.eslintrc.js
Normal file
11
.eslintrc.js
Normal 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"],
|
||||||
|
}
|
||||||
18
.gitattributes
vendored
18
.gitattributes
vendored
@@ -1,18 +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
|
|
||||||
.gitattributes export-ignore
|
|
||||||
.gitignore 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
1
.github/FUNDING.yml
vendored
@@ -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/"]
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -53,11 +53,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Which PHP version are you using?
|
label: Which PHP version are you using?
|
||||||
options:
|
options:
|
||||||
- "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
|
||||||
|
|||||||
5
.github/release-drafter.yml
vendored
5
.github/release-drafter.yml
vendored
@@ -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.4**
|
**Compatible with PHP 8.1 to 8.3**
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
|
|
||||||
Involved in this release: $CONTRIBUTORS
|
Involved in this release: $CONTRIBUTORS - thank you!
|
||||||
|
|||||||
43
.github/workflows/docker-base.yaml
vendored
Normal file
43
.github/workflows/docker-base.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: 'Docker base image'
|
||||||
|
# act -j build -W ./.github/workflows/docker-base.yaml --secret-file .dockerhub.secrets
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 4 5 * *" # build on the 5th of every month
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
server: [ fpm, apache ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||||
|
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .docker
|
||||||
|
file: .docker/Dockerfile.kimai-base
|
||||||
|
build-args: |
|
||||||
|
TIMEZONE=Europe/London
|
||||||
|
BASE=${{ matrix.server }}
|
||||||
|
platforms: linux/amd64,linux/arm64 #,linux/arm/v8,linux/arm/v7,linux/arm/v6
|
||||||
|
tags: |
|
||||||
|
kimai/kimai-base:${{ matrix.server }}
|
||||||
|
push: true
|
||||||
|
|
||||||
|
|
||||||
86
.github/workflows/docker.yaml
vendored
86
.github/workflows/docker.yaml
vendored
@@ -1,16 +1,16 @@
|
|||||||
name: 'Docker Build'
|
name: 'Docker Build'
|
||||||
|
# act -j build -W ./.github/workflows/docker.yaml --secret-file .dockerhub.secrets
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
kimai_tag:
|
|
||||||
description: 'Kimai tag to build'
|
|
||||||
required: true
|
|
||||||
release:
|
release:
|
||||||
types: [released]
|
types: [released]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
server: [ fpm, apache ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -21,73 +21,67 @@ jobs:
|
|||||||
- name: Install buildx
|
- name: Install buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Install lastversion
|
||||||
|
run: sudo apt-get update -y; sudo apt-get install --no-install-recommends -y python3 python3-pip; pip install lastversion
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{secrets.DOCKERHUB_USERNAME}}
|
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||||
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||||
|
|
||||||
- name: Determine version
|
- name: get lastest version
|
||||||
run: |
|
id: remote_version
|
||||||
input="${{ github.event.inputs.kimai_tag }}"
|
run: echo "kimai_version=$(lastversion https://github.com/kimai/kimai)" >> $GITHUB_ENV
|
||||||
|
|
||||||
# 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
|
- name: Build
|
||||||
echo "Invalid version number: $version"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "kimai_version=$version" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: FPM image
|
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
|
||||||
build-args: |
|
build-args: |
|
||||||
KIMAI=${{ env.kimai_version }}
|
KIMAI=${{ env.kimai_version }}
|
||||||
BASE=fpm
|
TIMEZONE=Europe/London
|
||||||
|
BASE=${{ matrix.server }}
|
||||||
target: prod
|
target: prod
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: |
|
tags: |
|
||||||
kimai/kimai2:latest
|
kimai/kimai2:${{ matrix.server }}
|
||||||
kimai/kimai2:fpm
|
kimai/kimai2:${{ matrix.server }}-${{ env.kimai_version }}
|
||||||
kimai/kimai2:fpm-${{ env.kimai_version }}
|
|
||||||
push: true
|
push: true
|
||||||
|
|
||||||
- name: Apache image
|
- name: Build dev image
|
||||||
|
if: matrix.server == 'apache'
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
|
||||||
build-args: |
|
|
||||||
KIMAI=${{ env.kimai_version }}
|
|
||||||
BASE=apache
|
|
||||||
target: prod
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: |
|
|
||||||
kimai/kimai2:apache
|
|
||||||
kimai/kimai2:apache-${{ env.kimai_version }}
|
|
||||||
push: true
|
|
||||||
|
|
||||||
- name: Development image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile
|
|
||||||
build-args: |
|
build-args: |
|
||||||
KIMAI=${{ env.kimai_version }}
|
KIMAI=${{ env.kimai_version }}
|
||||||
|
TIMEZONE=Europe/London
|
||||||
BASE=apache
|
BASE=apache
|
||||||
target: dev
|
target: dev
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: |
|
tags: |
|
||||||
kimai/kimai2:dev
|
kimai/kimai2:dev
|
||||||
push: true
|
push: true
|
||||||
|
|
||||||
|
tag:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{secrets.DOCKERHUB_USERNAME}}
|
||||||
|
password: ${{secrets.DOCKERHUB_PASSWORD}}
|
||||||
|
|
||||||
|
- name: Install buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Pull images
|
||||||
|
run: |
|
||||||
|
docker pull kimai/kimai2:fpm
|
||||||
|
|
||||||
|
- name: Tag latest
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t kimai/kimai2:latest kimai/kimai2:fpm
|
||||||
|
|||||||
16
.github/workflows/lockfiles.yaml
vendored
16
.github/workflows/lockfiles.yaml
vendored
@@ -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
|
||||||
|
|||||||
6
.github/workflows/testing.yaml
vendored
6
.github/workflows/testing.yaml
vendored
@@ -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.1', '8.2', '8.3', '8.4']
|
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
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload code coverage
|
- name: Upload code coverage
|
||||||
if: matrix.php == '8.2'
|
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
|
||||||
|
|||||||
2
.github/workflows/website.yaml
vendored
2
.github/workflows/website.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
echo "Using input provided: $input"
|
echo "Using input provided: $input"
|
||||||
version="$input"
|
version="$input"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "kimai_version=$version" >> $GITHUB_ENV
|
echo "kimai_version=$version" >> $GITHUB_ENV
|
||||||
|
|
||||||
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
|
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
/.env-*
|
/.env-*
|
||||||
/.idea/
|
/.idea/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/phpstan.sh
|
||||||
|
|
||||||
# custom apache rules e.g. to deactivate ioncube loader
|
# custom apache rules e.g. to deactivate ioncube loader
|
||||||
/public/.user.ini
|
/public/.user.ini
|
||||||
@@ -18,7 +19,6 @@
|
|||||||
# 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/invoices*
|
/var/invoices*
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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-unit`
|
- 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).
|
||||||
|
|||||||
279
Dockerfile
279
Dockerfile
@@ -4,27 +4,12 @@
|
|||||||
# | . \| | | | | | | (_| | |
|
# | . \| | | | | | | (_| | |
|
||||||
# |_|\_\_|_| |_| |_|\__,_|_|
|
# |_|\_\_|_| |_| |_|\__,_|_|
|
||||||
#
|
#
|
||||||
# Kimai images for:
|
|
||||||
# - plain PHP FPM (kimai/kimai2:fpm)
|
|
||||||
# - Apache with PHP (kimai/kimai2:apache)
|
|
||||||
# - Development (kimai/kimai2:dev)
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# For local testing by maintainer:
|
|
||||||
#
|
|
||||||
# docker build --no-cache -t kimai-fpm --build-arg BASE=fpm .
|
|
||||||
# docker build --no-cache -t kimai-apache --build-arg BASE=apache .
|
|
||||||
# docker run -d --name kimai-apache-app kimai-apache
|
|
||||||
# docker exec -ti kimai-apache-app /bin/bash
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# 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
|
|
||||||
# Pass-through Arguments: https://benkyriakou.com/posts/docker-args-empty
|
|
||||||
# Best practices: https://docs.docker.com/build/building/best-practices/
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Source base, one of: fpm, apache
|
# Source base [fpm/apache]
|
||||||
ARG BASE="fpm"
|
ARG BASE="fpm"
|
||||||
# Kimai branch/tag to run
|
ARG PHP_VER="8.2"
|
||||||
|
ARG COMPOSER_VER="latest"
|
||||||
|
# Branch name
|
||||||
ARG KIMAI="main"
|
ARG KIMAI="main"
|
||||||
# Timezone for images
|
# Timezone for images
|
||||||
ARG TIMEZONE="Europe/Berlin"
|
ARG TIMEZONE="Europe/Berlin"
|
||||||
@@ -33,209 +18,32 @@ ARG TIMEZONE="Europe/Berlin"
|
|||||||
# Shared tools
|
# Shared tools
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
# composer base image
|
# full kimai source
|
||||||
FROM composer:latest AS composer
|
FROM alpine:latest AS git-dev
|
||||||
|
# pass-through Arguments in every stage. See: https://benkyriakou.com/posts/docker-args-empty
|
||||||
###########################
|
|
||||||
# PHP extensions
|
|
||||||
###########################
|
|
||||||
|
|
||||||
# fpm alpine php extension base
|
|
||||||
FROM php:8.3-fpm-alpine AS fpm-php-ext-base
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
# build-tools
|
|
||||||
autoconf \
|
|
||||||
dpkg \
|
|
||||||
dpkg-dev \
|
|
||||||
file \
|
|
||||||
g++ \
|
|
||||||
gcc \
|
|
||||||
icu-dev \
|
|
||||||
libatomic \
|
|
||||||
libc-dev \
|
|
||||||
libgomp \
|
|
||||||
libmagic \
|
|
||||||
m4 \
|
|
||||||
make \
|
|
||||||
mpc1 \
|
|
||||||
mpfr4 \
|
|
||||||
musl-dev \
|
|
||||||
perl \
|
|
||||||
re2c \
|
|
||||||
# gd
|
|
||||||
freetype-dev \
|
|
||||||
libpng-dev \
|
|
||||||
# icu
|
|
||||||
icu-dev \
|
|
||||||
icu-data-full \
|
|
||||||
# ldap
|
|
||||||
openldap-dev \
|
|
||||||
libldap \
|
|
||||||
# zip
|
|
||||||
libzip-dev \
|
|
||||||
# xsl
|
|
||||||
libxslt-dev
|
|
||||||
|
|
||||||
# apache debian php extension base
|
|
||||||
FROM php:8.3-apache-bookworm AS apache-php-ext-base
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
libldap2-dev \
|
|
||||||
libicu-dev \
|
|
||||||
libpng-dev \
|
|
||||||
libzip-dev \
|
|
||||||
libxslt1-dev \
|
|
||||||
libfreetype6-dev
|
|
||||||
|
|
||||||
# php extension gd - 13.86s
|
|
||||||
FROM ${BASE}-php-ext-base AS php-ext-gd
|
|
||||||
RUN docker-php-ext-configure gd \
|
|
||||||
--with-freetype && \
|
|
||||||
docker-php-ext-install -j$(nproc) gd
|
|
||||||
|
|
||||||
# php extension intl : 15.26s
|
|
||||||
FROM ${BASE}-php-ext-base AS php-ext-intl
|
|
||||||
RUN docker-php-ext-install -j$(nproc) intl
|
|
||||||
|
|
||||||
# php extension ldap : 8.45s
|
|
||||||
FROM ${BASE}-php-ext-base AS php-ext-ldap
|
|
||||||
RUN docker-php-ext-configure ldap && \
|
|
||||||
docker-php-ext-install -j$(nproc) ldap
|
|
||||||
|
|
||||||
# php extension pdo_mysql : 6.14s
|
|
||||||
FROM ${BASE}-php-ext-base AS php-ext-pdo_mysql
|
|
||||||
RUN docker-php-ext-install -j$(nproc) pdo_mysql
|
|
||||||
|
|
||||||
# php extension zip : 8.18s
|
|
||||||
FROM ${BASE}-php-ext-base AS php-ext-zip
|
|
||||||
RUN docker-php-ext-install -j$(nproc) zip
|
|
||||||
|
|
||||||
# php extension xsl : ?.?? s
|
|
||||||
FROM ${BASE}-php-ext-base AS php-ext-xsl
|
|
||||||
RUN docker-php-ext-install -j$(nproc) xsl
|
|
||||||
|
|
||||||
# php extension opcache
|
|
||||||
FROM ${BASE}-php-ext-base AS php-ext-opcache
|
|
||||||
RUN docker-php-ext-install -j$(nproc) opcache
|
|
||||||
|
|
||||||
###########################
|
|
||||||
# fpm base build
|
|
||||||
###########################
|
|
||||||
|
|
||||||
FROM php:8.3-fpm-alpine AS fpm-base
|
|
||||||
ARG TIMEZONE
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
bash \
|
|
||||||
coreutils \
|
|
||||||
freetype \
|
|
||||||
haveged \
|
|
||||||
icu \
|
|
||||||
icu-data-full \
|
|
||||||
libldap \
|
|
||||||
libpng \
|
|
||||||
libzip \
|
|
||||||
libxslt-dev \
|
|
||||||
fcgi \
|
|
||||||
tzdata && \
|
|
||||||
touch /use_fpm && \
|
|
||||||
sed -i "s/;ping.path/ping.path/g" /usr/local/etc/php-fpm.d/www.conf && \
|
|
||||||
sed -i "s/;access.suppress_path\[\] = \/ping/access.suppress_path\[\] = \/ping/g" /usr/local/etc/php-fpm.d/www.conf
|
|
||||||
|
|
||||||
EXPOSE 9000
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
|
||||||
CMD \
|
|
||||||
SCRIPT_NAME=/ping \
|
|
||||||
SCRIPT_FILENAME=/ping \
|
|
||||||
REQUEST_METHOD=GET \
|
|
||||||
cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
|
|
||||||
|
|
||||||
###########################
|
|
||||||
# apache base build
|
|
||||||
###########################
|
|
||||||
|
|
||||||
FROM php:8.3-apache-bookworm AS apache-base
|
|
||||||
ARG TIMEZONE
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
bash \
|
|
||||||
haveged \
|
|
||||||
libicu72 \
|
|
||||||
libldap-common \
|
|
||||||
libpng16-16 \
|
|
||||||
libzip4 \
|
|
||||||
libxslt1.1 \
|
|
||||||
libfreetype6 \
|
|
||||||
unzip && \
|
|
||||||
echo "Listen 8001" > /etc/apache2/ports.conf && \
|
|
||||||
a2enmod rewrite && \
|
|
||||||
touch /use_apache
|
|
||||||
|
|
||||||
COPY .docker/000-default.conf /etc/apache2/sites-available/000-default.conf
|
|
||||||
|
|
||||||
EXPOSE 8001
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
|
|
||||||
CMD curl -f http://127.0.0.1:8001 || exit 1
|
|
||||||
|
|
||||||
###########################
|
|
||||||
# global base build
|
|
||||||
###########################
|
|
||||||
|
|
||||||
FROM ${BASE}-base AS php-base
|
|
||||||
ARG TIMEZONE
|
|
||||||
|
|
||||||
ENV TIMEZONE=${TIMEZONE}
|
|
||||||
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone && \
|
|
||||||
# make composer home dir
|
|
||||||
mkdir /composer && \
|
|
||||||
chown -R www-data:www-data /composer
|
|
||||||
|
|
||||||
# copy composer
|
|
||||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
|
||||||
|
|
||||||
# copy php extensions
|
|
||||||
|
|
||||||
# PHP extension xsl
|
|
||||||
COPY --from=php-ext-xsl /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini
|
|
||||||
COPY --from=php-ext-xsl /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xsl.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xsl.so
|
|
||||||
# PHP extension pdo_mysql
|
|
||||||
COPY --from=php-ext-pdo_mysql /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini
|
|
||||||
COPY --from=php-ext-pdo_mysql /usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so
|
|
||||||
# PHP extension zip
|
|
||||||
COPY --from=php-ext-zip /usr/local/etc/php/conf.d/docker-php-ext-zip.ini /usr/local/etc/php/conf.d/docker-php-ext-zip.ini
|
|
||||||
COPY --from=php-ext-zip /usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so
|
|
||||||
# PHP extension ldap
|
|
||||||
COPY --from=php-ext-ldap /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini
|
|
||||||
COPY --from=php-ext-ldap /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so
|
|
||||||
# PHP extension gd
|
|
||||||
COPY --from=php-ext-gd /usr/local/etc/php/conf.d/docker-php-ext-gd.ini /usr/local/etc/php/conf.d/docker-php-ext-gd.ini
|
|
||||||
COPY --from=php-ext-gd /usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so
|
|
||||||
# PHP extension intl
|
|
||||||
COPY --from=php-ext-intl /usr/local/etc/php/conf.d/docker-php-ext-intl.ini /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
|
|
||||||
COPY --from=php-ext-intl /usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so
|
|
||||||
# PHP extension opcache
|
|
||||||
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
|
|
||||||
###########################
|
|
||||||
|
|
||||||
FROM alpine:latest AS git-prod
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
# FPM base
|
||||||
|
FROM kimai/kimai-base:fpm AS fpm-base
|
||||||
|
|
||||||
|
# Apache base
|
||||||
|
FROM kimai/kimai-base:apache AS apache-base
|
||||||
|
COPY .docker/000-default.conf /etc/apache2/sites-available/000-default.conf
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# global base build
|
# global base build
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
FROM php-base AS base
|
FROM ${BASE}-base AS base
|
||||||
ARG KIMAI
|
ARG KIMAI
|
||||||
ARG TIMEZONE
|
ARG TIMEZONE
|
||||||
|
|
||||||
@@ -247,7 +55,8 @@ LABEL org.opencontainers.image.title="Kimai" \
|
|||||||
org.opencontainers.image.source="https://github.com/kimai/kimai" \
|
org.opencontainers.image.source="https://github.com/kimai/kimai" \
|
||||||
org.opencontainers.image.version="${KIMAI}" \
|
org.opencontainers.image.version="${KIMAI}" \
|
||||||
org.opencontainers.image.vendor="Kevin Papst" \
|
org.opencontainers.image.vendor="Kevin Papst" \
|
||||||
org.opencontainers.image.licenses="AGPL-3.0"
|
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||||
|
org.opencontainers.image.base.name="docker.io/library/alpine"
|
||||||
|
|
||||||
ENV KIMAI=${KIMAI}
|
ENV KIMAI=${KIMAI}
|
||||||
ENV TIMEZONE=${TIMEZONE}
|
ENV TIMEZONE=${TIMEZONE}
|
||||||
@@ -256,37 +65,49 @@ RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} >
|
|||||||
chown -R www-data:www-data /composer
|
chown -R www-data:www-data /composer
|
||||||
|
|
||||||
# copy startup script & DB checking script
|
# copy startup script & DB checking script
|
||||||
|
COPY .docker/startup.sh /startup.sh
|
||||||
|
COPY .docker/service.sh /service.sh
|
||||||
|
COPY .docker/self-test.sh /self-test.sh
|
||||||
COPY .docker/dbtest.php /dbtest.php
|
COPY .docker/dbtest.php /dbtest.php
|
||||||
COPY .docker/entrypoint.sh /entrypoint.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=5.7.40"
|
||||||
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=
|
||||||
ENV ADMINMAIL=
|
ENV ADMINMAIL=
|
||||||
|
ENV DB_TYPE=
|
||||||
|
ENV DB_USER=
|
||||||
|
ENV DB_PASS=
|
||||||
|
ENV DB_HOST=
|
||||||
|
ENV DB_PORT=
|
||||||
|
ENV DB_BASE=
|
||||||
|
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||||
|
# If this set then the image will start, run a self test and then exit. It's used for the release process
|
||||||
|
ENV TEST_AND_EXIT=
|
||||||
|
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||||
ENV USER_ID=
|
ENV USER_ID=
|
||||||
ENV GROUP_ID=
|
ENV GROUP_ID=
|
||||||
# default values to configure composer behavior
|
|
||||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
|
||||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
|
||||||
|
|
||||||
VOLUME [ "/opt/kimai/var" ]
|
VOLUME [ "/opt/kimai/var" ]
|
||||||
|
|
||||||
CMD [ "/entrypoint.sh" ]
|
CMD [ "/startup.sh" ]
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# final builds
|
# final builds
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
# development build
|
# developement 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 && \
|
||||||
@@ -297,12 +118,13 @@ RUN \
|
|||||||
mkdir -p /opt/kimai/var/logs && chmod 777 /opt/kimai/var/logs && \
|
mkdir -p /opt/kimai/var/logs && chmod 777 /opt/kimai/var/logs && \
|
||||||
sed "s/128M/-1/g" /usr/local/etc/php/php.ini-development > /opt/kimai/php-cli.ini && \
|
sed "s/128M/-1/g" /usr/local/etc/php/php.ini-development > /opt/kimai/php-cli.ini && \
|
||||||
sed -i "s/env php/env -S php -c \/opt\/kimai\/php-cli.ini/g" /opt/kimai/bin/console && \
|
sed -i "s/env php/env -S php -c \/opt\/kimai\/php-cli.ini/g" /opt/kimai/bin/console && \
|
||||||
|
tar -C /opt/kimai -zcvf /var/tmp/public.tgz public && \
|
||||||
/opt/kimai/bin/console kimai:version | awk '{print $2}' > /opt/kimai/version.txt
|
/opt/kimai/bin/console kimai:version | awk '{print $2}' > /opt/kimai/version.txt
|
||||||
ENV APP_ENV=dev
|
ENV APP_ENV=dev
|
||||||
ENV DATABASE_URL=
|
ENV DATABASE_URL=
|
||||||
ENV memory_limit=512M
|
ENV memory_limit=256M
|
||||||
|
|
||||||
# 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 +134,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 && \
|
||||||
@@ -324,7 +146,8 @@ RUN \
|
|||||||
mkdir -p /opt/kimai/var/logs && chmod 777 /opt/kimai/var/logs && \
|
mkdir -p /opt/kimai/var/logs && chmod 777 /opt/kimai/var/logs && \
|
||||||
sed "s/128M/-1/g" /usr/local/etc/php/php.ini-development > /opt/kimai/php-cli.ini && \
|
sed "s/128M/-1/g" /usr/local/etc/php/php.ini-development > /opt/kimai/php-cli.ini && \
|
||||||
chown -R www-data:www-data /opt/kimai /usr/local/etc/php/php.ini && \
|
chown -R www-data:www-data /opt/kimai /usr/local/etc/php/php.ini && \
|
||||||
|
tar -C /opt/kimai -zcvf /var/tmp/public.tgz public && \
|
||||||
/opt/kimai/bin/console kimai:version | awk '{print $2}' > /opt/kimai/version.txt
|
/opt/kimai/bin/console kimai:version | awk '{print $2}' > /opt/kimai/version.txt
|
||||||
ENV APP_ENV=prod
|
ENV APP_ENV=prod
|
||||||
ENV DATABASE_URL=
|
ENV DATABASE_URL=
|
||||||
ENV memory_limit=512M
|
ENV memory_limit=256M
|
||||||
|
|||||||
@@ -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 (support for PHP 8.2, 8.3, 8.4)
|
- 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`
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -1,24 +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 !!**
|
|
||||||
|
|
||||||
### Developer
|
|
||||||
|
|
||||||
Removed translations:
|
|
||||||
- `action.edit`: use `edit` instead
|
|
||||||
- `my.profile`: use `user_profile` instead
|
|
||||||
- `stats.userAmountToday`: use `` instead
|
|
||||||
- `stats.userAmountWeek`: use `` instead
|
|
||||||
- `stats.userAmountMonth`: use `` instead
|
|
||||||
- `stats.userAmountYear`: use `` instead
|
|
||||||
- `stats.userAmountTotal`: use `` instead
|
|
||||||
- `update_multiple`
|
|
||||||
14
UPGRADING.md
14
UPGRADING.md
@@ -1,4 +1,4 @@
|
|||||||
# Upgrading Kimai - Version 2.x
|
# Upgrading Kimai - Version2.x
|
||||||
|
|
||||||
_Make sure to create a backup before you start!_
|
_Make sure to create a backup before you start!_
|
||||||
|
|
||||||
@@ -8,6 +8,18 @@ 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.
|
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.
|
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||||
|
|
||||||
|
## [2.14.0](https://github.com/kimai/kimai/releases/tag/2.13.0)
|
||||||
|
|
||||||
|
**DEVELOPERS ONLY**
|
||||||
|
|
||||||
|
The API now uses pagination globally for all collection endpoints. This might break current implementations
|
||||||
|
|
||||||
|
All collections are limited to 50 entries by default (changes in Customer, Project, Activity, User).
|
||||||
|
|
||||||
|
The parameters to influence pagination, page size and page number are `size` (and `pageSize`) and `page`.
|
||||||
|
|
||||||
|
Read the [API pagination docs](https://www.kimai.org/documentation/api-pagination.html) for more infos.
|
||||||
|
|
||||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
## [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)
|
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)
|
||||||
|
|||||||
0
assets/.gitignore
vendored
Normal file
0
assets/.gitignore
vendored
Normal file
@@ -1,5 +0,0 @@
|
|||||||
require('highlight.js/styles/github-dark.css');
|
|
||||||
|
|
||||||
const hljs = require('highlight.js/lib/common');
|
|
||||||
|
|
||||||
global.hljs = hljs;
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default class KimaiAutocompleteTags extends KimaiAutocomplete {
|
|||||||
API.get(apiUrl, {'name': query}, (data) => {
|
API.get(apiUrl, {'name': query}, (data) => {
|
||||||
let results = [];
|
let results = [];
|
||||||
for (let item of data) {
|
for (let item of data) {
|
||||||
results.push({text: item.name, value: item.name, color: item.color});
|
results.push({text: item.name, value: item.name, color: item['color-safe']});
|
||||||
}
|
}
|
||||||
callback(results);
|
callback(results);
|
||||||
}, () => {
|
}, () => {
|
||||||
|
|||||||
@@ -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) {
|
} else if (element.dataset.eventBubbles !== undefined) {
|
||||||
form.dispatchEvent(new Event(event));
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -44,7 +44,7 @@ export default class KimaiFormTomselectPlugin extends KimaiFormPlugin {
|
|||||||
let item = '<div class="list-group-item border-0 p-1 ps-2 text-nowrap">';
|
let item = '<div class="list-group-item border-0 p-1 ps-2 text-nowrap">';
|
||||||
// if no color is set, do NOT add an empty placeholder
|
// if no color is set, do NOT add an empty placeholder
|
||||||
if (data.color !== undefined) {
|
if (data.color !== undefined) {
|
||||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item me-2"> </span>';
|
item += '<span style="background-color:' + data.color + '" class="color-choice-item"> </span>';
|
||||||
}
|
}
|
||||||
item += escape(data.text) + '</div>';
|
item += escape(data.text) + '</div>';
|
||||||
return item;
|
return item;
|
||||||
@@ -53,7 +53,7 @@ export default class KimaiFormTomselectPlugin extends KimaiFormPlugin {
|
|||||||
let item = '<div class="text-nowrap">';
|
let item = '<div class="text-nowrap">';
|
||||||
// if no color is set, do NOT add an empty placeholder
|
// if no color is set, do NOT add an empty placeholder
|
||||||
if (data.color !== undefined) {
|
if (data.color !== undefined) {
|
||||||
item += '<span style="background-color:' + data.color + '" class="color-choice-item me-2"> </span>';
|
item += '<span style="background-color:' + data.color + '" class="color-choice-item"> </span>';
|
||||||
}
|
}
|
||||||
item += escape(data.text) + '</div>';
|
item += escape(data.text) + '</div>';
|
||||||
return item;
|
return item;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
|
|||||||
|
|
||||||
_getModal()
|
_getModal()
|
||||||
{
|
{
|
||||||
return Modal.getOrCreateInstance(this._getModalElement());
|
return Modal.getOrCreateInstance(this._getModalElement())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(/,/, '.');
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,101 +23,64 @@ 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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default class KimaiThemeInitializer extends KimaiPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
@@ -73,6 +72,21 @@ export default class KimaiCalendar {
|
|||||||
/** @type {KimaiAlert} ALERT */
|
/** @type {KimaiAlert} ALERT */
|
||||||
const ALERT = this.kimai.getPlugin('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
|
||||||
BootstrapTheme.prototype.classes = {
|
BootstrapTheme.prototype.classes = {
|
||||||
@@ -95,18 +109,18 @@ export default class KimaiCalendar {
|
|||||||
nextYear: this.options['icons']['nextYear'],
|
nextYear: this.options['icons']['nextYear'],
|
||||||
};
|
};
|
||||||
BootstrapTheme.prototype.rtlIconClasses = {
|
BootstrapTheme.prototype.rtlIconClasses = {
|
||||||
prev: this.options['icons']['previous'],
|
prev: this.options['icons']['next'],
|
||||||
next: this.options['icons']['next'],
|
next: this.options['icons']['previous'],
|
||||||
prevYear: this.options['icons']['previousYear'],
|
prevYear: this.options['icons']['nextYear'],
|
||||||
nextYear: this.options['icons']['nextYear'],
|
nextYear: this.options['icons']['previousYear'],
|
||||||
};
|
};
|
||||||
|
|
||||||
let calendarOptions = {
|
let calendarOptions = {
|
||||||
locales: [ enGbLocale, enUsLocale, arLocale, csLocale, daLocale, deLocale, deAtLocale, elLocale,
|
locales: [ enGbLocale, enUsLocale, arLocale, csLocale, daLocale, deLocale, deAtLocale, elLocale,
|
||||||
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
|
||||||
@@ -141,9 +155,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'],
|
||||||
@@ -159,20 +172,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']
|
||||||
@@ -258,14 +265,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 =============
|
||||||
@@ -409,13 +410,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)
|
||||||
},
|
},
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
@@ -528,44 +529,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}
|
||||||
@@ -606,7 +569,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,73 +688,4 @@ export default class KimaiCalendar {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
}
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 => {
|
||||||
@@ -92,7 +93,7 @@ export default class KimaiPaginatedBoxWidget {
|
|||||||
if (node.tagName !== undefined && node.tagName === 'SCRIPT') {
|
if (node.tagName !== undefined && node.tagName === 'SCRIPT') {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.text = node.innerHTML;
|
script.text = node.innerHTML;
|
||||||
node.parentNode.replaceChild(script, node);
|
node.parentNode.replaceChild(script, node );
|
||||||
} else {
|
} else {
|
||||||
for (const child of node.childNodes) {
|
for (const child of node.childNodes) {
|
||||||
this._makeScriptExecutable(child);
|
this._makeScriptExecutable(child);
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,9 +26,6 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.input-group {
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +33,7 @@
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
border-radius: var(--tblr-border-radius);
|
border-radius: var(--tblr-border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,19 +53,4 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
.inline-search {
|
.inline-search {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
|
/* the entire inline-search should actually use .input-group-flat, but that causes some weird css problems inside the dropdown menu */
|
||||||
|
#searchTerm {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 360px) {
|
@media (min-width: 360px) {
|
||||||
|
|||||||
@@ -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
39
assets/sass/pages.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -17,35 +17,18 @@ fieldset.form-fieldset > legend {
|
|||||||
--tblr-dropdown-divider-margin-y: .5rem;
|
--tblr-dropdown-divider-margin-y: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host,:root,[data-bs-theme=light] {
|
/*
|
||||||
/* remove the shadow from all form element groups */
|
fixes translucent scroll border, e.g. in timesheet modal duration dropdown
|
||||||
--tblr-box-shadow-input: none;
|
https://github.com/tabler/tabler/issues/1606
|
||||||
|
*/
|
||||||
|
.dropdown-menu {
|
||||||
|
background-clip: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar[data-bs-theme=dark] {
|
/*
|
||||||
/* give navigation elements slightly higher contrast */
|
hide empty fieldset
|
||||||
--tblr-navbar-color: hsla(0,0%,100%,.8);
|
https://github.com/tabler/tabler/issues/1650
|
||||||
|
*/
|
||||||
|
fieldset:empty {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* this is not a bugfix, but it adds a border for better visual segregation */
|
|
||||||
.nav-pills .nav-link.active {
|
|
||||||
border-width: 1px;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
@@ -119,7 +123,7 @@ table.dataTable {
|
|||||||
}
|
}
|
||||||
th.weekend,
|
th.weekend,
|
||||||
td.weekend {
|
td.weekend {
|
||||||
background-color: var(--tblr-bg-surface-tertiary);
|
background-color: var(--tblr-gray-50);
|
||||||
}
|
}
|
||||||
/* order is important, "today” should overwrite "weekend" therefor later in the file */
|
/* order is important, "today” should overwrite "weekend" therefor later in the file */
|
||||||
th.today {
|
th.today {
|
||||||
@@ -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 {
|
||||||
|
|||||||
45
assets/sass/theme-dark.scss
Normal file
45
assets/sass/theme-dark.scss
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
@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 {
|
||||||
|
th.today {
|
||||||
|
color: var(--tblr-muted);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
17
babel.config.js
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "8.1.*||8.2.*||8.3.*||8.4.*",
|
"php": "8.1.*||8.2.*||8.3.*",
|
||||||
"ext-gd": "*",
|
"ext-gd": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
@@ -28,23 +28,19 @@
|
|||||||
"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": "^2.8",
|
"doctrine/orm": "^2.8",
|
||||||
"easybill/zugferd-php": "^2.1",
|
|
||||||
"endroid/qr-code": "^4.8",
|
"endroid/qr-code": "^4.8",
|
||||||
"erusev/parsedown": "^1.6",
|
"erusev/parsedown": "^1.6",
|
||||||
"friendsofsymfony/rest-bundle": "^3.0",
|
"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.2",
|
||||||
"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": "^3.0",
|
||||||
"phpoffice/phpspreadsheet": "^2.0",
|
"phpoffice/phpspreadsheet": "^2.0",
|
||||||
"phpoffice/phpword": "^1.0",
|
"phpoffice/phpword": "^1.0",
|
||||||
@@ -65,7 +61,6 @@
|
|||||||
"symfony/mailer": "^6.0",
|
"symfony/mailer": "^6.0",
|
||||||
"symfony/mime": "^6.0",
|
"symfony/mime": "^6.0",
|
||||||
"symfony/monolog-bundle": "^3.4",
|
"symfony/monolog-bundle": "^3.4",
|
||||||
"symfony/process": "^6.0",
|
|
||||||
"symfony/rate-limiter": "^6.0",
|
"symfony/rate-limiter": "^6.0",
|
||||||
"symfony/runtime": "^6.0",
|
"symfony/runtime": "^6.0",
|
||||||
"symfony/security-bundle": "^6.0",
|
"symfony/security-bundle": "^6.0",
|
||||||
@@ -74,7 +69,6 @@
|
|||||||
"symfony/translation": "^6.0",
|
"symfony/translation": "^6.0",
|
||||||
"symfony/twig-bundle": "^6.0",
|
"symfony/twig-bundle": "^6.0",
|
||||||
"symfony/validator": "^6.0",
|
"symfony/validator": "^6.0",
|
||||||
"symfony/webhook": "^6.0",
|
|
||||||
"symfony/webpack-encore-bundle": "^2.0",
|
"symfony/webpack-encore-bundle": "^2.0",
|
||||||
"symfony/yaml": "^6.0",
|
"symfony/yaml": "^6.0",
|
||||||
"twig/cssinliner-extra": "^3.0",
|
"twig/cssinliner-extra": "^3.0",
|
||||||
@@ -89,13 +83,13 @@
|
|||||||
"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": "^6.0",
|
"symfony/browser-kit": "^6.0",
|
||||||
"symfony/css-selector": "^6.0",
|
"symfony/css-selector": "^6.0",
|
||||||
"symfony/debug-bundle": "^6.0",
|
"symfony/debug-bundle": "^6.0",
|
||||||
@@ -140,10 +134,6 @@
|
|||||||
"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-php81": "*",
|
"symfony/polyfill-php81": "*",
|
||||||
"symfony/polyfill-php80": "*",
|
"symfony/polyfill-php80": "*",
|
||||||
|
|||||||
4975
composer.lock
generated
4975
composer.lock
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -25,8 +25,6 @@ doctrine:
|
|||||||
datetime_immutable: App\Doctrine\UTCDateTimeImmutableType
|
datetime_immutable: App\Doctrine\UTCDateTimeImmutableType
|
||||||
orm:
|
orm:
|
||||||
controller_resolver:
|
controller_resolver:
|
||||||
# FIXME this is causing a deprecation and needs to be changed
|
|
||||||
# auto_mapping: false
|
|
||||||
auto_mapping: true
|
auto_mapping: true
|
||||||
auto_generate_proxy_classes: '%kernel.debug%'
|
auto_generate_proxy_classes: '%kernel.debug%'
|
||||||
default_entity_manager: default
|
default_entity_manager: default
|
||||||
|
|||||||
@@ -39,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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ kimai:
|
|||||||
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
||||||
LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet']
|
LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet']
|
||||||
REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting']
|
REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting']
|
||||||
EVERYONE: ['api_access','hours_own_profile']
|
EVERYONE: ['api_access']
|
||||||
# permissions which are deactivated, as these features are hidden for now
|
# permissions which are deactivated, as these features are hidden for now
|
||||||
# brave users can try to activate them and be surprised what happens
|
# brave users can try to activate them and be surprised what happens
|
||||||
REGISTER_BETA: []
|
REGISTER_BETA: []
|
||||||
@@ -117,10 +117,10 @@ kimai:
|
|||||||
ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING', 'EVERYONE']
|
ROLE_SUPER_ADMIN: ['ACTIVITIES','PROJECTS','CUSTOMERS','INVOICE','INVOICE_ADMIN','TIMESHEET','TIMESHEET_OTHER','PROFILE','PROFILE_OTHER','USER','TEAMS','RATE','RATE_OTHER','EXPORT','BILLABLE','TAGS','LOCKDOWN','REPORTING', 'EVERYONE']
|
||||||
# mapping a "role name" to an array of "permission names"
|
# mapping a "role name" to an array of "permission names"
|
||||||
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','hours_own_profile']
|
||||||
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','hours_own_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','hours_own_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','hours_own_profile']
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
framework:
|
|
||||||
messenger:
|
|
||||||
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
|
|
||||||
# failure_transport: failed
|
|
||||||
|
|
||||||
transports:
|
|
||||||
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
|
||||||
# async: '%env(MESSENGER_TRANSPORT_DSN)%'
|
|
||||||
# failed: 'doctrine://default?queue_name=failed'
|
|
||||||
sync: 'sync://'
|
|
||||||
|
|
||||||
routing:
|
|
||||||
# Route your messages to the transports
|
|
||||||
# 'App\Message\YourMessage': async
|
|
||||||
|
|
||||||
# when@test:
|
|
||||||
# framework:
|
|
||||||
# messenger:
|
|
||||||
# transports:
|
|
||||||
# # replace with your transport name here (e.g., my_transport: 'in-memory://')
|
|
||||||
# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
|
|
||||||
# async: 'in-memory://'
|
|
||||||
@@ -6,16 +6,11 @@ when@prod:
|
|||||||
handlers:
|
handlers:
|
||||||
main:
|
main:
|
||||||
type: fingers_crossed
|
type: fingers_crossed
|
||||||
action_level: error
|
action_level: notice
|
||||||
handler: file_log
|
handler: nested
|
||||||
excluded_http_codes:
|
excluded_http_codes: [403, 404]
|
||||||
- {400: ['^/api/']}
|
|
||||||
- {401: ['^/api/']}
|
|
||||||
- 403
|
|
||||||
- 404
|
|
||||||
- {405: ['/homepage$', '/login_check$', '/export/data$']}
|
|
||||||
channels: ["!deprecation"]
|
channels: ["!deprecation"]
|
||||||
file_log:
|
nested:
|
||||||
type: stream
|
type: stream
|
||||||
level: info
|
level: info
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
@@ -29,20 +24,10 @@ when@dev:
|
|||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
main:
|
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
|
type: stream
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
level: debug
|
level: debug
|
||||||
|
channels: ["!event", "!deprecation"]
|
||||||
formatter: monolog.formatter.kimai
|
formatter: monolog.formatter.kimai
|
||||||
console:
|
console:
|
||||||
type: console
|
type: console
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ parameters:
|
|||||||
|
|
||||||
nelmio_cors:
|
nelmio_cors:
|
||||||
defaults:
|
defaults:
|
||||||
# for security reasons we do not allow CORS by default
|
allow_credentials: false
|
||||||
|
# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||||
|
# allow_headers: ['Content-Type', 'Authorization', 'X-AUTH-USER', 'X-AUTH-TOKEN']
|
||||||
|
# allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
|
# expose_headers: ['Link']
|
||||||
|
# max_age: 3600
|
||||||
allow_origin: []
|
allow_origin: []
|
||||||
allow_headers: []
|
allow_headers: []
|
||||||
allow_methods: []
|
allow_methods: []
|
||||||
@@ -12,18 +17,10 @@ nelmio_cors:
|
|||||||
hosts: []
|
hosts: []
|
||||||
origin_regex: true
|
origin_regex: true
|
||||||
forced_allow_origin_value: ~
|
forced_allow_origin_value: ~
|
||||||
|
# allow_private_network: true
|
||||||
paths:
|
paths:
|
||||||
# only the API endpoints are accessible
|
|
||||||
'^/api/':
|
'^/api/':
|
||||||
# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
|
||||||
allow_origin: ['*']
|
allow_origin: ['*']
|
||||||
allow_headers: ['Content-Type', 'Authorization', 'X-AUTH-USER', 'X-AUTH-TOKEN']
|
allow_headers: ['X-AUTH-USER', 'X-AUTH-TOKEN', 'Content-Type', 'Authorization']
|
||||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
# expose_headers: ['Link']
|
|
||||||
max_age: 3600
|
max_age: 3600
|
||||||
|
|
||||||
when@dev:
|
|
||||||
nelmio_cors:
|
|
||||||
paths:
|
|
||||||
'^/api/':
|
|
||||||
allow_private_network: true
|
|
||||||
|
|||||||
@@ -5,17 +5,3 @@ framework:
|
|||||||
limit: 250
|
limit: 250
|
||||||
interval: '1 hour'
|
interval: '1 hour'
|
||||||
lock_factory: null
|
lock_factory: null
|
||||||
reset_password:
|
|
||||||
policy: 'fixed_window'
|
|
||||||
limit: 10
|
|
||||||
interval: '1 hour'
|
|
||||||
lock_factory: null
|
|
||||||
|
|
||||||
when@test:
|
|
||||||
framework:
|
|
||||||
rate_limiter:
|
|
||||||
reset_password:
|
|
||||||
policy: 'fixed_window'
|
|
||||||
limit: 100
|
|
||||||
interval: '1 minute'
|
|
||||||
lock_factory: null
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ security:
|
|||||||
- 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: /
|
||||||
@@ -82,21 +81,21 @@ security:
|
|||||||
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
|
||||||
|
|
||||||
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 }
|
||||||
- {path: '^/{_locale}$', role: PUBLIC_ACCESS}
|
- { path: '^/{_locale}$', role: PUBLIC_ACCESS }
|
||||||
- {path: '^/{_locale}/auth', role: PUBLIC_ACCESS}
|
- { path: '^/{_locale}/auth', role: PUBLIC_ACCESS }
|
||||||
- {path: '^/{_locale}/login', role: PUBLIC_ACCESS}
|
- { path: '^/{_locale}/login', role: PUBLIC_ACCESS }
|
||||||
- {path: '^/{_locale}/register', role: PUBLIC_ACCESS}
|
- { path: '^/{_locale}/register', role: PUBLIC_ACCESS }
|
||||||
- {path: '^/{_locale}/resetting', role: PUBLIC_ACCESS}
|
- { path: '^/{_locale}/resetting', role: PUBLIC_ACCESS }
|
||||||
- {path: '^/{_locale}/', roles: ROLE_USER}
|
- { path: '^/{_locale}/', roles: ROLE_USER }
|
||||||
- {path: '^/api', roles: IS_AUTHENTICATED}
|
- { path: '^/api', roles: IS_AUTHENTICATED }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
# this configuration simplifies testing URLs protected by the security mechanism
|
# this configuration simplifies testing URLs protected by the security mechanism
|
||||||
@@ -106,6 +105,6 @@ when@test:
|
|||||||
App\Entity\User:
|
App\Entity\User:
|
||||||
algorithm: auto
|
algorithm: auto
|
||||||
# see https://github.com/symfony/recipes/pull/1026
|
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,5 +5,7 @@ twig:
|
|||||||
- 'form/vertical.html.twig'
|
- 'form/vertical.html.twig'
|
||||||
exception_controller: null
|
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
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ controllers:
|
|||||||
api.swagger_ui:
|
api.swagger_ui:
|
||||||
path: /api/doc
|
path: /api/doc
|
||||||
methods: GET
|
methods: GET
|
||||||
defaults: { _controller: nelmio_api_doc.controller.stoplight }
|
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
|
||||||
|
|
||||||
|
api.swagger:
|
||||||
|
path: /api/doc.json
|
||||||
|
methods: GET
|
||||||
|
defaults: { _controller: nelmio_api_doc.controller.swagger }
|
||||||
|
|
||||||
api:
|
api:
|
||||||
resource: ../src/API/
|
resource: ../src/API/
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#webhook:
|
|
||||||
# resource: '@FrameworkBundle/Resources/config/routing/webhook.xml'
|
|
||||||
# prefix: /webhook
|
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
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
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
autoconfigure: true
|
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%'
|
||||||
@@ -20,11 +22,8 @@ services:
|
|||||||
exclude:
|
exclude:
|
||||||
- '../src/API/Model/'
|
- '../src/API/Model/'
|
||||||
- '../src/DependencyInjection/'
|
- '../src/DependencyInjection/'
|
||||||
- '../src/Doctrine/Extensions/'
|
|
||||||
- '../src/Entity/'
|
- '../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/'
|
||||||
@@ -86,9 +85,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
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
@@ -115,104 +111,99 @@ services:
|
|||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
|
||||||
App\Repository\TimesheetRepository:
|
App\Repository\TimesheetRepository:
|
||||||
class: App\Repository\TimesheetRepository
|
class: App\Repository\TimesheetRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Timesheet']
|
arguments: ['App\Entity\Timesheet']
|
||||||
|
|
||||||
App\Repository\UserRepository:
|
App\Repository\UserRepository:
|
||||||
class: App\Repository\UserRepository
|
class: App\Repository\UserRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\User']
|
arguments: ['App\Entity\User']
|
||||||
|
|
||||||
App\Repository\TeamRepository:
|
App\Repository\TeamRepository:
|
||||||
class: App\Repository\TeamRepository
|
class: App\Repository\TeamRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Team']
|
arguments: ['App\Entity\Team']
|
||||||
|
|
||||||
App\Repository\ActivityRepository:
|
App\Repository\ActivityRepository:
|
||||||
class: App\Repository\ActivityRepository
|
class: App\Repository\ActivityRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Activity']
|
arguments: ['App\Entity\Activity']
|
||||||
|
|
||||||
App\Repository\ProjectRepository:
|
App\Repository\ProjectRepository:
|
||||||
class: App\Repository\ProjectRepository
|
class: App\Repository\ProjectRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Project']
|
arguments: ['App\Entity\Project']
|
||||||
|
|
||||||
App\Repository\TagRepository:
|
App\Repository\TagRepository:
|
||||||
class: App\Repository\TagRepository
|
class: App\Repository\TagRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Tag']
|
arguments: ['App\Entity\Tag']
|
||||||
|
|
||||||
App\Repository\CustomerRepository:
|
App\Repository\CustomerRepository:
|
||||||
class: App\Repository\CustomerRepository
|
class: App\Repository\CustomerRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Customer']
|
arguments: ['App\Entity\Customer']
|
||||||
|
|
||||||
App\Repository\InvoiceTemplateRepository:
|
App\Repository\InvoiceTemplateRepository:
|
||||||
class: App\Repository\InvoiceTemplateRepository
|
class: App\Repository\InvoiceTemplateRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\InvoiceTemplate']
|
arguments: ['App\Entity\InvoiceTemplate']
|
||||||
|
|
||||||
App\Repository\ConfigurationRepository:
|
App\Repository\ConfigurationRepository:
|
||||||
class: App\Repository\ConfigurationRepository
|
class: App\Repository\ConfigurationRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Configuration']
|
arguments: ['App\Entity\Configuration']
|
||||||
|
|
||||||
App\Repository\RoleRepository:
|
App\Repository\RoleRepository:
|
||||||
class: App\Repository\RoleRepository
|
class: App\Repository\RoleRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Role']
|
arguments: ['App\Entity\Role']
|
||||||
|
|
||||||
App\Repository\RolePermissionRepository:
|
App\Repository\RolePermissionRepository:
|
||||||
class: App\Repository\RolePermissionRepository
|
class: App\Repository\RolePermissionRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\RolePermission']
|
arguments: ['App\Entity\RolePermission']
|
||||||
|
|
||||||
App\Repository\InvoiceDocumentRepository:
|
App\Repository\InvoiceDocumentRepository:
|
||||||
class: App\Repository\InvoiceDocumentRepository
|
class: App\Repository\InvoiceDocumentRepository
|
||||||
arguments: ['%kimai.invoice.documents%']
|
arguments: ['%kimai.invoice.documents%']
|
||||||
|
|
||||||
App\Repository\CustomerRateRepository:
|
App\Repository\CustomerRateRepository:
|
||||||
class: App\Repository\CustomerRateRepository
|
class: App\Repository\CustomerRateRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\CustomerRate']
|
arguments: ['App\Entity\CustomerRate']
|
||||||
|
|
||||||
App\Repository\ActivityRateRepository:
|
App\Repository\ActivityRateRepository:
|
||||||
class: App\Repository\ActivityRateRepository
|
class: App\Repository\ActivityRateRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\ActivityRate']
|
arguments: ['App\Entity\ActivityRate']
|
||||||
|
|
||||||
App\Repository\ProjectRateRepository:
|
App\Repository\ProjectRateRepository:
|
||||||
class: App\Repository\ProjectRateRepository
|
class: App\Repository\ProjectRateRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\ProjectRate']
|
arguments: ['App\Entity\ProjectRate']
|
||||||
|
|
||||||
App\Repository\InvoiceRepository:
|
App\Repository\InvoiceRepository:
|
||||||
class: App\Repository\InvoiceRepository
|
class: App\Repository\InvoiceRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Invoice']
|
arguments: ['App\Entity\Invoice']
|
||||||
|
|
||||||
App\Repository\BookmarkRepository:
|
App\Repository\BookmarkRepository:
|
||||||
class: App\Repository\BookmarkRepository
|
class: App\Repository\BookmarkRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\Bookmark']
|
arguments: ['App\Entity\Bookmark']
|
||||||
|
|
||||||
App\Repository\WorkingTimeRepository:
|
App\Repository\WorkingTimeRepository:
|
||||||
class: App\Repository\WorkingTimeRepository
|
class: App\Repository\WorkingTimeRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\WorkingTime']
|
arguments: ['App\Entity\WorkingTime']
|
||||||
|
|
||||||
App\Repository\AccessTokenRepository:
|
App\Repository\AccessTokenRepository:
|
||||||
class: App\Repository\AccessTokenRepository
|
class: App\Repository\AccessTokenRepository
|
||||||
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:
|
||||||
@@ -224,6 +215,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']
|
|
||||||
|
|||||||
@@ -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
117
kimai.sh
@@ -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 ""
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -48,4 +48,9 @@ final class Version20240214061246 extends AbstractMigration
|
|||||||
|
|
||||||
$schema->dropTable('kimai2_access_token');
|
$schema->dropTable('kimai2_access_token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isTransactional(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Kimai time-tracking app.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use App\Doctrine\AbstractMigration;
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @version 2.23
|
|
||||||
*/
|
|
||||||
final class Version20240926111739 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Adds created_at and break duration column';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE kimai2_activities ADD created_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_customers ADD created_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_projects ADD created_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_timesheet ADD break INT DEFAULT NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE kimai2_timesheet DROP break');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_projects DROP created_at');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_customers DROP created_at');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_activities DROP created_at');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Kimai time-tracking app.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use App\Doctrine\AbstractMigration;
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @version 2.36.0
|
|
||||||
*/
|
|
||||||
final class Version20250608143244 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Create the export template table';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$table = $schema->createTable('kimai2_export_templates');
|
|
||||||
|
|
||||||
$table->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
|
||||||
$table->addColumn('title', 'string', ['notnull' => true, 'length' => 100]);
|
|
||||||
$table->addColumn('renderer', 'string', ['notnull' => true, 'length' => 20]);
|
|
||||||
$table->addColumn('language', 'string', ['notnull' => false, 'length' => 6]);
|
|
||||||
$table->addColumn('columns', 'json', ['notnull' => true]);
|
|
||||||
$table->addColumn('options', 'json', ['notnull' => true]);
|
|
||||||
|
|
||||||
$table->setPrimaryKey(['id']);
|
|
||||||
$table->addUniqueIndex(['title'], 'UNIQ_2F0CA26F2B36786B');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
if ($schema->hasTable('kimai2_export_templates')) {
|
|
||||||
$schema->dropTable('kimai2_export_templates');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isTransactional(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Kimai time-tracking app.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use App\Doctrine\AbstractMigration;
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @version 2.41
|
|
||||||
*/
|
|
||||||
final class Version20251031142000 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Add dedicated address fields for customer and customer-field for invoice template';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE kimai2_customers ADD address_line1 VARCHAR(150) DEFAULT NULL, ADD address_line2 VARCHAR(150) DEFAULT NULL, ADD address_line3 VARCHAR(150) DEFAULT NULL, ADD postcode VARCHAR(20) DEFAULT NULL, ADD city VARCHAR(50) DEFAULT NULL, ADD buyer_reference VARCHAR(50) DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_invoice_templates ADD customer_id INT DEFAULT NULL');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_invoice_templates MODIFY company VARCHAR(255) NULL');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_invoice_templates ADD CONSTRAINT FK_1626CFE99395C3F3 FOREIGN KEY (customer_id) REFERENCES kimai2_customers (id) ON DELETE SET NULL');
|
|
||||||
$this->addSql('CREATE INDEX IDX_1626CFE99395C3F3 ON kimai2_invoice_templates (customer_id)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE kimai2_invoice_templates DROP FOREIGN KEY FK_1626CFE99395C3F3');
|
|
||||||
$this->addSql('DROP INDEX IDX_1626CFE99395C3F3 ON kimai2_invoice_templates');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_invoice_templates DROP customer_id');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_customers DROP address_line1, DROP address_line2, DROP address_line3, DROP postcode, DROP city, DROP buyer_reference');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isTransactional(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Kimai time-tracking app.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use App\Doctrine\AbstractMigration;
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @version 2.41
|
|
||||||
*/
|
|
||||||
final class Version20251031143000 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Create the invoice template meta table';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TABLE kimai2_invoice_templates_meta (id INT AUTO_INCREMENT NOT NULL, template_id INT NOT NULL, name VARCHAR(50) NOT NULL, value TEXT DEFAULT NULL, visible TINYINT(1) DEFAULT 0 NOT NULL, INDEX IDX_A165B0555DA0FB8 (template_id), UNIQUE INDEX UNIQ_A165B0555DA0FB85E237E06 (template_id, name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
|
||||||
$this->addSql('ALTER TABLE kimai2_invoice_templates_meta ADD CONSTRAINT FK_A165B0555DA0FB8 FOREIGN KEY (template_id) REFERENCES kimai2_invoice_templates (id) ON DELETE CASCADE');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$table = $schema->getTable('kimai2_invoice_templates_meta');
|
|
||||||
$table->removeForeignKey('FK_A165B0555DA0FB8');
|
|
||||||
|
|
||||||
$schema->dropTable('kimai2_invoice_templates_meta');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isTransactional(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Kimai time-tracking app.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use App\Doctrine\AbstractMigration;
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @version 2.45
|
|
||||||
*/
|
|
||||||
final class Version20251214160001 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Add index for a query called on every timesheet page';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$table = $schema->getTable('kimai2_tags');
|
|
||||||
|
|
||||||
if (!$table->hasIndex('IDX_27CAF54C7AB0E859')) {
|
|
||||||
// used to count the tags for the dropdown (filter and timesheet edit)
|
|
||||||
$table->addIndex(['visible'], 'IDX_27CAF54C7AB0E859');
|
|
||||||
}
|
|
||||||
// deleted in this release
|
|
||||||
$this->addSql("DELETE from kimai2_configuration where name = 'defaults.user.language'");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$table = $schema->getTable('kimai2_tags');
|
|
||||||
|
|
||||||
if ($table->hasIndex('IDX_27CAF54C7AB0E859')) {
|
|
||||||
$table->dropIndex('IDX_27CAF54C7AB0E859');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isTransactional(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
75
package.json
75
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kimai",
|
"name": "kimai2",
|
||||||
"homepage": "https://github.com/kimai/kimai",
|
"homepage": "https://github.com/kimai/kimai",
|
||||||
"license": "AGPL",
|
"license": "MIT",
|
||||||
"maintainers": [
|
"maintainers": [
|
||||||
{
|
{
|
||||||
"name": "Kevin Papst",
|
"name": "Kevin Papst",
|
||||||
@@ -13,46 +13,41 @@
|
|||||||
"url": "git+https://github.com/kimai/kimai.git"
|
"url": "git+https://github.com/kimai/kimai.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev-server": "yarn encore dev-server",
|
"dev-server": "./node_modules/.bin/encore dev-server",
|
||||||
"dev": "yarn encore dev",
|
"dev": "./node_modules/.bin/encore dev",
|
||||||
"watch": "yarn encore dev --watch",
|
"watch": "./node_modules/.bin/encore dev --watch",
|
||||||
"lint": "yarn eslint assets/js/",
|
"build": "./node_modules/.bin/encore production",
|
||||||
"build": "yarn encore production"
|
"prod": "node_modules/.bin/encore production"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults"
|
"defaults"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"packageManager": "yarn@3.2.1",
|
||||||
"@babel/core": "^7",
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7",
|
"@babel/core": "^7.19",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7",
|
"@babel/eslint-parser": "^7.19",
|
||||||
"@babel/preset-env": "^7",
|
"@babel/preset-env": "^7.19",
|
||||||
"@eslint/js": "^9",
|
"@fortawesome/fontawesome-free": "^6.5",
|
||||||
"@fortawesome/fontawesome-free": "^6",
|
"@fullcalendar/bootstrap5": "^5.11",
|
||||||
"@fullcalendar/bootstrap5": "^5",
|
"@fullcalendar/core": "^5.11",
|
||||||
"@fullcalendar/core": "^5",
|
"@fullcalendar/daygrid": "^5.11",
|
||||||
"@fullcalendar/daygrid": "^5",
|
"@fullcalendar/google-calendar": "^5.11",
|
||||||
"@fullcalendar/google-calendar": "^5",
|
"@fullcalendar/icalendar": "^5.11",
|
||||||
"@fullcalendar/icalendar": "^5",
|
"@fullcalendar/interaction": "^5.11",
|
||||||
"@fullcalendar/interaction": "^5",
|
"@fullcalendar/timegrid": "^5.11",
|
||||||
"@fullcalendar/timegrid": "^5",
|
"@symfony/webpack-encore": "^4.0",
|
||||||
"@popperjs/core": "^2",
|
"@tabler/core": "1.0.0-beta19",
|
||||||
"@symfony/webpack-encore": "^5",
|
"chart.js": "^4.0",
|
||||||
"@tabler/core": "1.4",
|
"core-js": "^3.6",
|
||||||
"bootstrap": "^5.3",
|
"eslint": "^8.0",
|
||||||
"chart.js": "^4",
|
"eslint-webpack-plugin": "^3.0",
|
||||||
"core-js": "^3",
|
"gridstack": "^7.1",
|
||||||
"eslint": "^9",
|
"litepicker": "^2.0",
|
||||||
"globals": "^15",
|
"luxon": "^3.3",
|
||||||
"gridstack": "^7",
|
"sass": "^1.62",
|
||||||
"highlight.js": "^11.11.1",
|
"sass-loader": "^13.2",
|
||||||
"litepicker": "^2",
|
"tom-select": "^2.2",
|
||||||
"luxon": "^3",
|
"webpack": "^5.91",
|
||||||
"sass": "^1",
|
"webpack-cli": "^4.10"
|
||||||
"sass-loader": "^16",
|
}
|
||||||
"tom-select": "^2.4.3",
|
|
||||||
"webpack": "^5",
|
|
||||||
"webpack-cli": "^5"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@4.5.3"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Helper script to analyze plugins with PHP Coding Standards Fixer
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# ./php-cs-fixer.sh [plugin] [params]
|
|
||||||
# [plugin] - name of the plugin to analyze (without "Bundle")
|
|
||||||
# [params] - additional parameters for PHPCsFixer, e.g. --dry-run
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
phpcsfixer() {
|
|
||||||
vendor/bin/php-cs-fixer fix --config var/plugins/$1Bundle/.php-cs-fixer.dist.php ${ARGS}
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ -n $2 ]]; then
|
|
||||||
export ARGS=$2
|
|
||||||
else
|
|
||||||
export ARGS=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n $1 ]]; then
|
|
||||||
if [ -d "var/plugins/$1Bundle/" ]; then
|
|
||||||
phpcsfixer $1
|
|
||||||
exit
|
|
||||||
elif [ "$1" == 'core' ]; then
|
|
||||||
vendor/bin/php-cs-fixer fix --config .php-cs-fixer.dist.php ${ARGS}
|
|
||||||
exit
|
|
||||||
else
|
|
||||||
echo "Plugin $1 not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
vendor/bin/php-cs-fixer fix --config .php-cs-fixer.dist.php ${ARGS}
|
|
||||||
|
|
||||||
for dir in var/plugins/*Bundle/
|
|
||||||
do
|
|
||||||
echo ""
|
|
||||||
vendor/bin/php-cs-fixer fix --config ${dir}.php-cs-fixer.dist.php ${ARGS}
|
|
||||||
done
|
|
||||||
919
phpstan.neon
919
phpstan.neon
File diff suppressed because it is too large
Load Diff
48
phpstan.sh
48
phpstan.sh
@@ -1,48 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Helper script to analyze plugins with PHPStan
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# ./phpstan.sh [plugin] [params]
|
|
||||||
# [plugin] - name of the plugin to analyze (without "Bundle")
|
|
||||||
# [params] - additional parameters for PHPStan, e.g. --level=7 --pro
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
phpstan() {
|
|
||||||
vendor/bin/phpstan analyse -c var/plugins/$1Bundle/phpstan.neon var/plugins/$1Bundle/ ${ARGS}
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ -n $2 ]]; then
|
|
||||||
export ARGS=$2
|
|
||||||
else
|
|
||||||
export ARGS=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n $1 ]]; then
|
|
||||||
if [ -d "var/plugins/$1Bundle/" ]; then
|
|
||||||
phpstan $1
|
|
||||||
exit
|
|
||||||
elif [ "$1" == 'core' ]; then
|
|
||||||
vendor/bin/phpstan analyse -c phpstan.neon ${ARGS}
|
|
||||||
exit
|
|
||||||
elif [ "$1" == 'test' ] || [ "$1" == 'tests' ]; then
|
|
||||||
vendor/bin/phpstan analyse -c tests/phpstan.neon ${ARGS}
|
|
||||||
exit
|
|
||||||
else
|
|
||||||
echo "Plugin $1 not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
vendor/bin/phpstan analyse -c phpstan.neon ${ARGS}
|
|
||||||
vendor/bin/phpstan analyse -c tests/phpstan.neon ${ARGS}
|
|
||||||
|
|
||||||
for dir in var/plugins/*Bundle/
|
|
||||||
do
|
|
||||||
echo ""
|
|
||||||
echo "=======> $dir <======="
|
|
||||||
echo ""
|
|
||||||
vendor/bin/phpstan analyse -c ${dir}phpstan.neon ${dir} ${ARGS}
|
|
||||||
done
|
|
||||||
132
phpunit.xml.dist
132
phpunit.xml.dist
@@ -1,75 +1,73 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||||
backupGlobals="false"
|
backupGlobals="false"
|
||||||
colors="true"
|
colors="true"
|
||||||
bootstrap="tests/bootstrap.php"
|
bootstrap="tests/bootstrap.php">
|
||||||
cacheDirectory="var/cache/phpunit/"
|
<php>
|
||||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
<ini name="error_reporting" value="-1"/>
|
||||||
displayDetailsOnTestsThatTriggerErrors="true"
|
<ini name="max_execution_time" value="-1"/>
|
||||||
displayDetailsOnTestsThatTriggerNotices="true"
|
<ini name="date.timezone" value="UTC"/>
|
||||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
<ini name="intl.default_locale" value="en_US"/>
|
||||||
displayDetailsOnPhpunitDeprecations="true">
|
<ini name="date.timezone" value="Europe/Vienna"/>
|
||||||
<php>
|
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
|
||||||
<ini name="error_reporting" value="-1"/>
|
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
||||||
<ini name="max_execution_time" value="-1"/>
|
<env name="APP_ENV" value="test" force="true"/>
|
||||||
<ini name="date.timezone" value="UTC"/>
|
<env name="APP_DEBUG" value="0" force="true"/>
|
||||||
<ini name="intl.default_locale" value="en_US"/>
|
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
||||||
<ini name="date.timezone" value="Europe/Vienna"/>
|
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?charset=utf8mb4&serverVersion=10.5.8-MariaDB"/>
|
||||||
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
|
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
||||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
|
<env name="MAILER_URL" value="null://null"/>
|
||||||
<env name="APP_ENV" value="test" force="true"/>
|
<env name="MAILER_FROM" value="kimai@example.com"/>
|
||||||
<env name="APP_DEBUG" value="0" force="true"/>
|
<!--
|
||||||
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
|
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
|
||||||
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test?charset=utf8mb4&serverVersion=10.5.8-MariaDB"/>
|
|
||||||
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
|
- change to "true"
|
||||||
<env name="MAILER_URL" value="null://null"/>
|
- composer tests
|
||||||
<env name="MAILER_FROM" value="kimai@example.com"/>
|
- change back to "false"
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
|
||||||
|
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
|
||||||
|
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
|
||||||
|
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
|
||||||
|
-->
|
||||||
|
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Kimai">
|
||||||
|
<directory>tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<coverage processUncoveredFiles="true">
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src/</directory>
|
||||||
|
<directory suffix=".php">templates/</directory>
|
||||||
|
</include>
|
||||||
|
<exclude>
|
||||||
|
<directory suffix=".php">migrations/</directory>
|
||||||
|
<directory suffix=".php">assets/</directory>
|
||||||
|
<directory suffix=".php">bin/</directory>
|
||||||
|
<directory suffix=".php">config/</directory>
|
||||||
|
<directory suffix=".php">node_modules/</directory>
|
||||||
|
<directory suffix=".php">public/</directory>
|
||||||
|
<directory suffix=".php">tests/</directory>
|
||||||
|
<directory suffix=".php">translations/</directory>
|
||||||
|
<directory suffix=".php">var/</directory>
|
||||||
|
<directory suffix=".php">vendor/</directory>
|
||||||
|
</exclude>
|
||||||
|
</coverage>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
|
wrap tests inside database transactions, so tests can safely manipulate contents without
|
||||||
|
affecting other tests. @see https://github.com/dmaicher/doctrine-test-bundle
|
||||||
- change to "true"
|
|
||||||
- composer tests
|
|
||||||
- change back to "false"
|
|
||||||
|
|
||||||
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
|
|
||||||
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
|
|
||||||
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
|
|
||||||
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
|
|
||||||
-->
|
-->
|
||||||
<env name="BOOTSTRAP_RESET_DATABASE" value="true"/>
|
<extensions>
|
||||||
|
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
|
||||||
|
</extensions>
|
||||||
|
|
||||||
<!-- ###+ symfony/messenger ### -->
|
<listeners>
|
||||||
<!-- Choose one of the transports below -->
|
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
|
||||||
<!-- MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages -->
|
</listeners>
|
||||||
<!-- MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages -->
|
|
||||||
<env name="MESSENGER_TRANSPORT_DSN" value="doctrine://default?auto_setup=0"/>
|
|
||||||
<!-- ###- symfony/messenger ### -->
|
|
||||||
</php>
|
|
||||||
<testsuites>
|
|
||||||
<testsuite name="Kimai">
|
|
||||||
<directory>tests/</directory>
|
|
||||||
</testsuite>
|
|
||||||
</testsuites>
|
|
||||||
<extensions>
|
|
||||||
<bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
|
|
||||||
</extensions>
|
|
||||||
<source>
|
|
||||||
<include>
|
|
||||||
<directory suffix=".php">src/</directory>
|
|
||||||
<directory suffix=".php">templates/</directory>
|
|
||||||
</include>
|
|
||||||
<exclude>
|
|
||||||
<directory suffix=".php">migrations/</directory>
|
|
||||||
<directory suffix=".php">assets/</directory>
|
|
||||||
<directory suffix=".php">bin/</directory>
|
|
||||||
<directory suffix=".php">config/</directory>
|
|
||||||
<directory suffix=".php">node_modules/</directory>
|
|
||||||
<directory suffix=".php">public/</directory>
|
|
||||||
<directory suffix=".php">tests/</directory>
|
|
||||||
<directory suffix=".php">translations/</directory>
|
|
||||||
<directory suffix=".php">var/</directory>
|
|
||||||
<directory suffix=".php">vendor/</directory>
|
|
||||||
</exclude>
|
|
||||||
</source>
|
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
(self.webpackChunkkimai=self.webpackChunkkimai||[]).push([[113],{876:function(i,n,u){"use strict";u.r(n)},2395:function(i,n,u){u(876)}},function(i){var n;n=2395,i(i.s=n)}]);
|
|
||||||
File diff suppressed because one or more lines are too long
5
public/build/app-rtl.83358c8a.css
Normal file
5
public/build/app-rtl.83358c8a.css
Normal file
File diff suppressed because one or more lines are too long
1
public/build/app-rtl.97153087.js
Normal file
1
public/build/app-rtl.97153087.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
(self.webpackChunkkimai2=self.webpackChunkkimai2||[]).push([[113],{2395:function(i,n,u){u(3865)},3865:function(i,n,u){"use strict";u.r(n)}},function(i){var n;n=2395,i(i.s=n)}]);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user