Compare commits

...

11 Commits

Author SHA1 Message Date
Kevin Papst
2d5cef1a2f phpstan 2024-10-12 00:00:46 +02:00
Kevin Papst
52a9ff9860 Merge branch 'refs/heads/main' into activity-report
# Conflicts:
#	config/packages/kimai.yaml
#	phpstan.neon
#	src/Activity/ActivityStatisticService.php
#	src/Repository/ActivityRepository.php
2024-10-11 23:45:40 +02:00
Kevin Papst
ff9bf163ee Release 2.24 (#5097) 2024-10-11 21:56:54 +02:00
Kevin Papst
5fac6a642c Docker improvements (#5096)
* install missing unzip
* run update and install in one step
* remove unused TRUSTED_HOSTS env
2024-10-11 19:36:40 +02:00
Kevin Papst
b1b81ee8a6 remove dev dependencies from prod image 2024-10-04 17:35:25 +02:00
Kevin Papst
1ea64bf18e allow to manually run workflow 2024-10-04 17:03:31 +02:00
Kevin Papst
0c4fdda00a allow to manually run workflow 2024-10-04 17:01:25 +02:00
Kevin Papst
215bd6a01a allow to manually run workflow 2024-10-04 16:57:48 +02:00
Kevin Papst
63b8164926 install missing buildx 2024-10-04 16:45:51 +02:00
Weblate (bot)
8c26ff9aed Translated using Weblate (#5092)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
2024-10-04 11:24:50 +02:00
Kevin Papst
ecded0a065 added activity overview report 2023-11-19 16:10:32 +01:00
112 changed files with 1361 additions and 690 deletions

View File

@@ -8,7 +8,6 @@
PassEnv DATABASE_URL
PassEnv MAILER_URL
PassEnv TRUSTED_PROXIES
PassEnv TRUSTED_HOSTS
<Directory "/opt/kimai/public">
Require all granted

View File

@@ -1,6 +1,11 @@
name: 'Docker Build'
on:
workflow_dispatch:
inputs:
kimai_tag:
description: 'Kimai tag to build'
required: true
release:
types: [released]
@@ -13,6 +18,9 @@ jobs:
- 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:
@@ -20,7 +28,24 @@ jobs:
password: ${{secrets.DOCKERHUB_PASSWORD}}
- name: Determine version
run: echo "kimai_version=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
run: |
input="${{ github.event.inputs.kimai_tag }}"
# Determine between manual trigger and release event
if [ -z "$input" ]; then
echo "Using release tag: ${{ github.event.release.tag_name }}"
version="${{ github.event.release.tag_name }}"
else
echo "Using tag provided: $input"
version="$input"
fi
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
echo "Invalid version number: $version"
exit 1
fi
echo "kimai_version=$version" >> $GITHUB_ENV
- name: FPM image
uses: docker/build-push-action@v5

View File

@@ -13,8 +13,8 @@
#
# 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
# docker exec -ti kimai-apache /bin/bash
# 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
@@ -78,8 +78,8 @@ RUN apk add --no-cache \
# apache debian php extension base
FROM php:8.3-apache-bookworm AS apache-php-ext-base
RUN apt-get update
RUN apt-get install -y \
RUN apt-get update && \
apt-get install -y \
libldap2-dev \
libicu-dev \
libpng-dev \
@@ -165,7 +165,8 @@ RUN apt-get update && \
libpng16-16 \
libzip4 \
libxslt1.1 \
libfreetype6 && \
libfreetype6 \
unzip && \
echo "Listen 8001" > /etc/apache2/ports.conf && \
a2enmod rewrite && \
touch /use_apache
@@ -258,13 +259,12 @@ RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} >
# copy startup script & DB checking script
COPY .docker/dbtest.php /dbtest.php
COPY .docker/startup.sh /startup.sh
COPY .docker/entrypoint.sh /entrypoint.sh
ENV DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=8.3"
ENV APP_SECRET=change_this_to_something_unique
# The default container name for nginx is nginx
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_URL=null://localhost
ENV ADMINPASS=
@@ -277,7 +277,7 @@ ENV COMPOSER_ALLOW_SUPERUSER=1
VOLUME [ "/opt/kimai/var" ]
CMD [ "/startup.sh" ]
CMD [ "/entrypoint.sh" ]
###########################
# final builds
@@ -315,7 +315,7 @@ RUN \
export COMPOSER_HOME=/composer && \
composer --no-ansi install --working-dir=/opt/kimai --no-dev --optimize-autoloader && \
composer --no-ansi clearcache && \
composer --no-ansi require --working-dir=/opt/kimai laminas/laminas-ldap && \
composer --no-ansi require --update-no-dev --working-dir=/opt/kimai laminas/laminas-ldap && \
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/;opcache.enable=1/opcache.enable=1/g" /usr/local/etc/php/php.ini && \

View File

@@ -236,8 +236,13 @@ export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
options.push(optGroup);
}
// log the one with a group name first (e.g. non-global activities)
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)
node.value = selectedValue;

View File

@@ -41,4 +41,11 @@ fieldset:empty {
.nav-pills .nav-link.active {
border-width: 1px;
border-style: solid;
}
}
[data-bs-theme=dark] {
/* fixes https://github.com/tabler/tabler/issues/1974 */
.litepicker .container__days .day-item.is-in-range {
--litepicker-is-in-range-color: var(--tblr-primary-text-emphasis);
}
}

View File

@@ -104,7 +104,7 @@ kimai:
BILLABLE: ['edit_billable_own_timesheet','edit_billable_other_timesheet']
TEAMS: ['view_team','create_team','edit_team','delete_team']
LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet']
REPORTING: ['view_reporting','view_other_reporting','project_reporting','customer_reporting']
REPORTING: ['view_reporting','view_other_reporting','activity_reporting','project_reporting','customer_reporting']
EVERYONE: ['api_access','hours_own_profile']
# permissions which are deactivated, as these features are hidden for now
# brave users can try to activate them and be surprised what happens

View File

@@ -469,36 +469,6 @@ parameters:
count: 1
path: src/Command/ResetTestCommand.php
-
message: "#^Strict comparison using \\!\\=\\= between '\\$2y\\$13\\$X8…' and null will always evaluate to true\\.$#"
count: 1
path: src/Command/ResetTestCommand.php
-
message: "#^Strict comparison using \\!\\=\\= between 'Administrator'\\|'CFO'\\|'Developer'\\|'Developer \\(left…'\\|'Head of Development'\\|'Quality Tester 1'\\|'Quality Tester 2'\\|'Super Administrator' and null will always evaluate to true\\.$#"
count: 1
path: src/Command/ResetTestCommand.php
-
message: "#^Strict comparison using \\!\\=\\= between 'anna_admin'\\|'chris_user'\\|'clara_customer'\\|'john_user'\\|'susan_super'\\|'test_user_1'\\|'test_user_2'\\|'tony_teamlead' and null will always evaluate to true\\.$#"
count: 1
path: src/Command/ResetTestCommand.php
-
message: "#^Strict comparison using \\!\\=\\= between 'anna_admin@example…'\\|'chris_user@example…'\\|'clara_customer…'\\|'john_user@example…'\\|'susan_super@example…'\\|'test_user_1@example…'\\|'test_user_2@example…'\\|'tony_teamlead…' and null will always evaluate to true\\.$#"
count: 2
path: src/Command/ResetTestCommand.php
-
message: "#^Strict comparison using \\!\\=\\= between 0\\|1 and null will always evaluate to true\\.$#"
count: 1
path: src/Command/ResetTestCommand.php
-
message: "#^Strict comparison using \\!\\=\\= between array\\{\\}\\|array\\{'ROLE_ADMIN'\\}\\|array\\{'ROLE_CUSTOMER'\\}\\|array\\{'ROLE_SUPER_ADMIN'\\}\\|array\\{'ROLE_TEAMLEAD'\\} and null will always evaluate to true\\.$#"
count: 1
path: src/Command/ResetTestCommand.php
-
message: "#^Access to an undefined property SimpleXMLElement\\|false\\:\\:\\$file\\.$#"
count: 1
@@ -1114,16 +1084,6 @@ parameters:
count: 2
path: src/DataFixtures/TimesheetFixtures.php
-
message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:getUserDefinition\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/DataFixtures/UserFixtures.php
-
message: "#^Method App\\\\DataFixtures\\\\UserFixtures\\:\\:getUserPreferences\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/DataFixtures/UserFixtures.php
-
message: "#^Method App\\\\DependencyInjection\\\\AppExtension\\:\\:createPermissionParameter\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
count: 1

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,10 +3,10 @@
"app": {
"js": [
"/build/runtime.74179306.js",
"/build/app.3463e2a4.js"
"/build/app.6cdb49cb.js"
],
"css": [
"/build/app.75450933.css"
"/build/app.1ed18844.css"
]
},
"app-rtl": {
@@ -15,7 +15,7 @@
"/build/app-rtl.97153087.js"
],
"css": [
"/build/app-rtl.dbee41f9.css"
"/build/app-rtl.d38ad12c.css"
]
},
"export-pdf": {
@@ -72,10 +72,10 @@
},
"integrity": {
"/build/runtime.74179306.js": "sha384-OC1hTNUXUalKJcvmzrZ0TMCOIwnhxCxgG9dQkbcwR7WcBBCkl2H8bs3giiT2pAwG",
"/build/app.3463e2a4.js": "sha384-8Kq7fZHUdtxWd3CVdPUapwwO8qjw3/N2mP4w14TczsDI3jQ3WlJZeUyL0WPZP8QD",
"/build/app.75450933.css": "sha384-tW+c6BIV2/3j9k6AiygV7qMVFqcsYG8dNY2CzCcIuMjOf3inAYoZlGvrV8VgfR3+",
"/build/app.6cdb49cb.js": "sha384-psXCV8SG68O80ZJHHeSbCJPQ8RoepaMeI1pEypTHu0Q8ySLrhlclhKHIL2dZ0ZQJ",
"/build/app.1ed18844.css": "sha384-fEjS/tjc8m7jrw28/eWZFOTO6bugR9ILfksKvBrF46wtmqlage1s/P4+ZeYuqgpq",
"/build/app-rtl.97153087.js": "sha384-jX7jRUAa8rH29Eg8jLIUKGfGcOT6RBz/P90plXmZPadf2CXKUBdcNGrspaejCHkr",
"/build/app-rtl.dbee41f9.css": "sha384-52I26CWbi5SCx6Qr+s+xsgmaVX2Ybd4TVVVoEMP87laXeLbu6jtJ/4ZGwo7YDUQ0",
"/build/app-rtl.d38ad12c.css": "sha384-eFvMRueGVAxDxpr3s4y1EhHnVb8FQOr+KQgOuTzbW11ILydpLIvObiiuqNpf4mWO",
"/build/export-pdf.1442bee7.js": "sha384-C6agvjJnQUsMCxaZ/J7dUZU3cpC7uHKI9ZtnfGpRqYbjyw91YLy1oV3iNYgwYyCF",
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
"/build/invoice.7ef8a0c8.js": "sha384-z4lZ1Ig3+NPigrRyGPZoff0gG3n5PnCjaDJ73ATnzdUYk0lOCEtNV9fg35VRD0vG",

View File

@@ -1,7 +1,7 @@
{
"build/app.css": "/build/app.75450933.css",
"build/app.js": "/build/app.3463e2a4.js",
"build/app-rtl.css": "/build/app-rtl.dbee41f9.css",
"build/app.css": "/build/app.1ed18844.css",
"build/app.js": "/build/app.6cdb49cb.js",
"build/app-rtl.css": "/build/app-rtl.d38ad12c.css",
"build/app-rtl.js": "/build/app-rtl.97153087.js",
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
"build/export-pdf.js": "/build/export-pdf.1442bee7.js",

View File

@@ -10,24 +10,34 @@
namespace App\Activity;
use App\Entity\Activity;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Event\ActivityBudgetStatisticEvent;
use App\Event\ActivityStatisticEvent;
use App\Model\ActivityBudgetStatisticModel;
use App\Model\ActivityStatistic;
use App\Reporting\ActivityView\ActivityViewModel;
use App\Reporting\ActivityView\ActivityViewQuery;
use App\Repository\ActivityRepository;
use App\Repository\Loader\ActivityLoader;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
/**
* @final
*/
class ActivityStatisticService
{
public function __construct(private readonly TimesheetRepository $timesheetRepository, private readonly EventDispatcherInterface $dispatcher)
public function __construct(
private readonly TimesheetRepository $timesheetRepository,
private readonly ActivityRepository $activityRepository,
private readonly EventDispatcherInterface $dispatcher
)
{
}
@@ -197,4 +207,201 @@ class ActivityStatisticService
return $qb;
}
/**
* @return Activity[]
*/
public function findActivitiesForView(ActivityViewQuery $query): array
{
$user = $query->getUser();
$today = clone $query->getToday();
$qb = $this->activityRepository->createQueryBuilder('a');
$qb
->select('a')
->leftJoin('a.project', 'p')
->leftJoin('p.customer', 'c')
->andWhere($qb->expr()->eq('a.visible', true))
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('a.project'),
$qb->expr()->andX(
$qb->expr()->eq('p.visible', true),
$qb->expr()->eq('c.visible', true),
$qb->expr()->orX(
$qb->expr()->isNull('p.end'),
$qb->expr()->gte('p.end', ':project_end')
)
)
)
)
->addGroupBy('a')
->setParameter('project_end', $today, Types::DATETIME_MUTABLE)
;
if ($query->getProject() !== null) {
$qb->andWhere($qb->expr()->eq('p', ':project'));
$qb->setParameter('project', $query->getProject()->getId());
}
if (!$query->isIncludeNoWork()) {
$qb
->leftJoin(Timesheet::class, 't', 'WITH', 'a.id = t.activity')
->andHaving($qb->expr()->gt('SUM(t.duration)', 0))
;
}
if ($query->isIncludeWithBudget()) {
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->gt('a.timeBudget', 0),
$qb->expr()->gt('a.budget', 0)
)
);
} elseif ($query->isIncludeWithoutBudget()) {
$qb->andWhere(
$qb->expr()->andX(
$qb->expr()->eq('a.timeBudget', 0),
$qb->expr()->eq('a.budget', 0)
)
);
}
$this->activityRepository->addPermissionCriteria($qb, $user);
/** @var Activity[] $activities */
$activities = $qb->getQuery()->getResult();
// pre-cache project objects instead of joining them
$loader = new ActivityLoader($this->activityRepository->createQueryBuilder('a')->getEntityManager());
$loader->loadResults($activities);
return $activities;
}
/**
* @param Activity[] $activities
* @return ActivityViewModel[]
*/
public function getActivityView(User $user, array $activities, DateTimeInterface $today): array
{
$factory = DateTimeFactory::createByUser($user);
$today = clone $today;
$startOfWeek = $factory->getStartOfWeek($today);
$endOfWeek = $factory->getEndOfWeek($today);
$startMonth = (clone $startOfWeek)->modify('first day of this month');
$endMonth = (clone $startOfWeek)->modify('last day of this month');
$activityView = [];
foreach ($activities as $activity) {
$activityView[$activity->getId()] = new ActivityViewModel($activity);
}
$budgetStats = $this->getBudgetStatisticModelForActivities($activities, $today);
foreach ($budgetStats as $model) {
$activityView[$model->getActivity()->getId()]->setBudgetStatisticModel($model);
}
$activityIds = array_keys($activityView);
$tplQb = $this->timesheetRepository->createQueryBuilder('t');
$tplQb
->select('IDENTITY(t.activity) AS id')
->addSelect('COUNT(t.id) as amount')
->addSelect('COALESCE(SUM(t.duration), 0) AS duration')
->addSelect('COALESCE(SUM(t.rate), 0) AS rate')
->andWhere($tplQb->expr()->in('t.activity', ':activity'))
->groupBy('t.activity')
->setParameter('activity', $activityIds)
;
$qb = clone $tplQb;
$qb->addSelect('MAX(t.date) as lastRecord');
/** @var array<int, array{id: int, amount: int, duration: int, rate: float, lastRecord: string}> $result */
$result = $qb->getQuery()->getScalarResult();
foreach ($result as $row) {
$activityView[$row['id']]->setDurationTotal($row['duration']);
$activityView[$row['id']]->setRateTotal($row['rate']);
$activityView[$row['id']]->setTimesheetCounter($row['amount']);
if ($row['lastRecord'] !== null) {
// might be the wrong timezone
$activityView[$row['id']]->setLastRecord($factory->createDateTime($row['lastRecord']));
}
}
// values for today
$qb = clone $tplQb;
$qb
->andWhere('DATE(t.date) = :start_date')
->setParameter('start_date', $today, Types::DATETIME_MUTABLE)
;
/** @var array<int, array{id: int, amount: int, duration: int, rate: float}> $result */
$result = $qb->getQuery()->getScalarResult();
foreach ($result as $row) {
$activityView[$row['id']]->setDurationDay($row['duration'] ?? 0);
}
// values for the current week
$qb = clone $tplQb;
$qb
->andWhere('DATE(t.date) BETWEEN :start_date AND :end_date')
->setParameter('start_date', $startOfWeek, Types::DATETIME_MUTABLE)
->setParameter('end_date', $endOfWeek, Types::DATETIME_MUTABLE)
;
/** @var array<int, array{id: int, amount: int, duration: int, rate: float}> $result */
$result = $qb->getQuery()->getScalarResult();
foreach ($result as $row) {
$activityView[$row['id']]->setDurationWeek($row['duration']);
}
// values for the current month
$qb = clone $tplQb;
$qb
->andWhere('DATE(t.date) BETWEEN :start_date AND :end_date')
->setParameter('start_date', $startMonth, Types::DATETIME_MUTABLE)
->setParameter('end_date', $endMonth, Types::DATETIME_MUTABLE)
;
/** @var array<int, array{id: int, amount: int, duration: int, rate: float}> $result */
$result = $qb->getQuery()->getScalarResult();
foreach ($result as $row) {
$activityView[$row['id']]->setDurationMonth($row['duration']);
}
$qb = clone $tplQb;
$qb
->addSelect('t.exported')
->addSelect('t.billable')
->addGroupBy('t.exported')
->addGroupBy('t.billable')
;
/** @var array<int, array{id: int, amount: int, duration: int, rate: float, exported: bool, billable: bool}> $result */
$result = $qb->getQuery()->getScalarResult();
foreach ($result as $row) {
/** @var ActivityViewModel $view */
$view = $activityView[$row['id']];
if ($row['billable'] === 1 && $row['exported'] === 1) {
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
$view->setBillableRate($view->getBillableRate() + $row['rate']);
} elseif ($row['billable'] === 1 && $row['exported'] === 0) {
$view->setBillableDuration($view->getBillableDuration() + $row['duration']);
$view->setBillableRate($view->getBillableRate() + $row['rate']);
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
$view->setNotBilledDuration($view->getNotBilledDuration() + $row['duration']);
$view->setNotBilledRate($view->getNotBilledRate() + $row['rate']);
} elseif ($row['billable'] === 0 && $row['exported'] === 0) {
$view->setNotExportedDuration($view->getNotExportedDuration() + $row['duration']);
$view->setNotExportedRate($view->getNotExportedRate() + $row['rate']);
}
// the last possible case $row['billable'] === 0 && $row['exported'] === 1 is extremely unlikely and not used
}
return array_values($activityView);
}
}

View File

@@ -26,7 +26,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
*/
abstract class AbstractResetCommand extends Command
{
public function __construct(private string $kernelEnvironment)
public function __construct(private readonly string $kernelEnvironment)
{
parent::__construct();
}

View File

@@ -25,12 +25,13 @@ use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
/**
* Command used to execute all the basic application bootstrapping AFTER "composer install" was executed.
*
* This command is NOT used during runtime and only meant for developers and the CI processes for quality management.
* This is one of the cases where I don't feel like it is necessary to add tests, so lets "cheat" with:
* This is one of the cases where it is necessary to add tests:
* @codeCoverageIgnore
*/
#[AsCommand(name: 'kimai:reset:test', description: 'Resets the "test" environment')]
@@ -38,6 +39,7 @@ final class ResetTestCommand extends AbstractResetCommand
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UserPasswordHasherInterface $passwordHasher,
string $kernelEnvironment
)
{
@@ -80,227 +82,127 @@ final class ResetTestCommand extends AbstractResetCommand
$project->setCustomer($customer);
$this->entityManager->persist($project);
$user1 = new User();
$user1->setPreferenceValue(UserPreference::HOURLY_RATE, 53);
$user1->setAlias('Clara Haynes');
$user1->setRegisteredAt(new \DateTime('2018-02-06 23:28:57'));
$user1->setTitle('CFO');
$user1->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y');
$user1->setEnabled(true);
$user1->setRoles(['ROLE_CUSTOMER']);
$user1->setUserIdentifier('clara_customer');
$user1->setEmail('clara_customer@example.com');
$token1 = new AccessToken($user1, UserFixtures::DEFAULT_API_TOKEN . '_customer');
$token1->setName('Test fixture');
$user2 = new User();
$user2->setPreferenceValue(UserPreference::HOURLY_RATE, 82);
$user2->setAlias('John Doe');
$user2->setRegisteredAt(new \DateTime('2018-02-06 23:28:57'));
$user2->setTitle('Developer');
$user2->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y');
$user2->setEnabled(true);
$user2->setRoles(['ROLE_USER']);
$user2->setUserIdentifier('john_user');
$user2->setEmail('john_user@example.com');
$token2 = new AccessToken($user2, UserFixtures::DEFAULT_API_TOKEN . '_user');
$token2->setName('Test fixture');
$user3 = new User();
$user3->setPreferenceValue(UserPreference::HOURLY_RATE, 35);
$user3->setAlias('Chris Deactive');
$user3->setRegisteredAt(new \DateTime('2018-02-06 23:28:57'));
$user3->setTitle('Developer (left company)');
$user3->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y');
$user3->setEnabled(false);
$user3->setRoles(['ROLE_USER']);
$user3->setUserIdentifier('chris_user');
$user3->setEmail('chris_user@example.com');
$token3 = new AccessToken($user3, UserFixtures::DEFAULT_API_TOKEN . '_inactive');
$token3->setName('Test fixture');
$user4 = new User();
$user4->setPreferenceValue(UserPreference::HOURLY_RATE, 35);
$user4->setAlias('Tony Maier');
$user4->setRegisteredAt(new \DateTime('2018-02-06 23:28:57'));
$user4->setTitle('Head of Development');
$user4->setAvatar('https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg');
$user4->setEnabled(true);
$user4->setRoles(['ROLE_TEAMLEAD']);
$user4->setUserIdentifier('tony_teamlead');
$user4->setEmail('tony_teamlead@example.com');
$token4 = new AccessToken($user4, UserFixtures::DEFAULT_API_TOKEN . '_teamlead');
$token4->setName('Test fixture');
$user5 = new User();
$user5->setPreferenceValue(UserPreference::HOURLY_RATE, 81);
$user5->setAlias('Anna Smith');
$user5->setRegisteredAt(new \DateTime('2018-02-06 23:28:57'));
$user5->setTitle('Administrator');
$user5->setEnabled(true);
$user5->setRoles(['ROLE_ADMIN']);
$user5->setUserIdentifier('anna_admin');
$user5->setEmail('anna_admin@example.com');
$token5 = new AccessToken($user5, UserFixtures::DEFAULT_API_TOKEN . '_admin');
$token5->setName('Test fixture');
$user6 = new User();
$user6->setPreferenceValue(UserPreference::HOURLY_RATE, 46);
$user6->setRegisteredAt(new \DateTime('2018-02-06 23:28:57'));
$user6->setTitle('Super Administrator');
$user6->setAvatar('/bundles/avanzuadmintheme/img/avatar.png');
$user6->setEnabled(true);
$user6->setRoles(['ROLE_SUPER_ADMIN']);
$user6->setUserIdentifier('susan_super');
$user6->setEmail('susan_super@example.com');
$token6 = new AccessToken($user6, UserFixtures::DEFAULT_API_TOKEN . '_super');
$token6->setName('Test fixture');
$user7 = new User();
$user7->setAlias('Test User 1');
$user7->setTitle('Quality Tester 1');
$user7->setEnabled(true);
$user7->setRoles(['ROLE_USER']);
$user7->setUserIdentifier('test_user_1');
$user7->setEmail('test_user_1@example.com');
$token7 = new AccessToken($user7, UserFixtures::DEFAULT_API_TOKEN . '_qa1');
$token7->setName('Test fixture');
$user8 = new User();
$user8->setAlias('Test User 2');
$user8->setTitle('Quality Tester 2');
$user8->setEnabled(true);
$user8->setRoles(['ROLE_USER']);
$user8->setUserIdentifier('test_user_2');
$user8->setEmail('test_user_2@example.com');
$token8 = new AccessToken($user8, UserFixtures::DEFAULT_API_TOKEN . '_qa2');
$token8->setName('Test fixture');
/** @var array<int, array{0: User, 1: AccessToken}> $users */
$users = [
// 0=id, 1=hourly rate, 2=Alias, 3=registration date, 4=title, 5=avatar, 6=enabled, 7=password, 8=roles, 9=username, 10=username canonical, 11=email, 12=email canonical, 13=salt, 14=last login, 15=confirmation token, 16=password requested at, 17=api_token
[
1,
53,
'Clara Haynes',
'2018-02-06 23:28:57',
'CFO',
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y',
1,
'$2y$04$kKBYJ8sKCOhhakCjm9sCp.TQdwLTS1FPkPiWn2KBmaCA7xFL0NA42',
['ROLE_CUSTOMER'],
'clara_customer',
'clara_customer',
'clara_customer@example.com',
'clara_customer@example.com',
null,
null,
null,
null,
'$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO',
UserFixtures::DEFAULT_API_TOKEN . '_customer',
],
[
2,
82,
'John Doe',
'2018-02-06 23:28:57',
'Developer',
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y',
1,
'$2y$04$36P/xyhP6FbnfFYbXy7V0.ioSe8HjMlJQFYnlIzz2T6Agfi8ob6jK',
[],
'john_user',
'john_user',
'john_user@example.com',
'john_user@example.com',
null,
null,
null,
null,
'$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO',
UserFixtures::DEFAULT_API_TOKEN . '_user',
],
[
3,
35,
'Chris Deactive',
'2018-02-06 23:28:57',
'Developer (left company)',
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y',
0,
'$2y$04$MLtQBZ9JLzWu1Y01QnNjsuoLm8qC9XRkpUywf6DIbpd9OAL1mEcCi',
[],
'chris_user',
'chris_user',
'chris_user@example.com',
'chris_user@example.com',
null,
null,
null,
null,
'$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO',
UserFixtures::DEFAULT_API_TOKEN . '_inactive',
],
[
4,
35,
'Tony Maier',
'2018-02-06 23:28:57',
'Head of Development',
'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg',
1,
'$2y$04$rqxiiExfUVzIYRVL2x4JJumQWNPIG6PazXwrSJm/VQFEesR08Uj5i',
['ROLE_TEAMLEAD'],
'tony_teamlead',
'tony_teamlead',
'tony_teamlead@example.com',
'tony_teamlead@example.com',
null,
null,
null,
null,
'$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO',
UserFixtures::DEFAULT_API_TOKEN . '_teamlead',
],
[
5,
81,
'Anna Smith',
'2018-02-06 23:28:57',
'Administrator',
null,
1,
'$2y$04$ct/rVb.naDzYZECnvfTJ2uns/zPHv8.8KcunhTjYFwWQeg1dywI8G',
['ROLE_ADMIN'],
'anna_admin',
'anna_admin',
'anna_admin@example.com',
'anna_admin@example.com',
null,
null,
null,
null,
'$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO',
UserFixtures::DEFAULT_API_TOKEN . '_admin',
],
[
6,
46,
null,
'2018-02-06 23:28:57',
'Super Administrator',
'/bundles/avanzuadmintheme/img/avatar.png',
1,
'$2y$04$kuhEEPw/CBMYc3x7SOv27eC1hQSmrtFvgJI2ULRuJeddAVDyrPKJ2',
['ROLE_SUPER_ADMIN'],
'susan_super',
'susan_super',
'susan_super@example.com',
'susan_super@example.com',
null,
'2020-04-14 09:50:38',
null,
null,
'$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO',
UserFixtures::DEFAULT_API_TOKEN . '_super',
],
[
7,
null,
'Test User 1',
null,
'Quality Tester 1',
null,
1,
'$2y$04$kuhEEPw/CBMYc3x7SOv27eC1hQSmrtFvgJI2ULRuJeddAVDyrPKJ2',
[],
'test_user_1',
'test_user_1',
'test_user_1@example.com',
'test_user_1@example.com',
null,
null,
null,
null,
'$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO',
UserFixtures::DEFAULT_API_TOKEN . '_qa1',
],
[
8,
null,
'Test User 2',
null,
'Quality Tester 2',
null,
1,
'$2y$04$kuhEEPw/CBMYc3x7SOv27eC1hQSmrtFvgJI2ULRuJeddAVDyrPKJ2',
[],
'test_user_2',
'test_user_2',
'test_user_2@example.com',
'test_user_2@example.com',
null,
null,
null,
null,
'$2y$13$X8/msijlFUgvRaiGLCJP/ep2hRyjpd.TSNz3cuutZLp05FpuBsYfO',
UserFixtures::DEFAULT_API_TOKEN . '_qa2',
],
[$user1, $token1],
[$user2, $token2],
[$user3, $token3],
[$user4, $token4],
[$user5, $token5],
[$user6, $token6],
[$user7, $token7],
[$user8, $token8],
];
$userEntities = [];
foreach ($users as $userConf) {
$user = new User();
foreach ($users as $items) {
$user = $items[0];
$user->setPassword($this->passwordHasher->hashPassword($user, UserFixtures::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, UserFixtures::DEFAULT_API_TOKEN));
foreach (User::WIZARDS as $wizard) {
$user->setWizardAsSeen($wizard);
}
if ($userConf[1] !== null) {
$user->setPreferenceValue(UserPreference::HOURLY_RATE, $userConf[1]);
}
if ($userConf[2] !== null) {
$user->setAlias($userConf[2]);
}
if ($userConf[3] !== null) {
$user->setRegisteredAt(new \DateTime($userConf[3]));
}
if ($userConf[4] !== null) {
$user->setTitle($userConf[4]);
}
if ($userConf[5] !== null) {
$user->setAvatar($userConf[5]);
}
if ($userConf[6] !== null) {
$user->setEnabled((bool) $userConf[6]);
}
$user->setPassword($userConf[7]);
if ($userConf[8] !== null && !empty($userConf[8])) {
$user->setRoles($userConf[8]);
} else {
$user->setRoles(['ROLE_USER']);
}
$user->setUserIdentifier($userConf[9]);
if ($userConf[10] !== null) {
// removed field: UsernameCanonical
}
if ($userConf[11] !== null) {
$user->setEmail($userConf[11]);
}
if ($userConf[12] !== null) {
// removed field: EmailCanonical
}
if ($userConf[17] !== null) {
$user->setApiToken($userConf[17]);
}
$this->entityManager->persist($user);
$accessToken = new AccessToken($user, $userConf[18]);
$accessToken->setName('Test fixture');
$accessToken = $items[1];
$this->entityManager->persist($accessToken);
$this->entityManager->persist($user);
$userEntities[] = $user;
}

View File

@@ -36,6 +36,7 @@ final class UserLoginLinkCommand extends Command
parent::__construct();
$this->addArgument('email', InputArgument::REQUIRED, 'The email of the user');
$this->addOption('password-reset', null, InputOption::VALUE_NONE, 'Whether the user needs to reset the password afterwards');
$this->addOption('all-auth', null, InputOption::VALUE_NONE, 'Ignore that the user is using an external authentication system');
}
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -63,7 +64,7 @@ final class UserLoginLinkCommand extends Command
return Command::FAILURE;
}
if (!$user->isInternalUser()) {
if (!$user->isInternalUser() && !$input->getOption('all-auth')) {
$io->error('User does not use internal login');
return Command::FAILURE;

View File

@@ -33,10 +33,7 @@ final class SystemConfiguration
}
/**
* Set an array item to a given value using "dot" notation.
* If no key is given to the method, the entire array will be replaced.
*
* @internal
* Set a new or replace an existing system configuration.
*/
public function set(string $key, mixed $value): void
{
@@ -456,6 +453,11 @@ final class SystemConfiguration
return $this->getIncrement('quick_entry.recent_activities', 5, 0);
}
public function isBreakTimeEnabled(): bool
{
return (bool) $this->find('timesheet.rules.break_time_active');
}
// ========== Company configurations ==========
public function getFinancialYearStart(): ?string

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.23.0';
public const VERSION = '2.24.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 22300;
public const VERSION_ID = 22400;
/**
* The software name
*/

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller\Reporting;
use App\Activity\ActivityStatisticService;
use App\Controller\AbstractController;
use App\Reporting\ActivityView\ActivityViewForm;
use App\Reporting\ActivityView\ActivityViewQuery;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ActivityViewController extends AbstractController
{
#[Route(path: '/activity/project_view', name: 'report_activity_view', methods: ['GET', 'POST'])]
#[IsGranted('report:project')]
#[IsGranted(new Expression("is_granted('budget_any', 'activity')"))]
public function __invoke(Request $request, ActivityStatisticService $service): Response
{
$dateFactory = $this->getDateTimeFactory();
$user = $this->getUser();
$query = new ActivityViewQuery($dateFactory->createDateTime(), $user);
$form = $this->createFormForGetRequest(ActivityViewForm::class, $query);
$form->submit($request->query->all(), false);
$activities = $service->findActivitiesForView($query);
$entries = $service->getActivityView($user, $activities, $query->getToday());
$byCustomer = [];
foreach ($entries as $entry) {
$project = $entry->getActivity()->getProject();
$key = ($project === null || $project->getId() === null ? '__EMPTY__' : $project->getId());
if (!\array_key_exists($key, $byCustomer)) {
$byCustomer[$key] = ['project' => $project, 'activities' => [], 'name' => $project !== null ? $project->getName() : ''];
}
$byCustomer[$key]['activities'][] = $entry;
}
return $this->render('reporting/activity_view.html.twig', [
'entries' => $byCustomer,
'form' => $form->createView(),
'report_title' => 'report_activity_view',
'tableName' => 'activity_view_reporting',
'now' => $dateFactory->createDateTime(),
]);
}
}

View File

@@ -85,6 +85,10 @@ abstract class TimesheetAbstractController extends AbstractController
$table->addColumn('endtime', ['class' => 'd-none d-sm-table-cell text-center text-nowrap', 'orderBy' => 'end']);
}
if ($this->configuration->isBreakTimeEnabled()) {
$table->addColumn('break', ['class' => 'text-end text-nowrap']);
}
$table->addColumn('duration', ['class' => 'text-end text-nowrap']);
if ($canSeeRate) {

View File

@@ -29,8 +29,8 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
*/
final class UserFixtures extends Fixture implements FixtureGroupInterface
{
public const DEFAULT_PASSWORD = 'kitten';
public const DEFAULT_API_TOKEN = 'api_kitten';
public const DEFAULT_PASSWORD = 'password';
public const DEFAULT_API_TOKEN = 'token';
public const DEFAULT_AVATAR = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y';
public const USERNAME_USER = 'john_user';
@@ -43,7 +43,7 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
public const MIN_RATE = 30;
public const MAX_RATE = 120;
public function __construct(private UserPasswordHasherInterface $passwordHasher)
public function __construct(private readonly UserPasswordHasherInterface $passwordHasher)
{
}
@@ -65,32 +65,8 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
{
$allUsers = $this->getUserDefinition();
foreach ($allUsers as $userData) {
$user = new User();
$user->setAlias($userData[0]);
$user->setTitle($userData[1]);
$user->setUserIdentifier($userData[2]);
$user->setEmail($userData[3]);
$user->setRoles([$userData[4]]);
$user->setAvatar($userData[5]);
$user->setEnabled($userData[6]);
$user->setPassword($this->passwordHasher->hashPassword($user, $userData[8]));
$user->setApiToken($this->passwordHasher->hashPassword($user, $userData[9]));
$manager->persist($user);
$prefs = $this->getUserPreferences($user, $userData[7]);
$user->setPreferences($prefs);
// better to be able to test the wizard in demo installations
/*
foreach (User::WIZARDS as $wizard) {
$user->setWizardAsSeen($wizard);
}
*/
$manager->persist($prefs[0]);
$manager->persist($prefs[1]);
$accessToken = new AccessToken($user, $userData[10]);
$accessToken->setName('Test fixture');
$manager->persist($accessToken);
$manager->persist($userData[0]);
$manager->persist($userData[1]);
}
$manager->flush();
@@ -100,7 +76,7 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
/**
* @param User $user
* @param string|null $timezone
* @return array
* @return array<UserPreference>
*/
private function getUserPreferences(User $user, string $timezone = null): array
{
@@ -162,141 +138,158 @@ final class UserFixtures extends Fixture implements FixtureGroupInterface
$manager->clear();
}
/**
* Do NOT set wizard as seen here, because they should be visible in the demo installation.
*
* @return array<int, array{0: User, 1: AccessToken}>
*/
private function getUserDefinition(): array
{
// alias = $userData[0]
// title = $userData[1]
// username = $userData[2]
// email = $userData[3]
// roles = [$userData[4]]
// avatar = $userData[5]
// enabled = $userData[6]
// timezone = $userData[7]
// password = $userData[8]
// api old = $userData[9]
// api new = $userData[10]
$all = [];
return [
[
'John Doe',
'Developer',
self::USERNAME_USER,
'john_user@example.com',
User::ROLE_USER,
self::DEFAULT_AVATAR,
true,
'America/Vancouver',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
self::DEFAULT_API_TOKEN . '_john',
],
[
'John Doe',
'Developer',
'user',
'user@example.com',
User::ROLE_USER,
self::DEFAULT_AVATAR,
true,
'America/Vancouver',
'password',
'password',
self::DEFAULT_API_TOKEN . '_user',
],
// inactive user to test login
[
'Chris Deactive',
'Developer (left company)',
'chris_user',
'chris_user@example.com',
User::ROLE_USER,
self::DEFAULT_AVATAR,
false,
'Australia/Sydney',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
self::DEFAULT_API_TOKEN . '_inactive',
],
[
'Tony Maier',
'Head of Sales',
self::USERNAME_TEAMLEAD,
'tony_teamlead@example.com',
User::ROLE_TEAMLEAD,
'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg',
true,
'Asia/Bangkok',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
self::DEFAULT_API_TOKEN . '_teamlead',
],
[
'Tony Maier',
'Head of Sales',
'teamlead',
'teamlead@example.com',
User::ROLE_TEAMLEAD,
'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg',
true,
'Asia/Bangkok',
'password',
'password',
self::DEFAULT_API_TOKEN . '_tony',
],
// no avatar to test default image macro
[
'Anna Smith',
'Administrator',
self::USERNAME_ADMIN,
'anna_admin@example.com',
User::ROLE_ADMIN,
null,
true,
'Europe/London',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
self::DEFAULT_API_TOKEN . '_anna',
],
[
'Anna Smith',
'Administrator',
'administrator',
'administrator@example.com',
User::ROLE_ADMIN,
null,
true,
'Europe/London',
'password',
'password',
self::DEFAULT_API_TOKEN . '_admin',
],
// no alias to test twig username macro
[
null,
'Super Administrator',
self::USERNAME_SUPER_ADMIN,
'susan_super@example.com',
User::ROLE_SUPER_ADMIN,
'/touch-icon-192x192.png',
true,
'Europe/Berlin',
self::DEFAULT_PASSWORD,
self::DEFAULT_API_TOKEN,
self::DEFAULT_API_TOKEN . '_susan',
],
[
null,
'Super Administrator',
'super_admin',
'super_admin@example.com',
User::ROLE_SUPER_ADMIN,
'/touch-icon-192x192.png',
true,
'Europe/Berlin',
'password',
'password',
self::DEFAULT_API_TOKEN . '_super',
],
];
$user = new User();
$user->setAlias('John Doe');
$user->setTitle('Developer');
$user->setUserIdentifier(self::USERNAME_USER);
$user->setEmail('john_user@example.com');
$user->setRoles([User::ROLE_USER]);
$user->setAvatar(self::DEFAULT_AVATAR);
$user->setEnabled(true);
$prefs = $this->getUserPreferences($user, 'America/Vancouver');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_john');
$token->setName('User fixture');
$all[] = [$user, $token];
$user = new User();
$user->setAlias('John Doe');
$user->setTitle('Developer');
$user->setUserIdentifier('user');
$user->setEmail('user@example.com');
$user->setRoles([User::ROLE_USER]);
$user->setAvatar(self::DEFAULT_AVATAR);
$user->setEnabled(true);
$prefs = $this->getUserPreferences($user, 'America/Vancouver');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_user');
$token->setName('User fixture');
$all[] = [$user, $token];
// inactive user to test login
$user = new User();
$user->setAlias('Chris Deactive');
$user->setTitle('Developer (left company)');
$user->setUserIdentifier('chris_user');
$user->setEmail('chris_user@example.com');
$user->setRoles([User::ROLE_USER]);
$user->setAvatar(self::DEFAULT_AVATAR);
$user->setEnabled(false);
$prefs = $this->getUserPreferences($user, 'Australia/Sydney');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_inactive');
$token->setName('User fixture');
$all[] = [$user, $token];
$user = new User();
$user->setAlias('Tony Maier');
$user->setTitle('Head of Sales');
$user->setUserIdentifier(self::USERNAME_TEAMLEAD);
$user->setEmail('tony_teamlead@example.com');
$user->setRoles([User::ROLE_TEAMLEAD]);
$user->setAvatar('https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg');
$user->setEnabled(true);
$prefs = $this->getUserPreferences($user, 'Asia/Bangkok');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_teamlead');
$token->setName('User fixture');
$all[] = [$user, $token];
$user = new User();
$user->setAlias('Tony Maier');
$user->setTitle('Head of Sales');
$user->setUserIdentifier('teamlead');
$user->setEmail('teamlead@example.com');
$user->setRoles([User::ROLE_TEAMLEAD]);
$user->setAvatar('https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg');
$user->setEnabled(true);
$prefs = $this->getUserPreferences($user, 'Asia/Bangkok');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_tony');
$token->setName('User fixture');
$all[] = [$user, $token];
// no avatar to test default image macro
$user = new User();
$user->setAlias('Anna Smith');
$user->setTitle('Administrator');
$user->setUserIdentifier(self::USERNAME_ADMIN);
$user->setEmail('anna_admin@example.com');
$user->setRoles([User::ROLE_ADMIN]);
$user->setEnabled(true);
$prefs = $this->getUserPreferences($user, 'Europe/London');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_anna');
$token->setName('User fixture');
$all[] = [$user, $token];
$user = new User();
$user->setAlias('Anna Smith');
$user->setTitle('Administrator');
$user->setUserIdentifier('administrator');
$user->setEmail('administrator@example.com');
$user->setRoles([User::ROLE_ADMIN]);
$user->setEnabled(true);
$prefs = $this->getUserPreferences($user, 'Europe/London');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_admin');
$token->setName('User fixture');
$all[] = [$user, $token];
// no alias to test twig username macro
$user = new User();
$user->setTitle('Super Administrator');
$user->setUserIdentifier(self::USERNAME_SUPER_ADMIN);
$user->setEmail('susan_super@example.com');
$user->setRoles([User::ROLE_SUPER_ADMIN]);
$user->setAvatar('/touch-icon-192x192.png');
$user->setEnabled(true);
$prefs = $this->getUserPreferences($user, 'Europe/Berlin');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_susan');
$token->setName('User fixture');
$all[] = [$user, $token];
$user = new User();
$user->setTitle('Super Administrator');
$user->setUserIdentifier('super_admin');
$user->setEmail('super_admin@example.com');
$user->setRoles([User::ROLE_SUPER_ADMIN]);
$user->setAvatar('/touch-icon-192x192.png');
$user->setEnabled(true);
$prefs = $this->getUserPreferences($user, 'Europe/Berlin');
$user->setPreferences($prefs);
$user->setPassword($this->passwordHasher->hashPassword($user, self::DEFAULT_PASSWORD));
$user->setApiToken($this->passwordHasher->hashPassword($user, self::DEFAULT_API_TOKEN));
$token = new AccessToken($user, self::DEFAULT_API_TOKEN . '_super');
$token->setName('User fixture');
$all[] = [$user, $token];
return $all;
}
}

View File

@@ -1276,7 +1276,17 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
public function getWorkStartingDay(): ?\DateTimeInterface
{
$date = $this->getPreferenceValue(UserPreference::WORK_STARTING_DAY);
return $this->getPreferenceDate('work_start_day');
}
public function setWorkStartingDay(?\DateTimeInterface $date): void
{
$this->setPreferenceValue('work_start_day', $date?->format('Y-m-d'));
}
private function getPreferenceDate(string $prefName): ?\DateTimeInterface
{
$date = $this->getPreferenceValue($prefName);
if ($date === null) {
return null;
@@ -1290,9 +1300,14 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
return ($date instanceof \DateTimeInterface) ? $date : null;
}
public function setWorkStartingDay(?\DateTimeInterface $date): void
public function getLastWorkingDay(): ?\DateTimeInterface
{
$this->setPreferenceValue(UserPreference::WORK_STARTING_DAY, $date?->format('Y-m-d'));
return $this->getPreferenceDate('work_last_day');
}
public function setLastWorkingDay(?\DateTimeInterface $date): void
{
$this->setPreferenceValue('work_last_day', $date?->format('Y-m-d'));
}
public function getPublicHolidayGroup(): null|string

View File

@@ -47,7 +47,6 @@ class UserPreference
public const WORK_HOURS_SATURDAY = WorkingTimeCalculatorDay::WORK_HOURS_SATURDAY;
/** @deprecated since 2.22*/
public const WORK_HOURS_SUNDAY = WorkingTimeCalculatorDay::WORK_HOURS_SUNDAY;
public const WORK_STARTING_DAY = 'work_start_day';
public const PUBLIC_HOLIDAY_GROUP = 'public_holiday_group';
public const HOLIDAYS_PER_YEAR = 'holidays';
public const WORK_CONTRACT_TYPE = 'work_contract_type';

View File

@@ -26,7 +26,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
final class ActivityType extends AbstractType
{
public function __construct(private ActivityHelper $activityHelper, private ProjectHelper $projectHelper)
public function __construct(private readonly ActivityHelper $activityHelper, private readonly ProjectHelper $projectHelper)
{
}
@@ -35,10 +35,10 @@ final class ActivityType extends AbstractType
return $this->activityHelper->getChoiceLabel($activity);
}
public function groupBy(Activity $activity, $key, $index): ?string
public function groupBy(Activity $activity, $key, $index): string
{
if (null === $activity->getProject()) {
return null;
return '';
}
return $this->projectHelper->getChoiceLabel($activity->getProject());

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Reporting\ActivityView;
use App\Form\Type\ProjectType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<ActivityViewQuery>
*/
final class ActivityViewForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('project', ProjectType::class, [
'required' => false,
'width' => false,
]);
$builder->add('budgetType', ChoiceType::class, [
'label' => false,
'required' => false,
'placeholder' => null,
'expanded' => true,
'choices' => [
'all' => null,
'includeWithBudget' => true,
'includeNoBudget' => false
]
]);
$builder->add('includeNoWork', CheckboxType::class, [
'required' => false,
'label' => 'includeNoWork',
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ActivityViewQuery::class,
'csrf_protection' => false,
'method' => 'GET',
]);
}
}

View File

@@ -0,0 +1,181 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Reporting\ActivityView;
use App\Entity\Activity;
use App\Model\BudgetStatisticModelInterface;
use DateTime;
final class ActivityViewModel
{
private int $timesheetCounter = 0;
private int $durationDay = 0;
private int $durationWeek = 0;
private int $durationMonth = 0;
private int $durationTotal = 0;
private float $rateTotal = 0.00;
private int $notExportedDuration = 0;
private float $notExportedRate = 0.00;
private int $notBilledDuration = 0;
private float $notBilledRate = 0.00;
private int $billableDuration = 0;
private float $billableRate = 0.00;
private ?DateTime $lastRecord = null;
private ?BudgetStatisticModelInterface $budgetStatisticModel = null;
public function __construct(private readonly Activity $activity)
{
}
public function getActivity(): Activity
{
return $this->activity;
}
public function getTimesheetCounter(): int
{
return $this->timesheetCounter;
}
public function setTimesheetCounter(int $timesheetCounter): void
{
$this->timesheetCounter = $timesheetCounter;
}
public function getDurationDay(): int
{
return $this->durationDay;
}
public function setDurationDay(int $durationDay): void
{
$this->durationDay = $durationDay;
}
public function getDurationWeek(): int
{
return $this->durationWeek;
}
public function setDurationWeek(int $durationWeek): void
{
$this->durationWeek = $durationWeek;
}
public function getDurationMonth(): int
{
return $this->durationMonth;
}
public function setDurationMonth(int $durationMonth): void
{
$this->durationMonth = $durationMonth;
}
public function getDurationTotal(): int
{
return $this->durationTotal;
}
public function setDurationTotal(int $durationTotal): void
{
$this->durationTotal = $durationTotal;
}
public function getNotExportedDuration(): int
{
return $this->notExportedDuration;
}
public function setNotExportedDuration(int $notExportedDuration): void
{
$this->notExportedDuration = $notExportedDuration;
}
public function getNotExportedRate(): float
{
return $this->notExportedRate;
}
public function setNotExportedRate(float $notExportedRate): void
{
$this->notExportedRate = $notExportedRate;
}
public function getNotBilledDuration(): int
{
return $this->notBilledDuration;
}
public function setNotBilledDuration(int $notBilledDuration): void
{
$this->notBilledDuration = $notBilledDuration;
}
public function getNotBilledRate(): float
{
return $this->notBilledRate;
}
public function setNotBilledRate(float $notBilledRate): void
{
$this->notBilledRate = $notBilledRate;
}
public function getBillableDuration(): int
{
return $this->billableDuration;
}
public function setBillableDuration(int $billableDuration): void
{
$this->billableDuration = $billableDuration;
}
public function getBillableRate(): float
{
return $this->billableRate;
}
public function setBillableRate(float $billableRate): void
{
$this->billableRate = $billableRate;
}
public function getRateTotal(): float
{
return $this->rateTotal;
}
public function setRateTotal(float $rateTotal): void
{
$this->rateTotal = $rateTotal;
}
public function getLastRecord(): ?DateTime
{
return $this->lastRecord;
}
public function setLastRecord(DateTime $lastRecord): void
{
$this->lastRecord = $lastRecord;
}
public function getBudgetStatisticModel(): ?BudgetStatisticModelInterface
{
return $this->budgetStatisticModel;
}
public function setBudgetStatisticModel(BudgetStatisticModelInterface $budgetStatisticModel): void
{
$this->budgetStatisticModel = $budgetStatisticModel;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Reporting\ActivityView;
use App\Entity\Project;
use App\Entity\User;
final class ActivityViewQuery
{
private ?Project $project = null;
private bool $includeNoWork = false;
private ?bool $budgetType = true;
public function __construct(private readonly \DateTimeInterface $today, private readonly User $user)
{
}
public function getUser(): User
{
return $this->user;
}
public function getBudgetType(): ?bool
{
return $this->budgetType;
}
/**
* @internal
*/
public function setBudgetType(?bool $budgetType): void
{
$this->budgetType = $budgetType;
}
public function isIncludeWithoutBudget(): bool
{
return $this->budgetType === false;
}
public function isIncludeWithBudget(): bool
{
return $this->budgetType === true;
}
public function isIncludeNoWork(): bool
{
return $this->includeNoWork;
}
public function setIncludeNoWork(bool $includeNoWork): void
{
$this->includeNoWork = $includeNoWork;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(Project $project): void
{
$this->project = $project;
}
public function getToday(): \DateTimeInterface
{
return $this->today;
}
}

View File

@@ -41,6 +41,12 @@ final class ReportingService
$event->addReport(new Report('yearly_users_list', 'report_yearly_users', 'report_yearly_users', 'users'));
}
if ($this->security->isGranted('report:activity')) {
if ($viewOther) {
$event->addReport(new Report('activity_view', 'report_activity_view', 'report_activity_view', 'activity'));
}
}
if ($this->security->isGranted('report:project')) {
if ($this->security->isGranted('details', 'project')) {
$event->addReport(new Report('project_details', 'report_project_details', 'report_project_details', 'project'));

View File

@@ -94,7 +94,7 @@ class ActivityRepository extends EntityRepository
/**
* @param array<Team> $teams
*/
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false): void
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [], bool $globalsOnly = false): void
{
$permissions = $this->getPermissionCriteria($qb, $user, $teams, $globalsOnly);
if ($permissions->count() > 0) {

View File

@@ -22,6 +22,7 @@ final class ReportingVoter extends Voter
private const ALLOWED_ATTRIBUTES = [
'report:customer',
'report:other',
'report:activity',
'report:project',
'report:user',
];
@@ -65,6 +66,10 @@ final class ReportingVoter extends Voter
$permissions[] = 'view_other_timesheet';
break;
case 'report:activity':
$permissions[] = 'activity_reporting';
break;
case 'report:project':
$permissions[] = 'project_reporting';
break;

View File

@@ -102,6 +102,7 @@ final class WorkingTimeService
$stats = null;
$firstDay = $user->getWorkStartingDay();
$lastDay = $user->getLastWorkingDay();
$calculator = $this->getContractMode($user)->getCalculator($user);
foreach ($year->getMonths() as $month) {
@@ -119,7 +120,7 @@ final class WorkingTimeService
$dayDate = $day->getDay();
$result = new WorkingTime($user, $dayDate);
if ($firstDay === null || $firstDay <= $dayDate) {
if (($firstDay === null || $firstDay <= $dayDate) && ($lastDay === null || $lastDay >= $dayDate)) {
$result->setExpectedTime($calculator->getWorkHoursForDay($dayDate));
}

View File

@@ -269,6 +269,8 @@
.items td.cost,
.items th.column-rate,
.items th.column-internalRate,
.items th.column-hourlyRate,
.items th.column-fixedRate,
.items th.column-duration
{
text-align: right;
@@ -294,7 +296,7 @@
|
<label for="date-format">
{{ 'date'|trans }}:
{% set demo_date = create_date('2020-01-01 20:00:00') %}
{% set demo_date = create_date('2024-01-01 11:00:00') %}
<select id="date-format" name="date-format">
<option value="short">{{ demo_date|date_short }}</option>
<option value="time">{{ demo_date|date_time }}</option>
@@ -303,7 +305,7 @@
|
<label for="begin-format">
{{ 'begin'|trans }}:
{% set demo_date = create_date('2020-01-01 11:00:00') %}
{% set demo_date = create_date('2024-01-01 11:00:00') %}
<select id="begin-format" name="begin-format">
<option value="plain">{{ demo_date|date_format('H:i') }}</option>
<option value="short">{{ demo_date|date_short }}</option>
@@ -313,7 +315,7 @@
|
<label for="end-format">
{{ 'end'|trans }}:
{% set demo_date = create_date('2020-01-01 18:00:00') %}
{% set demo_date = create_date('2024-01-01 18:00:00') %}
<select id="end-format" name="end-format">
<option value="plain">{{ demo_date|date_format('H:i') }}</option>
<option value="short">{{ demo_date|date_short }}</option>
@@ -635,7 +637,7 @@
<tr>
<td class="column-date text-nowrap" {% if not columns.date %}style="display: none"{% endif %}>
<span class="dateformat" data-short="{{ entry.begin|date_short }}" data-full="{{ entry.begin|date_time }}" data-time="{{ entry.begin|date_time }}">
{{ entry.begin|date_time }}
{{ entry.begin|date_short }}
</span>
</td>
<td class="column-begin text-nowrap" {% if not columns.begin %}style="display: none"{% endif %}>
@@ -708,10 +710,10 @@
{% endif %}
</td>
{% endfor %}
<td class="column-hourlyRate text-nowrap" {% if not columns.hourlyRate %}style="display: none"{% endif %}>
<td class="cost column-hourlyRate text-nowrap" {% if not columns.hourlyRate %}style="display: none"{% endif %}>
{{ entry.hourlyRate|money(entry.project.customer.currency) }}
</td>
<td class="column-fixedRate text-nowrap" {% if not columns.fixedRate %}style="display: none"{% endif %}>
<td class="cost column-fixedRate text-nowrap" {% if not columns.fixedRate %}style="display: none"{% endif %}>
{{ entry.fixedRate|money(entry.project.customer.currency) }}
</td>
<td class="duration column-duration text-nowrap" {% if not columns.duration %}style="display: none"{% endif %}>

View File

@@ -0,0 +1,96 @@
{% extends 'reporting/layout.html.twig' %}
{% import "macros/datatables.html.twig" as tables %}
{% set availableColumns = {
'name': {'class': 'alwaysVisible'},
} %}
{% if is_granted('budget_time', 'activity') %}
{% set availableColumns = availableColumns|merge({
'timeBudget': {'class': 'd-none d-md-table-cell', 'title': 'timeBudget'|trans},
}) %}
{% endif %}
{% if is_granted('budget_money', 'activity') %}
{% set availableColumns = availableColumns|merge({
'budget': {'class': 'd-none d-md-table-cell', 'title': 'budget'|trans},
}) %}
{% endif %}
{% set availableColumns = availableColumns|merge({
'durationTotal': {'class': 'text-end hw-min w-min', 'title': 'stats.durationTotal'|trans, 'columnClass': 'w-min'},
'actions': {'class': 'actions alwaysVisible'},
}) %}
{% set tableName = tableName|default('activity_view_reporting') %}
{% set skipColumns = skipColumns is defined ? skipColumns : {} %}
{% set columns = {} %}
{% for name, config in availableColumns %}
{% if name not in skipColumns %}
{% set columns = columns|merge({(name): config}) %}
{% endif %}
{% endfor %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% endblock %}
{% block report %}
{% set hasData = entries|length > 0 %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/progressbar.html.twig" as progress %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% import "activity/actions.html.twig" as activityActions %}
{% block box_body_class %}{{ tableName }}-box {% if hasData %}p-0{% endif %}{% endblock %}
{% block box_body %}
{% if not hasData %}
{{ widgets.nothing_found() }}
{% else %}
{{ tables.datatable_header(tableName, columns, null, {boxClass: ''}) }}
{% for id, mapping in entries|sort((a, b) => a.name <=> b.name) %}
{% if mapping.project is not null %}
<tr class="summary">
<td colspan="{{ columns|length }}">
{{ widgets.label_customer(mapping.project) }}
</td>
</tr>
{% endif %}
{% for entry in mapping.activities|sort((a, b) => a.activity.name <=> b.activity.name) %}
{% set activity = entry.activity %}
{% set project = activity.project %}
{% set budgetStats = entry.getBudgetStatisticModel() %}
{% set currency = null %}
{% if project is not null %}
{% set currency = project.customer.currency %}
{% endif %}
<tr {{ widgets.activity_row_attr(activity, now) }}>
{% for name, column_config in columns %}
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
{% if name == 'name' %}
{{ widgets.label_activity(activity) }}
{% elseif name == 'durationTotal' %}
{{ entry.durationTotal|duration }}
{% elseif name == 'timeBudget' %}
{% if budgetStats.hasTimeBudget() and is_granted('time', activity) %}
{{ progress.progressbar_timebudget(budgetStats) }}
{% endif %}
{% elseif name == 'budget' %}
{% if activity.hasBudget() and is_granted('budget', activity) %}
{{ progress.progressbar_budget(budgetStats, currency) }}
{% endif %}
{% elseif name == 'comment' %}
{{ activity.comment }}
{% elseif name == 'actions' %}
{{ activityActions.activity(activity, 'custom') }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
{% endfor %}
{{ tables.data_table_footer(entries) }}
{% endif %}
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'reporting/activity_list_data.html.twig' %}
{% block report_form_layout %}
{{ form_widget(form.project, {'label': false, 'placeholder': 'please_choose'}) }}
<div class="dropdown">
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon('filter', true) }}
</button>
<ul class="dropdown-menu checkbox-menu">
{% for option in form.budgetType.children %}
<li class="dropdown-item">
{{ form_widget(option) }}
</li>
{% endfor %}
<li class="dropdown-divider"></li>
<li class="dropdown-item">
{{ form_widget(form.includeNoWork) }}
</li>
</ul>
</div>
{% endblock %}

View File

@@ -110,6 +110,8 @@
{% else %}
<i data-since="{{ entry.begin.format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</i>
{% endif %}
{% elseif column == 'break' %}
{{ entry.break|duration }}
{% elseif column == 'hourlyRate' %}
{{ entryHourlyRate }}
{% elseif column == 'rate' %}

View File

@@ -7,11 +7,10 @@
* file that was distributed with this source code.
*/
namespace API;
namespace App\Tests\API;
use App\Entity\Invoice;
use App\Entity\User;
use App\Tests\API\APIControllerBaseTest;
use App\Tests\DataFixtures\InvoiceFixtures;
/**

View File

@@ -13,6 +13,7 @@ use App\Command\ResetTestCommand;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
/**
* @covers \App\Command\ResetTestCommand
@@ -24,7 +25,11 @@ class ResetTestCommandTest extends KernelTestCase
{
$kernel = self::bootKernel();
$application = new Application($kernel);
$application->add(new ResetTestCommand($this->createMock(EntityManagerInterface::class), 'test'));
$application->add(new ResetTestCommand(
$this->createMock(EntityManagerInterface::class),
$this->createMock(UserPasswordHasherInterface::class),
'test'
));
self::assertTrue($application->has('kimai:reset:test'));
$command = $application->find('kimai:reset:test');
@@ -33,7 +38,11 @@ class ResetTestCommandTest extends KernelTestCase
public function testCommandNameIsNotEnabledInProd(): void
{
$sut = new ResetTestCommand($this->createMock(EntityManagerInterface::class), 'prod');
$sut = new ResetTestCommand(
$this->createMock(EntityManagerInterface::class),
$this->createMock(UserPasswordHasherInterface::class),
'prod'
);
self::assertFalse($sut->isEnabled());
}
}

View File

@@ -12,6 +12,7 @@ namespace App\Tests\Controller\Security;
use App\Configuration\SamlConfiguration;
use App\Configuration\SystemConfiguration;
use App\Controller\Security\SecurityController;
use App\DataFixtures\UserFixtures;
use App\Entity\User;
use App\Tests\Configuration\TestConfigLoader;
use App\Tests\Controller\ControllerBaseTest;
@@ -63,8 +64,8 @@ class SecurityControllerTest extends ControllerBaseTest
$form = $client->getCrawler()->filter('body form')->form();
$client->submit($form, [
'_username' => 'susan_super',
'_password' => 'kitten'
'_username' => UserFixtures::USERNAME_SUPER_ADMIN,
'_password' => UserFixtures::DEFAULT_PASSWORD
]);
$this->assertIsRedirect($client); // redirect to root URL

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target state="translated">لقد قمت بالتسجيل في متتبع الوقت Kimai بالبريد الإلكتروني%email%. يرجى تفعيل حسابك في الساعات القليلة القادمة ، قبل انتهاء صلاحية الرابط.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target state="translated">إعادة تعيين كلمة المرور</target>
<target state="needs-translation">إعادة تعيين كلمة المرور</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">يمكن أن يحدث ذلك …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target state="translated">إعادة تعيين كلمة المرور</target>
<target state="needs-translation">إعادة تعيين كلمة المرور</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,21 +22,21 @@
<source>registration.intro</source>
<target>Zaregistrovali jste se do Kimai time-tracker s e-mailovou adresou %email%. Nyní je potřeba váš účet aktivovat v několika nejbližších hodinách, jinak odkaz expiruje.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Reset hesla</target>
<target state="needs-translation">Reset hesla</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">Zapomněli jste heslo?</target>
<target state="needs-translation">Zapomněli jste heslo?</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Reset hesla</target>
<target state="needs-translation">Reset hesla</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">Nebojte se, Kimai vám pomůže vytvořit nové!</target>
<target state="needs-translation">Nebojte se, Kimai vám pomůže vytvořit nové!</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" xml:space="preserve">
<source>absence_created_supervisor_subject</source>

View File

@@ -14,17 +14,17 @@
<source>registration.button</source>
<target>Aktiver din konto</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Nulstil dit kodeord</target>
<target state="needs-translation">Nulstil dit kodeord</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Det kan ske …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Nulstil dit kodeord</target>
<target state="needs-translation">Nulstil dit kodeord</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,17 +22,17 @@
<source>You signed up at the Kimai time-tracker with the e-mail address %email%. Please activate your account in the next few hours, before the link expires.</source>
<target state="translated">Sie haben sich mit der E-Mail-Adresse %email% bei der Kimai-Zeiterfassung angemeldet. Bitte aktivieren Sie Ihr Konto in den nächsten Stunden, bevor der Link abläuft.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target state="translated">Passwort zurücksetzen</target>
<target state="needs-translation">Passwort zurücksetzen</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Das kann passieren…</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target state="translated">Passwort zurücksetzen</target>
<target state="needs-translation">Passwort zurücksetzen</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target>Εγγραφήκατε στο λογισμικό παρακολούθησης χρόνου Kimai με το e-mail %email%. Παρακαλούμε ενεργοποιήσετε τον λογαριασμό σας εντός μερικών ωρών, προτού λήξει ο σύνδεσμος.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Επαναφορά του κωδικού σας</target>
<target state="needs-translation">Επαναφορά του κωδικού σας</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Αυτό μπορεί να συμβεί …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Επαναφορά κωδικού πρόσβασης</target>
<target state="needs-translation">Επαναφορά κωδικού πρόσβασης</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target>Vi aliĝis al la temporegistrilo Kimai per la retadreso %email%. Bonvolu aktivigi vian konton dum la sekvaj kelkaj horoj, antaŭ ol la ligilo senvalidiĝos.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Restarigi vian pasvorton</target>
<target state="needs-translation">Restarigi vian pasvorton</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Tio povas okazi …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Restarigi vian pasvorton</target>
<target state="needs-translation">Restarigi vian pasvorton</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,21 +22,21 @@
<source>You signed up at the Kimai time-tracker with the e-mail address %email%. Please activate your account in the next few hours, before the link expires.</source>
<target state="translated">Te has registrado en el cronómetro de Kimai con la dirección de correo electrónico %email%. Por favor, activa tu cuenta en las próximas horas, antes de que caduque el enlace.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target state="translated">Restablecer su contraseña</target>
<target state="needs-translation">Restablecer su contraseña</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">¿Olvidaste tu contraseña?</target>
<target state="needs-translation">¿Olvidaste tu contraseña?</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target state="translated">Restablecer su contraseña</target>
<target state="needs-translation">Restablecer su contraseña</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">¡No te preocupes, Kimai te ayudará a crear uno nuevo!</target>
<target state="needs-translation">¡No te preocupes, Kimai te ayudará a crear uno nuevo!</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject">
<source>absence_created_supervisor_subject</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target state="translated">شما در رهگیر زمان Kimai با ایمیل %email% ثبت نام کرده اید. اکنون باید ایمیل خود را در همین زمان، قبل از باطل شدن لینک فعال کنید.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>پسورد خود را ریست کنید</target>
<target state="needs-translation">پسورد خود را ریست کنید</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">آن می تواند اتفاق بیفتد …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>پسورد خود را ریست کنید</target>
<target state="needs-translation">پسورد خود را ریست کنید</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target state="translated">Olet rekisteröitynyt Kimai työajan-seurantaan sähköpostiosoitteella %email%. Nyt sinun tulee aktivoida tunnuksesi seuraavien tuntien aikana, ennen kuin linkki vanhenee.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target state="translated">Resetoi salasanasi</target>
<target state="needs-translation">Resetoi salasanasi</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Näin voi käydä …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target state="translated">Resetoi salasanasi</target>
<target state="needs-translation">Resetoi salasanasi</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,21 +22,21 @@
<source>registration.intro</source>
<target>Vous vous êtes inscrit·e sur Kimai avec le courriel %email%. Veuillez activer votre compte dans les prochaines heures, avant que le lien expire.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Réinitialiser le mot de passe</target>
<target state="needs-translation">Réinitialiser le mot de passe</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">Vous avez oublié votre mot de passe?</target>
<target state="needs-translation">Vous avez oublié votre mot de passe?</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Réinitialiser le mot de passe</target>
<target state="needs-translation">Réinitialiser le mot de passe</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">Ne vous inquiétez pas, Kimai vous aidera à en créer un nouveau!</target>
<target state="needs-translation">Ne vous inquiétez pas, Kimai vous aidera à en créer un nouveau!</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" xml:space="preserve">
<source>absence_created_supervisor_subject</source>

View File

@@ -22,21 +22,21 @@
<source>You signed up at the Kimai time-tracker with the e-mail address %email%. Please activate your account in the next few hours, before the link expires.</source>
<target state="translated">נרשמת למערכת דיווח השעות Kimai עם כתובת הדוא״ל %email%. נא להפעיל את החשבון בשעות הקרובות לפני שיפוג תוקף הקישור.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target state="translated">איפוס הסיסמה שלך</target>
<target state="needs-translation">איפוס הסיסמה שלך</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve" approved="yes">
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve" approved="no">
<source>Forgot your password?</source>
<target state="final">שכחת את הסיסמה שלך?</target>
<target state="needs-translation">שכחת את הסיסמה שלך?</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve" approved="yes">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve" approved="no">
<source>reset.button</source>
<target state="final">איפוס הסיסמה שלך</target>
<target state="needs-translation">איפוס הסיסמה שלך</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve" approved="yes">
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve" approved="no">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="final">אל דאגה, Kimai יעזור לך ליצור סיסמה חדשה!</target>
<target state="needs-translation">אל דאגה, Kimai יעזור לך ליצור סיסמה חדשה!</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" xml:space="preserve" approved="yes">
<source>absence_created_supervisor_subject</source>

View File

@@ -22,21 +22,21 @@
<source>registration.intro</source>
<target state="translated">Prijavio/prijavila si se na Kimaijevo evidentiranje vremena s e-mail adresom %email%. Aktiviraj svoj račun u idućih par sati prije nego što poveznica istekne.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Obnovi lozinku</target>
<target state="needs-translation">Obnovi lozinku</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">Ne sjećaš se više kako glasi lozinka?</target>
<target state="needs-translation">Ne sjećaš se više kako glasi lozinka?</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Obnovi lozinku</target>
<target state="needs-translation">Obnovi lozinku</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">Ne brini, Kimai će ti pomoći stvoriti novu!</target>
<target state="needs-translation">Ne brini, Kimai će ti pomoći stvoriti novu!</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject">
<source>absence_created_supervisor_subject</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target>Ezzel az e-mail címmel regisztrált a Kimai időrögzítőbe: %email%. Aktiválja a fiókját pár órán belül, mielőtt a link lejár.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Jelszó visszaállítása</target>
<target state="needs-translation">Jelszó visszaállítása</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Ez megtörténhet…</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Jelszó visszaállítása</target>
<target state="needs-translation">Jelszó visszaállítása</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -2,17 +2,17 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="id" datatype="plaintext" original="email.en.xlf">
<body>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target state="translated">Reset kata sandi Anda</target>
<target state="needs-translation">Reset kata sandi Anda</target>
</trans-unit>
<trans-unit id="pjr3FDv" resname="registration.subject">
<source>registration.subject</source>
<target state="translated">Aktifkan akun Anda</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target state="translated">Reset kata sandi Anda</target>
<target state="needs-translation">Reset kata sandi Anda</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>

View File

@@ -22,21 +22,21 @@
<source>You signed up at the Kimai time-tracker with the e-mail address %email%. Please activate your account in the next few hours, before the link expires.</source>
<target state="final">Ti sei registrato al time-tracker Kimai con l'indirizzo e-mail %email%. Attiva il tuo account nelle prossime ore, prima che il collegamento scada.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject" approved="yes">
<trans-unit id="1ZLD_Y0" resname="reset.subject" approved="yes" xml:space="preserve">
<source>reset.subject</source>
<target state="final">Reimposta password</target>
<target state="final">Accedi a Kimai</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" approved="yes" xml:space="preserve">
<source>Forgot your password?</source>
<target state="final">Hai dimenticato la password?</target>
<target state="final">Accedi al tuo account</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button" approved="yes">
<trans-unit id="qbt.84Y" resname="reset.button" approved="yes" xml:space="preserve">
<source>reset.button</source>
<target state="final">Reimposta password</target>
<target state="final">Registrazione</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" approved="yes" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="final">Non preoccuparti, Kimai ti aiuterà a crearne una nuova!</target>
<target state="final">Questo link sarà valido solo per i prossimi 15 minuti.</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" approved="yes">
<source>absence_created_supervisor_subject</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target>이메일 %email%로 Kimai 시간 트래커에 등록했습니다. 이제 링크가 만료되기 전에 사용자 계정을 활성화해야합니다.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>사용자 비밀번호 재설정</target>
<target state="needs-translation">사용자 비밀번호 재설정</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">그럴 수 있습니다…</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>사용자 비밀번호 재설정</target>
<target state="needs-translation">사용자 비밀번호 재설정</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target>Du har registrert deg på Kimai-tidssporer med e-posten %email%. Aktiver kontoen din i løpet av de neste timene før lenken utløper.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Tilbakestill passordet</target>
<target state="needs-translation">Tilbakestill passordet</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Det kan skje …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Tilbakestill passordet</target>
<target state="needs-translation">Tilbakestill passordet</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,21 +22,21 @@
<source>registration.intro</source>
<target>U hebt zich aangemeld bij de Kimai tijdregistratie met de e-mail %email%. Activeer uw account alstublieft in de komende uren, voordat de link afloopt.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Reset uw wachtwoord</target>
<target state="needs-translation">Reset uw wachtwoord</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">Paswoord vergeten?</target>
<target state="needs-translation">Paswoord vergeten?</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Reset uw wachtwoord</target>
<target state="needs-translation">Reset uw wachtwoord</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">Maakt u zich geen zorgen, Kimai zal u helpen om er een nieuw te maken!</target>
<target state="needs-translation">Maakt u zich geen zorgen, Kimai zal u helpen om er een nieuw te maken!</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" xml:space="preserve">
<source>absence_created_supervisor_subject</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target state="translated">Zarejestrowałeś się w w rejestratorze czasu Kimai za pomocą e-maila %email%. Aktywuj konto w ciągu kilku następnych godzin, zanim link wygaśnie.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Zresetuj swoje hasło</target>
<target state="needs-translation">Zresetuj swoje hasło</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">To może się zdarzyć…</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Zresetuj swoje hasło</target>
<target state="needs-translation">Zresetuj swoje hasło</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -16,19 +16,19 @@
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">Não se preocupe, o Kimai vai ajudá-lo a criar uma nova!</target>
<target state="needs-translation">Não se preocupe, o Kimai vai ajudá-lo a criar uma nova!</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Redefinir a palavra-passe</target>
<target state="needs-translation">Redefinir a palavra-passe</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">Esqueceu-se da palavra-passe?</target>
<target state="needs-translation">Esqueceu-se da palavra-passe?</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Redefinir a palavra-passe</target>
<target state="needs-translation">Redefinir a palavra-passe</target>
</trans-unit>
<trans-unit id="IkjnHoP" resname="registration.intro" xml:space="preserve">
<source>You signed up at the Kimai time-tracker with the e-mail address %email%. Please activate your account in the next few hours, before the link expires.</source>

View File

@@ -22,21 +22,21 @@
<source>registration.intro</source>
<target>Você se inscreveu no rastreador de tempo Kimai com o e-mail %email%. ative a sua conta nas próximas horas, antes que o link expire.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Redefina a sua senha</target>
<target state="translated">Faça login no Kimai</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">Esqueceu a sua senha?</target>
<target state="translated">Faça login em sua conta</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Redefina a sua senha</target>
<target state="translated">Entrar</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">Não se preocupe, o Kimai vai ajudá-lo a criar uma nova!</target>
<target state="translated">Esse link tem validade para os próximos 15 minutos.</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject">
<source>absence_created_supervisor_subject</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target>Te-ai înscris la Kimai time-tracker cu adresa de e-mail %email%. Vă rugăm să vă activați contul în următoarele câteva ore, înainte ca link-ul să expire.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Resetează-ți parola</target>
<target state="needs-translation">Resetează-ți parola</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Asta se poate întâmpla …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Resetează-ți parola</target>
<target state="needs-translation">Resetează-ți parola</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target>Вы зарегистрировались в Kimai time-tracker с электронным адресом %email%. Пожалуйста, активируйте свою учетную запись в течение нескольких часов, пока ссылка не истекла.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Сбросить пароль</target>
<target state="needs-translation">Сбросить пароль</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Это может произойти …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Сбросить пароль</target>
<target state="needs-translation">Сбросить пароль</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target>Prihlásili ste sa do Kimai sledovanie času s emailom %email%. Teraz si musíte Váš účet aktivovať v priebeh niekoľkých hodín predtým než vyprší platnosť linku.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Resetujte si heslo</target>
<target state="needs-translation">Resetujte si heslo</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">To sa môže stať…</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Resetovať heslo</target>
<target state="needs-translation">Resetovať heslo</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,17 +22,17 @@
<source>You signed up at the Kimai time-tracker with the e-mail address %email%. Please activate your account in the next few hours, before the link expires.</source>
<target state="translated">Du har registrerat dig hos Kimai tidrapportering med e-postadressen %email%. Vänligen aktivera ditt konto inom de närmaste timmarna, innan länken går ut och blir ogiltig.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target state="translated">Återställ ditt lösenord</target>
<target state="needs-translation">Återställ ditt lösenord</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Det kan hända …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target state="translated">Återställ ditt lösenord</target>
<target state="needs-translation">Återställ ditt lösenord</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,21 +22,21 @@
<source>You signed up at the Kimai time-tracker with the e-mail address %email%. Please activate your account in the next few hours, before the link expires.</source>
<target state="translated">Kimai zaman izleyicisine %email% e-posta adresiyle kaydoldunuz. Lütfen bağlantının süresi dolmadan birkaç saat içinde hesabınızı etkinleştirin.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Parolanızı sıfırlayın</target>
<target state="needs-translation">Parolanızı sıfırlayın</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">Parolanızı mı unuttunuz?</target>
<target state="needs-translation">Parolanızı mı unuttunuz?</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Parolanızı sıfırlayın</target>
<target state="needs-translation">Parolanızı sıfırlayın</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">Endişelenmeyin, Kimai yeni bir tane oluşturmanıza yardım edecek!</target>
<target state="needs-translation">Endişelenmeyin, Kimai yeni bir tane oluşturmanıza yardım edecek!</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject">
<source>absence_created_supervisor_subject</source>

View File

@@ -22,17 +22,17 @@
<source>registration.intro</source>
<target state="translated">Ви зареєструвалися у реєстраторі часу Kimai з електронною поштою %email%. Будь ласка, активуйте свій обліковий запис у найближчі кілька годин, доки не сплив термін дії посилання.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target state="translated">Скинути пароль</target>
<target state="needs-translation">Скинути пароль</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="needs-translation">Таке може статися …</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target state="translated">Скинути пароль</target>
<target state="needs-translation">Скинути пароль</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>

View File

@@ -22,21 +22,21 @@
<source>registration.intro</source>
<target>Bạn đã đăng ký theo dõi thời gian Kimai với email %email%. Bây giờ bạn phải kích hoạt tài khoản của mình trong vài giờ tới, trước khi liên kết hết hạn.</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>Đặt lại mật khẩu của bạn</target>
<target state="needs-translation">Đặt lại mật khẩu của bạn</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve">
<source>Forgot your password?</source>
<target state="translated">Quên mật khẩu?</target>
<target state="needs-translation">Quên mật khẩu?</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target>Đặt lại mật khẩu của bạn</target>
<target state="needs-translation">Đặt lại mật khẩu của bạn</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="translated">Đừng lo, Kimai sẽ giúp bạn tạo mật khẩu mới!</target>
<target state="needs-translation">Đừng lo, Kimai sẽ giúp bạn tạo mật khẩu mới!</target>
</trans-unit>
<trans-unit id="yohIAA_" resname="absence_created_supervisor_subject" xml:space="preserve">
<source>absence_created_supervisor_subject</source>

View File

@@ -10,9 +10,9 @@
<source>registration.title</source>
<target>欢迎 %username%</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve">
<source>reset.subject</source>
<target>重置您的密码</target>
<target state="needs-translation">重置您的密码</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve">
<source>Don't worry, Kimai will help you create a new one!</source>
@@ -30,9 +30,9 @@
<source>Forgot your password?</source>
<target state="needs-translation">这可能发生…</target>
</trans-unit>
<trans-unit id="qbt.84Y" resname="reset.button">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve">
<source>reset.button</source>
<target state="translated">重置您的密码</target>
<target state="needs-translation">重置您的密码</target>
</trans-unit>
<trans-unit id="IkjnHoP" resname="registration.intro">
<source>registration.intro</source>

View File

@@ -2,21 +2,21 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="zh-Hant" datatype="plaintext" original="email.en.xlf">
<body>
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve" approved="yes">
<trans-unit id="qbt.84Y" resname="reset.button" xml:space="preserve" approved="no">
<source>reset.button</source>
<target state="final">重設您的密碼</target>
<target state="needs-translation">重設您的密碼</target>
</trans-unit>
<trans-unit id="pjr3FDv" resname="registration.subject" xml:space="preserve" approved="yes">
<source>registration.subject</source>
<target state="final">啟用您的帳號</target>
</trans-unit>
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve" approved="yes">
<trans-unit id="1ZLD_Y0" resname="reset.subject" xml:space="preserve" approved="no">
<source>reset.subject</source>
<target state="final">重設您的密碼</target>
<target state="needs-translation">重設您的密碼</target>
</trans-unit>
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve" approved="yes">
<trans-unit id="G2mhn_2" resname="reset.title" xml:space="preserve" approved="no">
<source>Forgot your password?</source>
<target state="final">忘記密碼了嗎?</target>
<target state="needs-translation">忘記密碼了嗎?</target>
</trans-unit>
<trans-unit id="YMLel3P" resname="registration.button" xml:space="preserve" approved="yes">
<source>registration.button</source>
@@ -30,9 +30,9 @@
<source>registration.title</source>
<target state="final">歡迎 %username%</target>
</trans-unit>
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve" approved="yes">
<trans-unit id="rhkMAep" resname="reset.intro" xml:space="preserve" approved="no">
<source>Don't worry, Kimai will help you create a new one!</source>
<target state="final">別擔心Kimai 將協助您設定新的密碼!</target>
<target state="needs-translation">別擔心Kimai 將協助您設定新的密碼!</target>
</trans-unit>
<trans-unit id="8jDJmrK" resname="automated_email_dont_answer" xml:space="preserve" approved="yes">
<source>automated_email_dont_answer</source>

View File

@@ -594,9 +594,9 @@
<source>tag</source>
<target>الوسوم</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>الميزانية الزمنية</target>
<target state="needs-translation">الميزانية الزمنية</target>
</trans-unit>
<trans-unit id="hlu4iX5" resname="error.no_comments_found">
<source>error.no_comments_found</source>

View File

@@ -178,9 +178,9 @@
<source>budget</source>
<target>Rozpočet</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Hodinový rozpočet</target>
<target state="needs-translation">Hodinový rozpočet</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -198,9 +198,9 @@
<source>budget</source>
<target>Budget</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Tidsbudget</target>
<target state="needs-translation">Tidsbudget</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -1798,6 +1798,10 @@
<source>costs</source>
<target>Kosten</target>
</trans-unit>
<trans-unit id="FOvlalA" resname="break">
<source>break</source>
<target>Pause</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -734,9 +734,9 @@
<source>activity</source>
<target>Tätigkeit</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Zeit-Budget</target>
<target state="needs-translation">Zeit-Budget</target>
</trans-unit>
<trans-unit id="CvlqjtY" resname="budget">
<source>budget</source>

View File

@@ -258,9 +258,9 @@
<source>budget</source>
<target>Προϋπολογισμός</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Χρονικός Προϋπολογισμός</target>
<target state="needs-translation">Χρονικός Προϋπολογισμός</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -1798,6 +1798,10 @@
<source>costs</source>
<target>Costs</target>
</trans-unit>
<trans-unit id="FOvlalA" resname="break">
<source>break</source>
<target>Break</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -218,9 +218,9 @@
<source>budget</source>
<target>Buĝeto</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Tempobuĝeto</target>
<target state="needs-translation">Tempobuĝeto</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -196,7 +196,7 @@
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target state="translated">Presupuesto de tiempo</target>
<target state="needs-translation">Presupuesto de tiempo</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -206,9 +206,9 @@
<source>budget</source>
<target>Aurrekontua</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Ordu-poltsa</target>
<target state="needs-translation">Ordu-poltsa</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -246,9 +246,9 @@
<source>activity</source>
<target>فعالیت</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>بودجه زمان</target>
<target state="needs-translation">بودجه زمان</target>
</trans-unit>
<trans-unit id="CvlqjtY" resname="budget">
<source>budget</source>

View File

@@ -222,9 +222,9 @@
<source>budget</source>
<target state="translated">Budjetti</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target state="translated">Aika budjetti</target>
<target state="needs-translation">Aika budjetti</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -130,9 +130,9 @@
<source>budget</source>
<target>Fíggjarætlan</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Tíðarætlan</target>
<target state="needs-translation">Tíðarætlan</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -174,9 +174,9 @@
<source>budget</source>
<target>Budget</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Budget temps</target>
<target state="needs-translation">Budget temps</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -218,9 +218,9 @@
<source>budget</source>
<target>תקציב</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>תקציב זמן</target>
<target state="needs-translation">תקציב זמן</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -270,9 +270,9 @@
<source>budget</source>
<target>Budžet</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Vremenski budžet</target>
<target state="needs-translation">Vremenski budžet</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -174,9 +174,9 @@
<source>budget</source>
<target>Költségvetés</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Meghatározott időkeret</target>
<target state="needs-translation">Meghatározott időkeret</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -744,7 +744,7 @@
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target state="translated">Anggaran waktu</target>
<target state="needs-translation">Anggaran waktu</target>
</trans-unit>
<trans-unit id="QEMUsfS" resname="hours" xml:space="preserve">
<source>hours</source>

View File

@@ -222,9 +222,9 @@
<source>budget</source>
<target state="final">Βudget</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget" approved="yes">
<trans-unit id="qbD0dHa" resname="timeBudget" approved="yes" xml:space="preserve">
<source>timeBudget</source>
<target state="final">Budget di tempo</target>
<target state="final">Quota oraria</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity" approved="yes">
<source>activity</source>
@@ -1790,6 +1790,14 @@
<source>work_days</source>
<target state="final">Giorni lavorativi</target>
</trans-unit>
<trans-unit id="0jvo0RQ" resname="profit" xml:space="preserve" approved="yes">
<source>profit</source>
<target state="final">Profitto</target>
</trans-unit>
<trans-unit id="vN5pKOU" resname="costs" xml:space="preserve" approved="yes">
<source>costs</source>
<target state="final">Costi</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -174,9 +174,9 @@
<source>budget</source>
<target>予算</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>タイム・バジェット(時間予算)</target>
<target state="needs-translation">タイム・バジェット(時間予算)</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -178,9 +178,9 @@
<source>budget</source>
<target state="translated">예산</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target state="translated">예상 시간</target>
<target state="needs-translation">예상 시간</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -154,9 +154,9 @@
<source>budget</source>
<target>Budsjett</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Tidsbudsjett</target>
<target state="needs-translation">Tidsbudsjett</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -194,9 +194,9 @@
<source>budget</source>
<target>Budget</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target state="translated">Tijdsbegroting</target>
<target state="needs-translation">Tijdsbegroting</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -210,9 +210,9 @@
<source>tags</source>
<target state="translated">ਟੈਗ</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target state="translated">ਸਮਾਂ ਬਜਟ</target>
<target state="needs-translation">ਸਮਾਂ ਬਜਟ</target>
</trans-unit>
<trans-unit id="E_bHhGR" resname="hourlyRate">
<source>hourlyRate</source>

View File

@@ -206,9 +206,9 @@
<source>budget</source>
<target>Budżet</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Budżet czasu</target>
<target state="needs-translation">Budżet czasu</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -174,9 +174,9 @@
<source>budget</source>
<target>Orçamento</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Orçamento de tempo</target>
<target state="needs-translation">Orçamento de tempo</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -174,9 +174,9 @@
<source>budget</source>
<target state="translated">Orçamento</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Orçamento de tempo</target>
<target state="translated">Cota por hora</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>
@@ -1790,6 +1790,14 @@
<source>hours_per_month</source>
<target state="translated">Horas por mês</target>
</trans-unit>
<trans-unit id="0jvo0RQ" resname="profit" xml:space="preserve" approved="no">
<source>profit</source>
<target state="translated">Lucro</target>
</trans-unit>
<trans-unit id="vN5pKOU" resname="costs" xml:space="preserve" approved="no">
<source>costs</source>
<target state="translated">Custos</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -218,9 +218,9 @@
<source>budget</source>
<target>Buget</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target>Buget de timp</target>
<target state="needs-translation">Buget de timp</target>
</trans-unit>
<trans-unit id="BlGGO.X" resname="activity">
<source>activity</source>

View File

@@ -902,9 +902,9 @@
<source>layout</source>
<target state="translated">Отображение: формат</target>
</trans-unit>
<trans-unit id="qbD0dHa" resname="timeBudget">
<trans-unit id="qbD0dHa" resname="timeBudget" xml:space="preserve">
<source>timeBudget</source>
<target state="translated">Бюджет времени</target>
<target state="needs-translation">Бюджет времени</target>
</trans-unit>
<trans-unit id="ypVQO7o" resname="menu.reporting">
<source>menu.reporting</source>

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