0
0
Fork 0
mirror of https://github.com/kevinpapst/kimai2.git synced 2025-05-11 12:15:43 +00:00

[FEATURE] Add new widgets

This commit is contained in:
Simon Schaufelberger 2025-02-23 22:29:30 +01:00
parent cce49cc409
commit 0006b4dd46
54 changed files with 1207 additions and 83 deletions

View file

@ -42,6 +42,13 @@ parameters:
containerXmlPath: %rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml
ignoreErrors:
- '#^Method .*\(\) has parameter \$builder with generic interface Symfony\\Component\\Form\\FormBuilderInterface but does not specify its types\: TData$#'
-
message: '#^Call to an undefined method DateTimeInterface\:\:modify\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Timesheet/DateTimeFactory.php
-
message: "#^Parameter \\#1 \\$user of class App\\\\Event\\\\PageActionsEvent constructor expects App\\\\Entity\\\\User, App\\\\Entity\\\\User\\|null given\\.$#"
count: 4
@ -1482,11 +1489,6 @@ parameters:
count: 1
path: src/Event/PermissionsEvent.php
-
message: "#^Method App\\\\Event\\\\RevenueStatisticEvent\\:\\:getRevenue\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Event/RevenueStatisticEvent.php
-
message: "#^Method App\\\\Event\\\\ThemeJavascriptTranslationsEvent\\:\\:getTranslations\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
@ -1497,11 +1499,6 @@ parameters:
count: 2
path: src/Event/UserPreferenceEvent.php
-
message: "#^Method App\\\\Event\\\\UserRevenueStatisticEvent\\:\\:getRevenue\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Event/UserRevenueStatisticEvent.php
-
message: "#^Method App\\\\EventSubscriber\\\\Actions\\\\AbstractActionsSubscriber\\:\\:isGranted\\(\\) has parameter \\$attributes with no type specified\\.$#"
count: 1

View file

@ -35,6 +35,9 @@ final class RevenueStatisticEvent extends Event
return $this->end;
}
/**
* @return array<string, float>
*/
public function getRevenue(): array
{
return $this->revenue;

View file

@ -34,6 +34,9 @@ final class UserRevenueStatisticEvent extends Event
return $this->event->getEnd();
}
/**
* @return array<string, float>
*/
public function getRevenue(): array
{
return $this->event->getRevenue();

View file

@ -171,24 +171,33 @@ final class DateTimeFactory
return $defaultDate;
}
$financialYear = $this->createDateTime($financialYear);
$financialYear = $financialYear->setDate((int) $defaultDate->format('Y'), (int) $financialYear->format('m'), (int) $financialYear->format('d'));
$financialYear = $financialYear->setTime(0, 0, 0);
$year = $this->createDateTime($financialYear);
$year = $year->setDate((int) $defaultDate->format('Y'), (int) $year->format('m'), (int) $year->format('d'));
$year = $year->setTime(0, 0, 0);
$today = $this->createDateTime('00:00:00');
if ($financialYear > $today) {
$financialYear = $financialYear->modify('-1 year');
if ($year > $today) {
$year = $year->modify('-1 year');
}
return $financialYear;
return $year;
}
public function createStartOfPreviousFinancialYear(?string $financialYear = null): DateTimeInterface
{
return $this->createStartOfFinancialYear($financialYear)->modify('-2 years');
}
public function createEndOfFinancialYear(DateTimeInterface $financialYear): DateTimeInterface
{
$yearEnd = DateTime::createFromInterface($financialYear);
$yearEnd = $yearEnd->modify('+1 year')->modify('-1 day')->setTime(23, 59, 59);
return $yearEnd;
return $yearEnd->modify('+1 year')->modify('-1 day')->setTime(23, 59, 59);
}
public function createEndOfPreviousFinancialYear(DateTimeInterface $financialYear): DateTimeInterface
{
return $this->createEndOfFinancialYear($financialYear);
}
}

View file

@ -14,9 +14,9 @@ use App\Timesheet\DateTimeFactory;
abstract class AbstractCounterYear extends AbstractWidgetType
{
private bool $isFinancialYear = false;
protected bool $isFinancialYear = false;
public function __construct(private SystemConfiguration $systemConfiguration)
public function __construct(protected SystemConfiguration $systemConfiguration)
{
}
@ -25,8 +25,8 @@ abstract class AbstractCounterYear extends AbstractWidgetType
*/
public function getData(array $options = []): mixed
{
$begin = $this->createDate('01 january this year 00:00:00');
$end = $this->createDate('31 december this year 23:59:59');
$begin = $this->createYearStartDate();
$end = $this->createYearEndDate();
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$factory = new DateTimeFactory($this->getTimezone());

View file

@ -86,6 +86,26 @@ abstract class AbstractWidget implements WidgetInterface
return new \DateTime($date, $this->getTimezone());
}
protected function createYearStartDate(): \DateTime
{
return $this->createDate('01 january this year 00:00:00');
}
protected function createYearEndDate(): \DateTime
{
return $this->createDate('31 december this year 23:59:59');
}
protected function createPreviousYearStartDate(): \DateTime
{
return $this->createDate('01 january previous year 00:00:00');
}
protected function createPreviousYearEndDate(): \DateTime
{
return $this->createDate('31 december previous year 23:59:59');
}
protected function createMonthStartDate(): \DateTime
{
return $this->createDate('first day of this month 00:00:00');
@ -96,16 +116,36 @@ abstract class AbstractWidget implements WidgetInterface
return $this->createDate('last day of this month 23:59:59');
}
protected function createPreviousMonthStartDate(): \DateTime
{
return $this->createDate('first day of previous month 00:00:00');
}
protected function createPreviousMonthEndDate(): \DateTime
{
return $this->createDate('last day of previous month 23:59:59');
}
protected function createWeekStartDate(): \DateTime
{
return $this->createDate('monday this week 00:00:00');
}
protected function createPreviousWeekStartDate(): \DateTime
{
return $this->createDate('monday previous week 00:00:00');
}
protected function createWeekEndDate(): \DateTime
{
return $this->createDate('sunday this week 23:59:59');
}
protected function createPreviousWeekEndDate(): \DateTime
{
return $this->createDate('sunday previous week 23:59:59');
}
protected function createTodayStartDate(): \DateTime
{
return $this->createDate('00:00:00');
@ -116,6 +156,16 @@ abstract class AbstractWidget implements WidgetInterface
return $this->createDate('23:59:59');
}
protected function createYesterdayStartDate(): \DateTime
{
return $this->createDate('yesterday 00:00:00');
}
protected function createYesterdayEndDate(): \DateTime
{
return $this->createDate('yesterday 23:59:59');
}
public function getTimezone(): \DateTimeZone
{
$timezone = date_default_timezone_get();

View file

@ -16,7 +16,7 @@ use App\Widget\WidgetInterface;
final class ActiveTimesheets extends AbstractWidgetType
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -56,7 +56,7 @@ final class ActiveTimesheets extends AbstractWidgetType
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->countActiveEntries();

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class ActiveUsersMonth extends AbstractActiveUsers
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -36,7 +36,7 @@ final class ActiveUsersMonth extends AbstractActiveUsers
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->countActiveUsers($this->createMonthStartDate(), $this->createMonthEndDate(), null);

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class ActiveUsersToday extends AbstractActiveUsers
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -36,7 +36,7 @@ final class ActiveUsersToday extends AbstractActiveUsers
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->countActiveUsers($this->createTodayStartDate(), $this->createTodayEndDate(), null);

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class ActiveUsersTotal extends AbstractActiveUsers
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -36,7 +36,7 @@ final class ActiveUsersTotal extends AbstractActiveUsers
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->countActiveUsers(null, null, null);

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class ActiveUsersWeek extends AbstractActiveUsers
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -36,7 +36,7 @@ final class ActiveUsersWeek extends AbstractActiveUsers
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->countActiveUsers($this->createWeekStartDate(), $this->createWeekEndDate(), null);

View file

@ -16,8 +16,10 @@ use App\Widget\WidgetInterface;
final class ActiveUsersYear extends AbstractCounterYear
{
public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration
) {
parent::__construct($systemConfiguration);
}
@ -36,7 +38,7 @@ final class ActiveUsersYear extends AbstractCounterYear
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): int
{
try {
return $this->repository->countActiveUsers($begin, $end, null);

View file

@ -29,8 +29,9 @@ final class AmountMonth extends AbstractAmountPeriod
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
return $this->getRevenue($this->createMonthStartDate(), $this->createMonthEndDate(), $options);
}

View file

@ -0,0 +1,43 @@
<?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\Widget\Type;
use App\Widget\WidgetInterface;
final class AmountPreviousMonth extends AbstractAmountPeriod
{
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
}
public function getId(): string
{
return 'AmountPreviousMonth';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): array
{
return $this->getRevenue($this->createPreviousMonthStartDate(), $this->createPreviousMonthEndDate(), $options);
}
public function getPermissions(): array
{
return ['view_all_data'];
}
}

View file

@ -0,0 +1,43 @@
<?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\Widget\Type;
use App\Widget\WidgetInterface;
final class AmountPreviousWeek extends AbstractAmountPeriod
{
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
}
public function getId(): string
{
return 'AmountPreviousWeek';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): array
{
return $this->getRevenue($this->createPreviousWeekStartDate(), $this->createPreviousWeekEndDate(), $options);
}
public function getPermissions(): array
{
return ['view_all_data'];
}
}

View file

@ -0,0 +1,105 @@
<?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\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Event\RevenueStatisticEvent;
use App\Model\Revenue;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
final class AmountPreviousYear extends AbstractCounterYear
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration,
private readonly EventDispatcherInterface $dispatcher
) {
parent::__construct($systemConfiguration);
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'money',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): array
{
$begin = $this->createPreviousYearStartDate();
$end = $this->createPreviousYearEndDate();
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$factory = new DateTimeFactory($this->getTimezone());
$begin = $factory->createStartOfPreviousFinancialYear($financialYear);
$end = $factory->createEndOfPreviousFinancialYear($begin);
$this->isFinancialYear = true;
}
return $this->getYearData($begin, $end, $options);
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): array
{
try {
/** @var array<Revenue> $data */
$data = $this->repository->getRevenue($begin, $end, null);
$event = new RevenueStatisticEvent($begin, $end);
foreach ($data as $row) {
$event->addRevenue($row->getCurrency(), $row->getAmount());
}
$this->dispatcher->dispatch($event);
return $event->getRevenue();
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
public function getId(): string
{
return 'AmountPreviousYear';
}
protected function getFinancialYearTitle(): string
{
return 'stats.amountPreviousFinancialYear';
}
public function getTemplateName(): string
{
return 'widget/widget-counter-money.html.twig';
}
public function getPermissions(): array
{
return ['view_all_data'];
}
}

View file

@ -29,8 +29,9 @@ final class AmountToday extends AbstractAmountPeriod
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
return $this->getRevenue($this->createTodayStartDate(), $this->createTodayEndDate(), $options);
}

View file

@ -29,8 +29,9 @@ final class AmountWeek extends AbstractAmountPeriod
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
return $this->getRevenue($this->createWeekStartDate(), $this->createWeekEndDate(), $options);
}

View file

@ -19,8 +19,11 @@ use Psr\EventDispatcher\EventDispatcherInterface;
final class AmountYear extends AbstractCounterYear
{
public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher)
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration,
private readonly EventDispatcherInterface $dispatcher
) {
parent::__construct($systemConfiguration);
}
@ -38,8 +41,9 @@ final class AmountYear extends AbstractCounterYear
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): array
{
try {
/** @var array<Revenue> $data */

View file

@ -0,0 +1,43 @@
<?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\Widget\Type;
use App\Widget\WidgetInterface;
final class AmountYesterday extends AbstractAmountPeriod
{
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
}
public function getId(): string
{
return 'AmountYesterday';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): array
{
return $this->getRevenue($this->createYesterdayStartDate(), $this->createYesterdayEndDate(), $options);
}
public function getPermissions(): array
{
return ['view_all_data'];
}
}

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class DurationMonth extends AbstractCounterDuration
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -46,7 +46,7 @@ final class DurationMonth extends AbstractCounterDuration
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createMonthStartDate(), $this->createMonthEndDate(), null);

View file

@ -0,0 +1,59 @@
<?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\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
final class DurationPreviousMonth extends AbstractCounterDuration
{
public function __construct(private readonly TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_other_timesheet'];
}
public function getId(): string
{
return 'DurationPreviousMonth';
}
public function getTitle(): string
{
return 'stats.durationPreviousMonth';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createPreviousMonthStartDate(), $this->createPreviousMonthEndDate(), null);
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View file

@ -0,0 +1,59 @@
<?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\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
final class DurationPreviousWeek extends AbstractCounterDuration
{
public function __construct(private readonly TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_other_timesheet'];
}
public function getId(): string
{
return 'DurationPreviousWeek';
}
public function getTitle(): string
{
return 'stats.durationPreviousWeek';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createPreviousWeekStartDate(), $this->createPreviousWeekEndDate(), null);
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View file

@ -0,0 +1,90 @@
<?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\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
final class DurationPreviousYear extends AbstractCounterYear
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration
) {
parent::__construct($systemConfiguration);
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'duration',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): int
{
$begin = $this->createPreviousYearStartDate();
$end = $this->createPreviousYearEndDate();
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$factory = new DateTimeFactory($this->getTimezone());
$begin = $factory->createStartOfPreviousFinancialYear($financialYear);
$end = $factory->createEndOfPreviousFinancialYear($begin);
$this->isFinancialYear = true;
}
return $this->getYearData($begin, $end, $options);
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($begin, $end, null);
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
public function getId(): string
{
return 'DurationPreviousYear';
}
public function getPermissions(): array
{
return ['view_other_timesheet'];
}
protected function getFinancialYearTitle(): string
{
return 'stats.durationPreviousFinancialYear';
}
public function getTemplateName(): string
{
return 'widget/widget-counter-duration.html.twig';
}
}

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class DurationToday extends AbstractCounterDuration
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class DurationTotal extends AbstractCounterDuration
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -46,7 +46,7 @@ final class DurationTotal extends AbstractCounterDuration
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange(null, null, null);

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class DurationWeek extends AbstractCounterDuration
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -46,7 +46,7 @@ final class DurationWeek extends AbstractCounterDuration
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createWeekStartDate(), $this->createWeekEndDate(), null);

View file

@ -16,8 +16,10 @@ use App\Widget\WidgetInterface;
final class DurationYear extends AbstractCounterYear
{
public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration
) {
parent::__construct($systemConfiguration);
}
@ -36,7 +38,7 @@ final class DurationYear extends AbstractCounterYear
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($begin, $end, null);

View file

@ -0,0 +1,59 @@
<?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\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
final class DurationYesterday extends AbstractCounterDuration
{
public function __construct(private readonly TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_other_timesheet'];
}
public function getId(): string
{
return 'DurationYesterday';
}
public function getTitle(): string
{
return 'stats.durationYesterday';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createYesterdayStartDate(), $this->createYesterdayEndDate(), null);
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class TotalsActivity extends AbstractWidget
{
public function __construct(private ActivityRepository $activity)
public function __construct(private readonly ActivityRepository $activity)
{
}
@ -40,7 +40,7 @@ final class TotalsActivity extends AbstractWidget
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
$user = $this->getUser();
$query = new ActivityQuery();

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class TotalsCustomer extends AbstractWidget
{
public function __construct(private CustomerRepository $customer)
public function __construct(private readonly CustomerRepository $customer)
{
}
@ -40,7 +40,7 @@ final class TotalsCustomer extends AbstractWidget
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
$user = $this->getUser();
$query = new CustomerQuery();

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class TotalsProject extends AbstractWidget
{
public function __construct(private ProjectRepository $project)
public function __construct(private readonly ProjectRepository $project)
{
}
@ -40,7 +40,7 @@ final class TotalsProject extends AbstractWidget
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
$user = $this->getUser();
$query = new ProjectQuery();

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class TotalsUser extends AbstractWidget
{
public function __construct(private UserRepository $repository)
public function __construct(private readonly UserRepository $repository)
{
}
@ -35,7 +35,7 @@ final class TotalsUser extends AbstractWidget
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
$user = $this->getUser();
$query = new UserQuery();

View file

@ -29,8 +29,9 @@ final class UserAmountMonth extends AbstractUserRevenuePeriod
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
return $this->getRevenue($this->createMonthStartDate(), $this->createMonthEndDate(), $options);
}

View file

@ -0,0 +1,38 @@
<?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\Widget\Type;
use App\Widget\WidgetInterface;
final class UserAmountPreviousMonth extends AbstractUserRevenuePeriod
{
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
}
public function getId(): string
{
return 'UserAmountPreviousMonth';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): array
{
return $this->getRevenue($this->createPreviousMonthStartDate(), $this->createPreviousMonthEndDate(), $options);
}
}

View file

@ -0,0 +1,37 @@
<?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\Widget\Type;
use App\Widget\WidgetInterface;
final class UserAmountPreviousWeek extends AbstractUserRevenuePeriod
{
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
}
public function getId(): string
{
return 'UserAmountPreviousWeek';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
{
return $this->getRevenue($this->createPreviousWeekStartDate(), $this->createPreviousWeekEndDate(), $options);
}
}

View file

@ -0,0 +1,105 @@
<?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\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Event\UserRevenueStatisticEvent;
use App\Model\Revenue;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
final class UserAmountPreviousYear extends AbstractCounterYear
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration,
private readonly EventDispatcherInterface $dispatcher
) {
parent::__construct($systemConfiguration);
}
public function getTemplateName(): string
{
return 'widget/widget-counter-money.html.twig';
}
public function getPermissions(): array
{
return ['view_rate_own_timesheet'];
}
protected function getFinancialYearTitle(): string
{
return 'stats.amountPreviousFinancialYear';
}
public function getId(): string
{
return 'UserAmountPreviousYear';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'money',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): array
{
$begin = $this->createPreviousYearStartDate();
$end = $this->createPreviousYearEndDate();
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$factory = new DateTimeFactory($this->getTimezone());
$begin = $factory->createStartOfPreviousFinancialYear($financialYear);
$end = $factory->createEndOfPreviousFinancialYear($begin);
$this->isFinancialYear = true;
}
return $this->getYearData($begin, $end, $options);
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): array
{
try {
/** @var array<Revenue> $data */
$data = $this->repository->getRevenue($begin, $end, $this->getUser());
$event = new UserRevenueStatisticEvent($this->getUser(), $begin, $end);
foreach ($data as $row) {
$event->addRevenue($row->getCurrency(), $row->getAmount());
}
$this->dispatcher->dispatch($event);
return $event->getRevenue();
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View file

@ -29,8 +29,9 @@ final class UserAmountToday extends AbstractUserRevenuePeriod
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
return $this->getRevenue($this->createTodayStartDate(), $this->createTodayEndDate(), $options);
}

View file

@ -29,8 +29,9 @@ final class UserAmountTotal extends AbstractUserRevenuePeriod
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
return $this->getRevenue(null, null, $options);
}

View file

@ -29,8 +29,9 @@ final class UserAmountWeek extends AbstractUserRevenuePeriod
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
return $this->getRevenue($this->createWeekStartDate(), $this->createWeekEndDate(), $options);
}

View file

@ -19,8 +19,11 @@ use Psr\EventDispatcher\EventDispatcherInterface;
final class UserAmountYear extends AbstractCounterYear
{
public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher)
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration,
private readonly EventDispatcherInterface $dispatcher
) {
parent::__construct($systemConfiguration);
}
@ -58,8 +61,9 @@ final class UserAmountYear extends AbstractCounterYear
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): array
{
try {
/** @var array<Revenue> $data */

View file

@ -0,0 +1,38 @@
<?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\Widget\Type;
use App\Widget\WidgetInterface;
final class UserAmountYesterday extends AbstractUserRevenuePeriod
{
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
}
public function getId(): string
{
return 'UserAmountYesterday';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return array<string, float>
*/
public function getData(array $options = []): array
{
return $this->getRevenue($this->createYesterdayStartDate(), $this->createYesterdayEndDate(), $options);
}
}

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class UserDurationMonth extends AbstractCounterDuration
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -41,7 +41,7 @@ final class UserDurationMonth extends AbstractCounterDuration
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createMonthStartDate(), $this->createMonthEndDate(), $this->getUser());

View file

@ -0,0 +1,54 @@
<?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\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
final class UserDurationPreviousMonth extends AbstractCounterDuration
{
public function __construct(private readonly TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_own_timesheet'];
}
public function getId(): string
{
return 'UserDurationPreviousMonth';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createPreviousMonthStartDate(), $this->createPreviousMonthEndDate(), $this->getUser());
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View file

@ -0,0 +1,54 @@
<?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\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
final class UserDurationPreviousWeek extends AbstractCounterDuration
{
public function __construct(private readonly TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_own_timesheet'];
}
public function getId(): string
{
return 'UserDurationPreviousWeek';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createPreviousWeekStartDate(), $this->createPreviousWeekEndDate(), $this->getUser());
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View file

@ -0,0 +1,85 @@
<?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\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
final class UserDurationPreviousYear extends AbstractCounterYear
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration
) {
parent::__construct($systemConfiguration);
}
public function getId(): string
{
return 'UserDurationPreviousYear';
}
protected function getFinancialYearTitle(): string
{
return 'stats.durationPreviousFinancialYear';
}
public function getTemplateName(): string
{
return 'widget/widget-counter-duration.html.twig';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'duration',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): int
{
$begin = $this->createPreviousYearStartDate();
$end = $this->createPreviousYearEndDate();
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$factory = new DateTimeFactory($this->getTimezone());
$begin = $factory->createStartOfPreviousFinancialYear($financialYear);
$end = $factory->createEndOfPreviousFinancialYear($begin);
$this->isFinancialYear = true;
}
return $this->getYearData($begin, $end, $options);
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($begin, $end, $this->getUser());
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class UserDurationToday extends AbstractCounterDuration
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -41,7 +41,7 @@ final class UserDurationToday extends AbstractCounterDuration
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createTodayStartDate(), $this->createTodayEndDate(), $this->getUser());

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class UserDurationTotal extends AbstractCounterDuration
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -41,7 +41,7 @@ final class UserDurationTotal extends AbstractCounterDuration
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange(null, null, $this->getUser());

View file

@ -15,7 +15,7 @@ use App\Widget\WidgetInterface;
final class UserDurationWeek extends AbstractCounterDuration
{
public function __construct(private TimesheetRepository $repository)
public function __construct(private readonly TimesheetRepository $repository)
{
}
@ -41,7 +41,7 @@ final class UserDurationWeek extends AbstractCounterDuration
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createWeekStartDate(), $this->createWeekEndDate(), $this->getUser());

View file

@ -16,8 +16,10 @@ use App\Widget\WidgetInterface;
final class UserDurationYear extends AbstractCounterYear
{
public function __construct(private TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
{
public function __construct(
private readonly TimesheetRepository $repository,
SystemConfiguration $systemConfiguration
) {
parent::__construct($systemConfiguration);
}
@ -46,7 +48,7 @@ final class UserDurationYear extends AbstractCounterYear
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): mixed
protected function getYearData(\DateTimeInterface $begin, \DateTimeInterface $end, array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($begin, $end, $this->getUser());

View file

@ -0,0 +1,54 @@
<?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\Widget\Type;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
use App\Widget\WidgetInterface;
final class UserDurationYesterday extends AbstractCounterDuration
{
public function __construct(private readonly TimesheetRepository $repository)
{
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
@return array<string, string|bool|int|null|array<string, mixed>>
*/
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_own_timesheet'];
}
public function getId(): string
{
return 'UserDurationYesterday';
}
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
*/
public function getData(array $options = []): int
{
try {
return $this->repository->getDurationForTimeRange($this->createYesterdayStartDate(), $this->createYesterdayEndDate(), $this->getUser());
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View file

@ -9,6 +9,7 @@
namespace App\Widget\Type;
use App\Model\ProjectBudgetStatisticModel;
use App\Project\ProjectStatisticService;
use App\Repository\Loader\TeamLoader;
use App\Widget\WidgetInterface;
@ -19,8 +20,7 @@ final class UserTeamProjects extends AbstractWidget
public function __construct(
private readonly ProjectStatisticService $statisticService,
private readonly EntityManagerInterface $entityManager
)
{
) {
}
public function getWidth(): int
@ -61,8 +61,9 @@ final class UserTeamProjects extends AbstractWidget
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return ProjectBudgetStatisticModel[]
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
$user = $this->getUser();
$teams = $user->getTeams();

View file

@ -9,6 +9,7 @@
namespace App\Widget\Type;
use App\Entity\Team;
use App\Widget\WidgetInterface;
final class UserTeams extends AbstractWidget
@ -48,8 +49,9 @@ final class UserTeams extends AbstractWidget
/**
* @param array<string, string|bool|int|null|array<string, mixed>> $options
* @return Team[]
*/
public function getData(array $options = []): mixed
public function getData(array $options = []): array
{
return $this->getUser()->getTeams();
}

View file

@ -894,9 +894,9 @@
<source>stats.workingTimeFinancialYear</source>
<target>Financial year</target>
</trans-unit>
<trans-unit id="BXBFJP8" resname="stats.workingTime" xml:space="preserve" approved="yes">
<trans-unit id="BXBFJP8" resname="stats.workingTime">
<source>Working hours</source>
<target state="final">Working hours</target>
<target>Working hours</target>
</trans-unit>
<trans-unit id="Nx9_QiO" resname="revenue">
<source>revenue</source>
@ -906,22 +906,42 @@
<source>stats.durationToday</source>
<target>Working hours today</target>
</trans-unit>
<trans-unit id="7R2dowt" resname="stats.durationYesterday">
<source>stats.durationYesterday</source>
<target>Working hours yesterday</target>
</trans-unit>
<trans-unit id="XhKalZH" resname="stats.durationWeek">
<source>stats.durationWeek</source>
<target>Working hours this week</target>
</trans-unit>
<trans-unit id="t5QlmRi" resname="stats.durationPreviousWeek">
<source>stats.durationPreviousWeek</source>
<target>Working hours previous week</target>
</trans-unit>
<trans-unit id="uaOwf_P" resname="stats.durationMonth">
<source>stats.durationMonth</source>
<target>Working hours this month</target>
</trans-unit>
<trans-unit id="c7yE5.3" resname="stats.durationPreviousMonth">
<source>stats.durationPreviousMonth</source>
<target>Working hours previous month</target>
</trans-unit>
<trans-unit id="WqF84KR" resname="stats.durationYear">
<source>stats.durationYear</source>
<target>Working hours this year</target>
</trans-unit>
<trans-unit id="Uh1JUB5" resname="stats.durationPreviousYear">
<source>stats.durationPreviousYear</source>
<target>Working hours previous year</target>
</trans-unit>
<trans-unit id="xkugSAA" resname="stats.durationFinancialYear">
<source>stats.durationFinancialYear</source>
<target>Working hours this financial year</target>
</trans-unit>
<trans-unit id="MxLC46J" resname="stats.durationPreviousFinancialYear">
<source>stats.durationPreviousFinancialYear</source>
<target>Working hours previous financial year</target>
</trans-unit>
<trans-unit id="YtvPnl1" resname="stats.durationTotal">
<source>stats.durationTotal</source>
<target>Working hours total</target>
@ -930,18 +950,34 @@
<source>stats.userDurationToday</source>
<target>My working hours today</target>
</trans-unit>
<trans-unit id="k5ZZw6d" resname="stats.userDurationYesterday">
<source>stats.userDurationYesterday</source>
<target>My working hours yesterday</target>
</trans-unit>
<trans-unit id="DXcvOMM" resname="stats.userDurationWeek">
<source>stats.userDurationWeek</source>
<target>My working hours this week</target>
</trans-unit>
<trans-unit id="uxUdtKD" resname="stats.userDurationPreviousWeek">
<source>stats.userDurationPreviousWeek</source>
<target>My working hours previous week</target>
</trans-unit>
<trans-unit id="0WpgaGa" resname="stats.userDurationMonth">
<source>stats.userDurationMonth</source>
<target>My working hours this month</target>
</trans-unit>
<trans-unit id="Y_jQvXv" resname="stats.userDurationPreviousMonth">
<source>stats.userDurationPreviousMonth</source>
<target>My working hours previous month</target>
</trans-unit>
<trans-unit id="BjyLITj" resname="stats.userDurationYear">
<source>stats.userDurationYear</source>
<target>My working hours this year</target>
</trans-unit>
<trans-unit id="NVwZpsz" resname="stats.userDurationPreviousYear">
<source>stats.userDurationPreviousYear</source>
<target>My working hours previous year</target>
</trans-unit>
<trans-unit id="EedlgkM" resname="stats.userDurationTotal">
<source>stats.userDurationTotal</source>
<target>My working hours total</target>
@ -954,22 +990,42 @@
<source>stats.amountToday</source>
<target>Revenue today</target>
</trans-unit>
<trans-unit id="sP2cnvE" resname="stats.amountYesterday">
<source>stats.amountYesterday</source>
<target>Revenue yesterday</target>
</trans-unit>
<trans-unit id="xnJvYAE" resname="stats.amountWeek">
<source>stats.amountWeek</source>
<target>Revenue this week</target>
</trans-unit>
<trans-unit id="7hEUrao" resname="stats.amountPreviousWeek">
<source>stats.amountPreviousWeek</source>
<target>Revenue previous week</target>
</trans-unit>
<trans-unit id="ulr3reE" resname="stats.amountMonth">
<source>stats.amountMonth</source>
<target>Revenue this month</target>
</trans-unit>
<trans-unit id="28vpRIL" resname="stats.amountPreviousMonth">
<source>stats.amountPreviousMonth</source>
<target>Revenue previous month</target>
</trans-unit>
<trans-unit id="bQZyZaO" resname="stats.amountYear">
<source>stats.amountYear</source>
<target>Revenue this year</target>
</trans-unit>
<trans-unit id="1jHLd_9" resname="stats.amountPreviousYear">
<source>stats.amountPreviousYear</source>
<target>Revenue previous year</target>
</trans-unit>
<trans-unit id="wuUWovn" resname="stats.amountFinancialYear">
<source>stats.amountFinancialYear</source>
<target>Revenue this financial year</target>
</trans-unit>
<trans-unit id="AAnLEzy" resname="stats.amountPreviousFinancialYear">
<source>stats.amountPreviousFinancialYear</source>
<target>Revenue previous financial year</target>
</trans-unit>
<trans-unit id="Z0fz23v" resname="stats.amountTotal">
<source>stats.amountTotal</source>
<target>Total revenue</target>
@ -978,18 +1034,34 @@
<source>stats.userAmountToday</source>
<target>My revenue today</target>
</trans-unit>
<trans-unit id="rI6FnFK" resname="stats.userAmountYesterday">
<source>stats.userAmountYesterday</source>
<target>My revenue yesterday</target>
</trans-unit>
<trans-unit id="LDS9o5X" resname="stats.userAmountWeek">
<source>stats.userAmountWeek</source>
<target>My revenue this week</target>
</trans-unit>
<trans-unit id="c_eMtvp" resname="stats.userAmountPreviousWeek">
<source>stats.userAmountPreviousWeek</source>
<target>My revenue previous week</target>
</trans-unit>
<trans-unit id="E3Kj9lt" resname="stats.userAmountMonth">
<source>stats.userAmountMonth</source>
<target>My revenue this month</target>
</trans-unit>
<trans-unit id=".RqeEEx" resname="stats.userAmountPreviousMonth">
<source>stats.userAmountPreviousMonth</source>
<target>My revenue previous month</target>
</trans-unit>
<trans-unit id="xvdsZsS" resname="stats.userAmountYear">
<source>stats.userAmountYear</source>
<target>My revenue this year</target>
</trans-unit>
<trans-unit id="uZKsdVm" resname="stats.userAmountPreviousYear">
<source>stats.userAmountPreviousYear</source>
<target>My revenue previous year</target>
</trans-unit>
<trans-unit id="9jlbhkf" resname="stats.userAmountTotal">
<source>stats.userAmountTotal</source>
<target>My total revenue</target>