Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)

This commit is contained in:
Kevin Papst
2022-12-31 21:19:55 +01:00
committed by GitHub
parent 95e06746bd
commit 90a0fd8a22
2164 changed files with 83700 additions and 86426 deletions

View File

@@ -0,0 +1,209 @@
<?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\DataProvider;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Model\Statistic\Day;
use App\Repository\TimesheetRepository;
use DateTime;
use DateTimeInterface;
/**
* This class should really be deleted and replaced by TimesheetStatisticService::getDailyStatistics()
* @deprecated since 2.0
* @codeCoverageIgnore
* @internal
* @final
*/
class DailyWorkingTimeChartProvider
{
public function __construct(private TimesheetRepository $repository)
{
}
/**
* In case this method is called with one timezone and the results are from another timezone,
* it might return rows outside the time-range.
*
* @param DateTimeInterface $begin
* @param DateTimeInterface $end
* @param User|null $user
* @return array<mixed>
*/
protected function getDailyData(DateTimeInterface $begin, DateTimeInterface $end, ?User $user = null): array
{
$qb = $this->repository->createQueryBuilder('t');
$or = $qb->expr()->orX();
$or->add($qb->expr()->between(':begin', 't.begin', 't.end'));
$or->add($qb->expr()->between(':end', 't.begin', 't.end'));
$or->add($qb->expr()->between('t.begin', ':begin', ':end'));
$or->add($qb->expr()->between('t.end', ':begin', ':end'));
$qb->select('t, p, a, c')
->andWhere($qb->expr()->isNotNull('t.end'))
->andWhere($or)
->orderBy('t.begin', 'DESC')
->setParameter('begin', $begin)
->setParameter('end', $end)
->leftJoin('t.activity', 'a')
->leftJoin('t.project', 'p')
->leftJoin('p.customer', 'c')
;
if (null !== $user) {
$qb
->andWhere($qb->expr()->eq('t.user', ':user'))
->setParameter('user', $user)
;
}
$timesheets = $qb->getQuery()->getResult();
$results = [];
/** @var Timesheet $result */
foreach ($timesheets as $result) {
/** @var DateTime $beginTmp */
$beginTmp = $result->getBegin();
/** @var DateTime $endTmp */
$endTmp = $result->getEnd();
$dateKeyEnd = $endTmp->format('Ymd');
do {
$dateKey = $beginTmp->format('Ymd');
if ($dateKey !== $dateKeyEnd) {
$newDateBegin = clone $beginTmp;
$newDateBegin->add(new \DateInterval('P1D'));
// overlapping records should always start at midnight
$newDateBegin->setTime(0, 0, 0);
} else {
$newDateBegin = clone $endTmp;
}
// make sure to exclude entries that are outside the requested time-range:
// these entries can exist if you have long running entries that started before $begin
// for statistical reasons we have to include everything between $begin and $end while
// excluding everything that is outside of that range
// --------------------------------------------------------------------------------------
// Be aware that this will NOT filter every record, in case there is a timezone mismatch between the
// begin/end dates and the ones from the database (eg. recorded in UTC) - which might actually be
// before $begin (which happens thanks to the timezone conversion when querying the database)
if ($newDateBegin > $begin && $beginTmp < $end) {
if (!isset($results[$dateKey])) {
$results[$dateKey] = [
'rate' => 0,
'duration' => 0,
'billable' => 0, // duration
'month' => $beginTmp->format('n'),
'year' => $beginTmp->format('Y'),
'day' => $beginTmp->format('j'),
'details' => []
];
}
$duration = $newDateBegin->getTimestamp() - $beginTmp->getTimestamp();
$durationPercent = 0;
if ($result->getDuration() !== null && $result->getDuration() > 0) {
$durationPercent = $duration / $result->getDuration();
}
$rate = $result->getRate() * $durationPercent;
$results[$dateKey]['rate'] += $rate;
$results[$dateKey]['duration'] += $duration;
if ($result->isBillable()) {
$results[$dateKey]['billable'] += $duration;
}
$detailsId =
$result->getProject()->getCustomer()->getId()
. '_' . $result->getProject()->getId()
. '_' . $result->getActivity()->getId()
;
if (!isset($results[$dateKey]['details'][$detailsId])) {
$results[$dateKey]['details'][$detailsId] = [
'project' => $result->getProject(),
'activity' => $result->getActivity(),
'duration' => 0,
'rate' => 0,
'billable' => 0, // duration
];
}
$results[$dateKey]['details'][$detailsId]['duration'] += $duration;
$results[$dateKey]['details'][$detailsId]['rate'] += $rate;
if ($result->isBillable()) {
$results[$dateKey]['details'][$detailsId]['billable'] += $duration;
}
}
$beginTmp = $newDateBegin;
// yes, we only want to compare the day, not the time
if ((int) $end->format('Ymd') < (int) $newDateBegin->format('Ymd')) {
break;
}
} while ($dateKey !== $dateKeyEnd);
}
ksort($results);
foreach ($results as $key => $value) {
$results[$key]['details'] = array_values($results[$key]['details']);
}
return array_values($results);
}
/**
* @deprecated since 2.0 - use TimesheetStatisticService::getDailyStatistics() instead
*
* @param User|null $user
* @param DateTimeInterface $begin
* @param DateTimeInterface $end
* @return Day[]
* @throws \Exception
*/
public function getData(?User $user, DateTimeInterface $begin, DateTimeInterface $end): array
{
/** @var Day[] $days */
$days = [];
// prefill the array
$tmp = DateTime::createFromInterface($end);
$until = (int) $begin->format('Ymd');
while ((int) $tmp->format('Ymd') >= $until) {
$last = clone $tmp;
$days[$last->format('Ymd')] = new Day($last, 0, 0.00);
$tmp->modify('-1 day');
}
$results = $this->getDailyData($begin, $end, $user);
foreach ($results as $statRow) {
$dateTime = DateTime::createFromInterface($begin);
$dateTime->setDate($statRow['year'], $statRow['month'], $statRow['day']);
$dateTime->setTime(0, 0, 0);
$day = new Day($dateTime, (int) $statRow['duration'], (float) $statRow['rate']);
$day->setTotalDurationBillable($statRow['billable']);
$day->setDetails($statRow['details']);
$dateKey = $dateTime->format('Ymd');
// make sure entries from other timezones are filtered
if (!\array_key_exists($dateKey, $days)) {
continue;
}
$days[$dateKey] = $day;
}
ksort($days);
return array_values($days);
}
}

View File

@@ -1,42 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Renderer;
use App\Widget\WidgetRendererInterface;
use Twig\Environment;
abstract class AbstractTwigRenderer implements WidgetRendererInterface
{
/**
* @var Environment
*/
protected $twig;
/**
* @param Environment $twig
*/
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
/**
* @param string $template
* @param array $data
* @return string
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
protected function renderTemplate(string $template, array $data): string
{
return $this->twig->render($template, $data);
}
}

View File

@@ -1,29 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Renderer;
use App\Widget\Type\CompoundChart;
use App\Widget\WidgetInterface;
class CompoundChartRenderer extends AbstractTwigRenderer
{
public function supports(WidgetInterface $widget): bool
{
return ($widget instanceof CompoundChart);
}
public function render(WidgetInterface $widget, array $options = []): string
{
return $this->renderTemplate('widget/section-chart.html.twig', [
'title' => $widget->getTitle(),
'widgets' => $widget->getData(),
]);
}
}

View File

@@ -1,29 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Renderer;
use App\Widget\Type\CompoundRow;
use App\Widget\WidgetInterface;
class CompoundRowRenderer extends AbstractTwigRenderer
{
public function supports(WidgetInterface $widget): bool
{
return ($widget instanceof CompoundRow);
}
public function render(WidgetInterface $widget, array $options = []): string
{
return $this->renderTemplate('widget/section-simple.html.twig', [
'title' => $widget->getTitle(),
'widgets' => $widget->getData(),
]);
}
}

View File

@@ -1,39 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Renderer;
use App\Widget\Type\SimpleWidget;
use App\Widget\WidgetInterface;
class SimpleWidgetRenderer extends AbstractTwigRenderer
{
public function supports(WidgetInterface $widget): bool
{
return $widget instanceof SimpleWidget;
}
/**
* @param SimpleWidget $widget
* @param array $options
* @return string
* @throws \ReflectionException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function render(WidgetInterface $widget, array $options = []): string
{
return $this->renderTemplate($widget->getTemplateName(), [
'data' => $widget->getData($options),
'options' => $widget->getOptions($options),
'title' => $widget->getTitle(),
]);
}
}

View File

@@ -0,0 +1,45 @@
<?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;
abstract class AbstractActiveUsers extends AbstractSimpleStatisticChart
{
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'users',
], parent::getOptions($options));
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(false);
$this->setQuery(TimesheetRepository::STATS_QUERY_USER);
return parent::getData($options);
}
public function getTitle(): string
{
return 'stats.' . lcfirst($this->getId());
}
public function getPermissions(): array
{
return ['ROLE_TEAMLEAD'];
}
public function getTemplateName(): string
{
return 'widget/widget-counter.html.twig';
}
}

View File

@@ -13,44 +13,47 @@ use App\Event\RevenueStatisticEvent;
use App\Repository\TimesheetRepository;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
abstract class AbstractAmountPeriod extends SimpleStatisticChart
abstract class AbstractAmountPeriod extends AbstractWidget
{
private $dispatcher;
public function __construct(TimesheetRepository $repository, EventDispatcherInterface $dispatcher)
public function __construct(private TimesheetRepository $repository, private EventDispatcherInterface $dispatcher)
{
parent::__construct($repository);
$this->dispatcher = $dispatcher;
}
public function getTitle(): string
{
return 'stats.' . $this->getId();
return 'stats.' . lcfirst($this->getId());
}
public function getTemplateName(): string
{
return 'widget/widget-counter.html.twig';
return 'widget/widget-counter-money.html.twig';
}
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'money',
'dataType' => 'money',
], parent::getOptions($options));
}
public function getData(array $options = [])
protected function getRevenue(?string $begin, ?string $end, array $options = [])
{
$this->setQuery(TimesheetRepository::STATS_QUERY_RATE);
$this->setQueryWithUser(false);
$user = $this->getUser();
$timezone = new \DateTimeZone($user->getTimezone());
$data = parent::getData($options);
if ($begin !== null) {
$begin = new \DateTime($begin, $timezone);
}
$event = new RevenueStatisticEvent($this->begin, $this->end);
if ($data !== null) {
$event->addRevenue($data);
if ($end !== null) {
$end = new \DateTime($end, $timezone);
}
$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);

View File

@@ -1,95 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
use App\Widget\WidgetContainerInterface;
use App\Widget\WidgetInterface;
use BadMethodCallException;
abstract class AbstractContainer implements WidgetContainerInterface
{
/**
* @var string
*/
protected $title = '';
/**
* @var WidgetInterface[]
*/
protected $widgets = [];
/**
* @var int
*/
protected $order = 0;
public function setTitle(string $title)
{
$this->title = $title;
}
/**
* @param array $options
* @return WidgetInterface[]|array|mixed|null
*/
public function getData(array $options = [])
{
return $this->getWidgets();
}
public function getOptions(array $options = []): array
{
return $options;
}
/**
* @param string $name
* @param mixed $value
*/
public function setOption(string $name, $value): void
{
throw new BadMethodCallException('setOption() is not supported on AbstractContainer');
}
public function getId(): string
{
return $this->title;
}
public function getOrder(): int
{
return $this->order;
}
public function setOrder(int $order)
{
$this->order = $order;
return $this;
}
public function getTitle(): string
{
return $this->title;
}
/**
* @return WidgetInterface[]
*/
public function getWidgets(): array
{
return $this->widgets;
}
public function addWidget(WidgetInterface $widget)
{
$this->widgets[] = $widget;
return $this;
}
}

View File

@@ -0,0 +1,39 @@
<?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;
abstract class AbstractCounterDuration extends AbstractSimpleStatisticChart
{
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'duration',
], parent::getOptions($options));
}
public function getData(array $options = []): mixed
{
$this->setQuery(TimesheetRepository::STATS_QUERY_DURATION);
return parent::getData($options);
}
public function getTitle(): string
{
return 'stats.' . lcfirst($this->getId());
}
public function getTemplateName(): string
{
return 'widget/widget-counter-duration.html.twig';
}
}

View File

@@ -0,0 +1,56 @@
<?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;
abstract class AbstractCounterYear extends AbstractSimpleStatisticChart
{
private bool $isFinancialYear = false;
public function __construct(TimesheetRepository $repository, private SystemConfiguration $systemConfiguration)
{
parent::__construct($repository);
}
public function getData(array $options = []): mixed
{
$this->setBegin('01 january this year 00:00:00');
$this->setEnd('31 december this year 23:59:59');
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$factory = new DateTimeFactory($this->getTimezone());
$begin = $factory->createStartOfFinancialYear($financialYear);
$this->setBegin($begin);
$this->setEnd($factory->createEndOfFinancialYear($begin));
$this->isFinancialYear = true;
}
return parent::getData($options);
}
abstract protected function getFinancialYearTitle(): string;
public function getTitle(): string
{
if ($this->isFinancialYear) {
return $this->getFinancialYearTitle();
}
return 'stats.' . lcfirst($this->getId());
}
public function getTemplateName(): string
{
return 'widget/widget-counter.html.twig';
}
}

View File

@@ -0,0 +1,139 @@
<?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;
abstract class AbstractSimpleStatisticChart extends AbstractWidgetType
{
/**
* @var TimesheetRepository::STATS_QUERY_*
*/
private string $query;
/**
* @var string|\DateTime|null
*/
private $begin;
/**
* @var string|\DateTime|null
*/
private $end;
private bool $queryWithUser = false;
public function __construct(private TimesheetRepository $repository)
{
}
public function getWidth(): int
{
return WidgetInterface::WIDTH_SMALL;
}
public function getHeight(): int
{
return WidgetInterface::HEIGHT_SMALL;
}
/**
* @param TimesheetRepository::STATS_QUERY_* $query
* @return void
*/
public function setQuery(string $query): void
{
$this->query = $query;
}
public function setBegin(null|string|\DateTime $begin): self
{
$this->begin = $begin;
return $this;
}
public function getBegin(): ?\DateTime
{
if ($this->begin === null) {
return null;
}
if ($this->begin instanceof \DateTime) {
return $this->begin;
}
return new \DateTime($this->begin, $this->getTimezone());
}
public function setEnd(null|string|\DateTime $end): self
{
$this->end = $end;
return $this;
}
public function getEnd(): ?\DateTime
{
if ($this->end === null) {
return null;
}
if ($this->end instanceof \DateTime) {
return $this->end;
}
return new \DateTime($this->end, $this->getTimezone());
}
public function setQueryWithUser(bool $queryWithUser): self
{
$this->queryWithUser = $queryWithUser;
return $this;
}
public function getTimezone(): \DateTimeZone
{
$timezone = date_default_timezone_get();
if (null !== $this->getUser()) {
$timezone = $this->getUser()->getTimezone();
}
return new \DateTimeZone($timezone);
}
/**
* @param array $options
* @return mixed|null
* @throws WidgetException
*/
public function getData(array $options = []): mixed
{
try {
$user = null;
if (true === $this->queryWithUser) {
$user = $this->getUser();
}
return $this->repository->getStatistic($this->query, $this->getBegin(), $this->getEnd(), $user);
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
public function getTemplateName(): string
{
$name = (new \ReflectionClass($this))->getShortName();
return sprintf('widget/widget-%s.html.twig', strtolower($name));
}
}

View File

@@ -1,59 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
use App\Event\UserRevenueStatisticEvent;
use App\Repository\TimesheetRepository;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
abstract class AbstractUserAmountPeriod extends SimpleStatisticChart
{
private $dispatcher;
public function __construct(TimesheetRepository $repository, EventDispatcherInterface $dispatcher)
{
parent::__construct($repository);
$this->dispatcher = $dispatcher;
}
public function getTitle(): string
{
return 'stats.' . str_replace('userA', 'a', $this->getId());
}
public function getTemplateName(): string
{
return 'widget/widget-counter.html.twig';
}
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'money',
'dataType' => 'money',
], parent::getOptions($options));
}
public function getData(array $options = [])
{
$this->setQuery(TimesheetRepository::STATS_QUERY_RATE);
$this->setQueryWithUser(true);
$data = parent::getData($options);
$event = new UserRevenueStatisticEvent($this->user, $this->begin, $this->end);
if ($data !== null) {
$event->addRevenue($data);
}
$this->dispatcher->dispatch($event);
return $event->getRevenue();
}
}

View File

@@ -0,0 +1,67 @@
<?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\Event\UserRevenueStatisticEvent;
use App\Repository\TimesheetRepository;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
abstract class AbstractUserRevenuePeriod extends AbstractWidget
{
public function __construct(private TimesheetRepository $repository, private EventDispatcherInterface $dispatcher)
{
}
public function getTitle(): string
{
return 'stats.' . lcfirst($this->getId());
}
public function getTemplateName(): string
{
return 'widget/widget-counter-money.html.twig';
}
public function getPermissions(): array
{
return ['view_rate_own_timesheet'];
}
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'money',
], parent::getOptions($options));
}
protected function getRevenue(?string $begin, ?string $end, array $options = [])
{
$user = $this->getUser();
$timezone = new \DateTimeZone($user->getTimezone());
if ($begin !== null) {
$begin = new \DateTime($begin, $timezone);
}
if ($end !== null) {
$end = new \DateTime($end, $timezone);
}
$data = $this->repository->getRevenue($begin, $end, $user);
$event = new UserRevenueStatisticEvent($user, $begin, $end);
foreach ($data as $row) {
$event->addRevenue($row->getCurrency(), $row->getAmount());
}
$this->dispatcher->dispatch($event);
return $event->getRevenue();
}
}

View File

@@ -0,0 +1,74 @@
<?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\Entity\User;
use App\Widget\WidgetInterface;
use Symfony\Component\Form\Form;
abstract class AbstractWidget implements WidgetInterface
{
private array $options = [];
private ?User $user = null;
public function hasForm(): bool
{
return false;
}
public function getForm(): ?Form
{
return null;
}
public function getHeight(): int
{
return WidgetInterface::HEIGHT_SMALL;
}
public function getWidth(): int
{
return WidgetInterface::WIDTH_SMALL;
}
public function getPermissions(): array
{
return [];
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function getUser(): User
{
return $this->user;
}
/**
* @param string $name
* @param mixed $value
*/
public function setOption(string $name, $value): void
{
$this->options[$name] = $value;
}
public function getOptions(array $options = []): array
{
return array_merge($this->options, $options);
}
public function isInternal(): bool
{
return false;
}
}

View File

@@ -11,24 +11,40 @@ namespace App\Widget\Type;
use App\Widget\WidgetInterface;
abstract class AbstractWidgetType implements WidgetInterface
abstract class AbstractWidgetType extends AbstractWidget
{
private ?string $id = null;
private string $title = '';
private int $height = WidgetInterface::HEIGHT_SMALL;
private int $width = WidgetInterface::WIDTH_SMALL;
/**
* @var string
* @var array<string>
*/
protected $id;
/**
* @var string
*/
protected $title = '';
/**
* @var array
*/
protected $options = [];
/**
* @var mixed
*/
protected $data;
private array $permissions = [];
public function getHeight(): int
{
return $this->height;
}
public function setHeight(int $height): AbstractWidgetType
{
$this->height = $height;
return $this;
}
public function getWidth(): int
{
return $this->width;
}
public function setWidth(int $width): AbstractWidgetType
{
$this->width = $width;
return $this;
}
public function setId(string $id): self
{
@@ -46,22 +62,6 @@ abstract class AbstractWidgetType implements WidgetInterface
return (new \ReflectionClass($this))->getShortName();
}
public function setData($data): self
{
$this->data = $data;
return $this;
}
/**
* @param array $options
* @return mixed|null
*/
public function getData(array $options = [])
{
return $this->data;
}
public function setTitle(string $title): self
{
$this->title = $title;
@@ -77,37 +77,21 @@ abstract class AbstractWidgetType implements WidgetInterface
public function setOptions(array $options): self
{
foreach ($options as $key => $value) {
$this->options[$key] = $value;
$this->setOption($key, $value);
}
return $this;
}
/**
* @param string $name
* @param mixed $value
*/
public function setOption(string $name, $value): void
public function getPermissions(): array
{
$this->options[$name] = $value;
return $this->permissions;
}
/**
* @param string $name
* @param mixed|null $default
* @return mixed|null
*/
public function getOption(string $name, $default = null)
public function setPermissions(array $permissions): AbstractWidgetType
{
if (\array_key_exists($name, $this->options)) {
return $this->options[$name];
}
$this->permissions = $permissions;
return $default;
}
public function getOptions(array $options = []): array
{
return array_merge($this->options, $options);
return $this;
}
}

View File

@@ -0,0 +1,49 @@
<?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\WidgetInterface;
final class ActiveTimesheets extends AbstractSimpleStatisticChart
{
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TOTAL, 'icon' => 'duration'], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_all_data'];
}
public function getId(): string
{
return 'activeRecordings';
}
public function getTitle(): string
{
return 'stats.activeRecordings';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(false);
$this->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE);
return parent::getData($options);
}
public function getTemplateName(): string
{
return 'widget/widget-counter.html.twig';
}
}

View File

@@ -0,0 +1,33 @@
<?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 ActiveUsersMonth extends AbstractActiveUsers
{
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_MONTH], parent::getOptions($options));
}
public function getId(): string
{
return 'activeUsersMonth';
}
public function getData(array $options = []): mixed
{
$this->setBegin('first day of this month 00:00:00');
$this->setEnd('last day of this month 23:59:59');
return parent::getData($options);
}
}

View File

@@ -0,0 +1,33 @@
<?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 ActiveUsersToday extends AbstractActiveUsers
{
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TODAY], parent::getOptions($options));
}
public function getId(): string
{
return 'activeUsersToday';
}
public function getData(array $options = []): mixed
{
$this->setBegin('00:00:00');
$this->setEnd('23:59:59');
return parent::getData($options);
}
}

View File

@@ -0,0 +1,30 @@
<?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 ActiveUsersTotal extends AbstractActiveUsers
{
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
}
public function getId(): string
{
return 'activeUsersTotal';
}
public function getData(array $options = []): mixed
{
return parent::getData($options);
}
}

View File

@@ -0,0 +1,33 @@
<?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 ActiveUsersWeek extends AbstractActiveUsers
{
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_WEEK], parent::getOptions($options));
}
public function getId(): string
{
return 'activeUsersWeek';
}
public function getData(array $options = []): mixed
{
$this->setBegin('monday this week 00:00:00');
$this->setEnd('sunday this week 23:59:59');
return parent::getData($options);
}
}

View File

@@ -9,26 +9,39 @@
namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetInterface;
final class ActiveUsersYear extends CounterYear
final class ActiveUsersYear extends AbstractCounterYear
{
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
public function getOptions(array $options = []): array
{
parent::__construct($repository, $systemConfiguration);
$this->setId('activeUsersYear');
$this->setOption('icon', 'user');
$this->setOption('color', 'yellow');
$this->setTitle('stats.userActiveYear');
return array_merge([
'icon' => 'users',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$this->titleYear = 'stats.userActiveFinancialYear';
$this->setQuery(TimesheetRepository::STATS_QUERY_USER);
$this->setQueryWithUser(false);
return parent::getData($options);
}
protected function getFinancialYearTitle(): string
{
return 'stats.activeUsersFinancialYear';
}
public function getPermissions(): array
{
return ['ROLE_TEAMLEAD'];
}
public function getId(): string
{
return 'activeUsersYear';
}
}

View File

@@ -20,14 +20,16 @@ final class AmountMonth extends AbstractAmountPeriod
public function getId(): string
{
return 'amountMonth';
return 'AmountMonth';
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$this->setBegin('first day of this month 00:00:00');
$this->setEnd('last day of this month 23:59:59');
return $this->getRevenue('first day of this month 00:00:00', 'last day of this month 23:59:59', $options);
}
return parent::getData($options);
public function getPermissions(): array
{
return ['view_all_data'];
}
}

View File

@@ -20,14 +20,16 @@ final class AmountToday extends AbstractAmountPeriod
public function getId(): string
{
return 'amountToday';
return 'AmountToday';
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$this->setBegin('00:00:00');
$this->setEnd('23:59:59');
return $this->getRevenue('00:00:00', '23:59:59', $options);
}
return parent::getData($options);
public function getPermissions(): array
{
return ['view_all_data'];
}
}

View File

@@ -22,4 +22,14 @@ final class AmountTotal extends AbstractAmountPeriod
{
return 'amountTotal';
}
public function getPermissions(): array
{
return ['view_all_data'];
}
public function getData(array $options = []): mixed
{
return $this->getRevenue(null, null, $options);
}
}

View File

@@ -20,14 +20,16 @@ final class AmountWeek extends AbstractAmountPeriod
public function getId(): string
{
return 'amountWeek';
return 'AmountWeek';
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$this->setBegin('monday this week 00:00:00');
$this->setEnd('sunday this week 23:59:59');
return $this->getRevenue('monday this week 00:00:00', 'sunday this week 23:59:59', $options);
}
return parent::getData($options);
public function getPermissions(): array
{
return ['view_all_data'];
}
}

View File

@@ -11,39 +11,60 @@ namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Event\RevenueStatisticEvent;
use App\Model\Revenue;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class AmountYear extends CounterYear
final class AmountYear extends AbstractCounterYear
{
private $dispatcher;
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration, EventDispatcherInterface $dispatcher)
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher)
{
parent::__construct($repository, $systemConfiguration);
$this->dispatcher = $dispatcher;
$this->setId('amountYear');
$this->setOption('dataType', 'money');
$this->setOption('icon', 'money');
$this->setOption('color', WidgetInterface::COLOR_YEAR);
$this->setTitle('stats.amountYear');
}
public function getData(array $options = [])
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'money',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
public function getData(array $options = []): mixed
{
$this->titleYear = 'stats.amountFinancialYear';
$this->setQuery(TimesheetRepository::STATS_QUERY_RATE);
$this->setQueryWithUser(false);
/** @var array<Revenue> $data */
$data = parent::getData($options);
$event = new RevenueStatisticEvent($this->begin, $this->end);
if ($data !== null) {
$event->addRevenue($data);
$event = new RevenueStatisticEvent($this->getBegin(), $this->getEnd());
foreach ($data as $row) {
$event->addRevenue($row->getCurrency(), $row->getAmount());
}
$this->dispatcher->dispatch($event);
return $event->getRevenue();
}
public function getId(): string
{
return 'AmountYear';
}
protected function getFinancialYearTitle(): string
{
return 'stats.amountFinancialYear';
}
public function getTemplateName(): string
{
return 'widget/widget-counter-money.html.twig';
}
public function getPermissions(): array
{
return ['view_all_data'];
}
}

View File

@@ -1,21 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
interface AuthorizedWidget
{
/**
* Return a list of granted syntax string.
* If ANY of the given permission strings matches, access is granted.
*
* @return string[]
*/
public function getPermissions(): array;
}

View File

@@ -1,14 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
class CompoundChart extends AbstractContainer
{
}

View File

@@ -1,14 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
class CompoundRow extends AbstractContainer
{
}

View File

@@ -9,13 +9,10 @@
namespace App\Widget\Type;
use App\Repository\TimesheetRepository;
final class Counter extends SimpleStatisticChart
final class Counter extends AbstractSimpleStatisticChart
{
public function __construct(TimesheetRepository $repository)
public function getTemplateName(): string
{
parent::__construct($repository);
$this->setOption('dataType', 'int');
return 'widget/widget-counter.html.twig';
}
}

View File

@@ -1,52 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
class CounterYear extends SimpleStatisticChart
{
private $systemConfiguration;
/**
* @var string|null
*/
protected $titleYear;
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
{
parent::__construct($repository);
$this->systemConfiguration = $systemConfiguration;
$this->setOption('dataType', 'int');
}
public function getData(array $options = [])
{
$this->begin = '01 january this year 00:00:00';
$this->end = '31 december this year 23:59:59';
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$factory = new DateTimeFactory($this->getTimezone());
$this->begin = $factory->createStartOfFinancialYear($financialYear);
$this->end = $factory->createEndOfFinancialYear($this->begin);
if (!empty($this->titleYear)) {
$this->setTitle($this->titleYear);
}
}
return parent::getData($options);
}
public function getTemplateName(): string
{
return 'widget/widget-counter.html.twig';
}
}

View File

@@ -11,62 +11,54 @@ namespace App\Widget\Type;
use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\User;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use App\Widget\DataProvider\DailyWorkingTimeChartProvider;
use App\Widget\WidgetInterface;
use DateTime;
class DailyWorkingTimeChart extends SimpleWidget implements UserWidget
/**
* This is rendered inside the PaginatedWorkingTimeChart.
*/
final class DailyWorkingTimeChart extends AbstractWidget
{
public const DEFAULT_CHART = 'bar';
/**
* @var TimesheetRepository
*/
protected $repository;
public function __construct(TimesheetRepository $repository)
public function __construct(private DailyWorkingTimeChartProvider $dailyWorkingTimeChartProvider)
{
$this->repository = $repository;
$this->setId('DailyWorkingTimeChart');
$this->setTitle('stats.yourWorkingHours');
$this->setOptions([
'begin' => null,
'end' => null,
'color' => '',
'type' => self::DEFAULT_CHART,
'id' => '',
]);
}
public function setUser(User $user): void
public function getWidth(): int
{
$this->setOption('user', $user);
return WidgetInterface::WIDTH_FULL;
}
public function getHeight(): int
{
return WidgetInterface::HEIGHT_LARGE;
}
public function getPermissions(): array
{
return ['view_own_timesheet'];
}
public function isInternal(): bool
{
return true;
}
public function getOptions(array $options = []): array
{
$options = parent::getOptions($options);
if (!\in_array($options['type'], ['bar', 'line'])) {
$options['type'] = self::DEFAULT_CHART;
}
if (empty($options['id'])) {
$options['id'] = uniqid('DailyWorkingTimeChart_');
}
return $options;
return array_merge([
'begin' => null,
'end' => null,
'color' => '',
'type' => 'bar',
'id' => uniqid('DailyWorkingTimeChart_'),
], parent::getOptions($options));
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$options = $this->getOptions($options);
$user = $options['user'];
if (null === $user || !($user instanceof User)) {
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
}
$user = $this->getUser();
$dateTimeFactory = DateTimeFactory::createByUser($user);
@@ -74,7 +66,7 @@ class DailyWorkingTimeChart extends SimpleWidget implements UserWidget
$options['begin'] = $dateTimeFactory->getStartOfWeek();
}
if ($options['begin'] instanceof DateTime) {
if ($options['begin'] instanceof \DateTimeInterface) {
$begin = $options['begin'];
} else {
$begin = new DateTime($options['begin'], new \DateTimeZone($user->getTimezone()));
@@ -84,14 +76,14 @@ class DailyWorkingTimeChart extends SimpleWidget implements UserWidget
$options['end'] = $dateTimeFactory->getEndOfWeek($begin);
}
if ($options['end'] instanceof DateTime) {
if ($options['end'] instanceof \DateTimeInterface) {
$end = $options['end'];
} else {
$end = new DateTime($options['end'], new \DateTimeZone($user->getTimezone()));
}
$activities = [];
$statistics = $this->repository->getDailyStats($user, $begin, $end);
$statistics = $this->dailyWorkingTimeChartProvider->getData($user, $begin, $end);
foreach ($statistics as $day) {
foreach ($day->getDetails() as $entry) {
@@ -114,4 +106,19 @@ class DailyWorkingTimeChart extends SimpleWidget implements UserWidget
'data' => $statistics,
];
}
public function getTitle(): string
{
return 'stats.yourWorkingHours';
}
public function getId(): string
{
return 'DailyWorkingTimeChart';
}
public function getTemplateName(): string
{
return 'widget/widget-dailyworkingtimechart.html.twig';
}
}

View File

@@ -0,0 +1,44 @@
<?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 DurationMonth extends AbstractCounterDuration
{
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 'DurationMonth';
}
public function getTitle(): string
{
return 'stats.durationMonth';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(false);
$this->setBegin('first day of this month 00:00:00');
$this->setEnd('last day of this month 23:59:59');
return parent::getData($options);
}
}

View File

@@ -0,0 +1,44 @@
<?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 DurationToday extends AbstractCounterDuration
{
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 'DurationToday';
}
public function getTitle(): string
{
return 'stats.durationToday';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(false);
$this->setBegin('00:00:00');
$this->setEnd('23:59:59');
return parent::getData($options);
}
}

View File

@@ -0,0 +1,42 @@
<?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 DurationTotal extends AbstractCounterDuration
{
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_other_timesheet'];
}
public function getId(): string
{
return 'durationTotal';
}
public function getTitle(): string
{
return 'stats.durationTotal';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(false);
return parent::getData($options);
}
}

View File

@@ -0,0 +1,44 @@
<?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 DurationWeek extends AbstractCounterDuration
{
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 'DurationWeek';
}
public function getTitle(): string
{
return 'stats.durationWeek';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(false);
$this->setBegin('monday this week 00:00:00');
$this->setEnd('sunday this week 23:59:59');
return parent::getData($options);
}
}

View File

@@ -9,27 +9,44 @@
namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetInterface;
final class DurationYear extends CounterYear
final class DurationYear extends AbstractCounterYear
{
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
public function getOptions(array $options = []): array
{
parent::__construct($repository, $systemConfiguration);
$this->setId('durationYear');
$this->setOption('dataType', 'duration');
$this->setOption('icon', 'duration');
$this->setOption('color', 'yellow');
$this->setTitle('stats.durationYear');
return array_merge([
'icon' => 'duration',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$this->titleYear = 'stats.durationFinancialYear';
$this->setQuery(TimesheetRepository::STATS_QUERY_DURATION);
$this->setQueryWithUser(false);
return parent::getData($options);
}
public function getPermissions(): array
{
return ['view_other_timesheet'];
}
protected function getFinancialYearTitle(): string
{
return 'stats.durationFinancialYear';
}
public function getTemplateName(): string
{
return 'widget/widget-counter-duration.html.twig';
}
public function getId(): string
{
return 'DurationYear';
}
}

View File

@@ -9,10 +9,28 @@
namespace App\Widget\Type;
class More extends SimpleWidget
class More extends AbstractWidgetType
{
public function __construct()
private mixed $data = null;
public function setData($data): self
{
$this->setOption('dataType', 'int');
$this->data = $data;
return $this;
}
/**
* @param array $options
* @return mixed|null
*/
public function getData(array $options = []): mixed
{
return $this->data;
}
public function getTemplateName(): string
{
return 'widget/widget-more.html.twig';
}
}

View File

@@ -10,31 +10,40 @@
namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Entity\User;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use App\Widget\WidgetInterface;
use DateTime;
final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
final class PaginatedWorkingTimeChart extends AbstractWidget
{
private $repository;
private $systemConfiguration;
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
public function __construct(private TimesheetRepository $repository, private SystemConfiguration $systemConfiguration)
{
$this->repository = $repository;
$this->systemConfiguration = $systemConfiguration;
$this->setTitle('stats.yourWorkingHours');
}
public function setUser(User $user): void
public function getWidth(): int
{
$this->setOption('user', $user);
$now = new DateTime('now', new \DateTimeZone($user->getTimezone()));
$this->setOptions([
'year' => $now->format('o'),
'week' => $now->format('W'),
]);
return WidgetInterface::WIDTH_FULL;
}
public function getHeight(): int
{
return WidgetInterface::HEIGHT_MAXIMUM;
}
public function getPermissions(): array
{
return ['view_own_timesheet'];
}
public function getTitle(): string
{
return 'stats.yourWorkingHours';
}
public function getTemplateName(): string
{
return 'widget/widget-paginatedworkingtimechart.html.twig';
}
public function getOptions(array $options = []): array
@@ -45,9 +54,20 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
$options['type'] = 'bar';
}
if (!\array_key_exists('year', $options)) {
$options['year'] = (new DateTime('now'))->format('o');
$options['week'] = (new DateTime('now'))->format('W');
if (!\array_key_exists('year', $options) || !\array_key_exists('week', $options)) {
$timezone = date_default_timezone_get();
if ($this->getUser() !== null) {
$timezone = $this->getUser()->getTimezone();
}
$now = new DateTime('now', new \DateTimeZone($timezone));
if (!\array_key_exists('year', $options)) {
$options['year'] = $now->format('o');
}
if (!\array_key_exists('week', $options)) {
$options['week'] = $now->format('W');
}
}
return $options;
@@ -61,14 +81,9 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
return $lastWeekInYear->format('W') === '53' ? 53 : 52;
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$options = $this->getOptions($options);
$user = $options['user'];
if (null === $user || !($user instanceof User)) {
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
}
$user = $this->getUser();
$dateTimeFactory = DateTimeFactory::createByUser($user);
@@ -96,7 +111,7 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
$yearBegin = $dateTimeFactory->createDateTime(sprintf('01 january %s 00:00:00', $year));
$yearEnd = $dateTimeFactory->createDateTime(sprintf('31 december %s 23:59:59', $year));
$yearData = $this->repository->getStatistic('duration', $yearBegin, $yearEnd, $user);
$yearData = $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $yearBegin, $yearEnd, $user);
$financialYearData = null;
$financialYearBegin = null;
@@ -104,22 +119,26 @@ final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
if (null !== ($financialYear = $this->systemConfiguration->getFinancialYearStart())) {
$financialYearBegin = $dateTimeFactory->createStartOfFinancialYear($financialYear);
$financialYearEnd = $dateTimeFactory->createEndOfFinancialYear($financialYearBegin);
$financialYearData = $this->repository->getStatistic('duration', $financialYearBegin, $financialYearEnd, $user);
$financialYearData = $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $financialYearBegin, $financialYearEnd, $user);
}
return [
'begin' => clone $weekBegin,
'end' => clone $weekEnd,
'stats' => $this->repository->getDailyStats($user, $weekBegin, $weekEnd),
'thisMonth' => $thisMonth,
'lastWeekInYear' => $lastWeekInYear,
'lastWeekInLastYear' => $lastWeekInLastYear,
'day' => $this->repository->getStatistic('duration', $dayBegin, $dayEnd, $user),
'week' => $this->repository->getStatistic('duration', $weekBegin, $weekEnd, $user),
'month' => $this->repository->getStatistic('duration', $monthBegin, $monthEnd, $user),
'day' => $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $dayBegin, $dayEnd, $user),
'week' => $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $weekBegin, $weekEnd, $user),
'month' => $this->repository->getStatistic(TimesheetRepository::STATS_QUERY_DURATION, $monthBegin, $monthEnd, $user),
'year' => $yearData,
'financial' => $financialYearData,
'financialBegin' => $financialYearBegin,
];
}
public function getId(): string
{
return 'PaginatedWorkingTimeChart';
}
}

View File

@@ -1,129 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
use App\Entity\User;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetException;
class SimpleStatisticChart extends SimpleWidget implements UserWidget
{
/**
* @var TimesheetRepository
*/
private $repository;
/**
* @var string
*/
private $query;
/**
* @var string|\DateTime
*/
protected $begin;
/**
* @var string|\DateTime
*/
protected $end;
/**
* @var User|null
*/
protected $user;
/**
* @var bool
*/
private $queryWithUser = false;
public function __construct(TimesheetRepository $repository)
{
$this->repository = $repository;
}
public function setQuery(string $query): SimpleStatisticChart
{
$this->query = $query;
return $this;
}
public function setBegin(?string $begin): SimpleStatisticChart
{
$this->begin = $begin;
return $this;
}
public function setEnd(?string $end): SimpleStatisticChart
{
$this->end = $end;
return $this;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setData($data): AbstractWidgetType
{
throw new \InvalidArgumentException('Cannot set data on instances of SimpleStatisticChart');
}
public function setQueryWithUser(bool $queryWithUser): SimpleStatisticChart
{
$this->queryWithUser = $queryWithUser;
return $this;
}
public function getTimezone(): \DateTimeZone
{
$timezone = date_default_timezone_get();
if (null !== $this->user) {
$timezone = $this->user->getTimezone();
}
return new \DateTimeZone($timezone);
}
/**
* @param array $options
* @return mixed|null
* @throws WidgetException
*/
public function getData(array $options = [])
{
$timezone = $this->getTimezone();
$begin = $this->begin;
$end = $this->end;
if (!empty($begin) && \is_string($begin)) {
$this->begin = new \DateTime($begin, $timezone);
}
if (!empty($end) && \is_string($end)) {
$this->end = new \DateTime($end, $timezone);
}
try {
$user = null;
if (true === $this->queryWithUser) {
$user = $this->user;
}
return $this->repository->getStatistic($this->query, $this->begin, $this->end, $user);
} catch (\Exception $ex) {
throw new WidgetException(
'Failed loading widget data: ' . $ex->getMessage()
);
}
}
}

View File

@@ -1,20 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
class SimpleWidget extends AbstractWidgetType
{
public function getTemplateName(): string
{
$name = (new \ReflectionClass($this))->getShortName();
return sprintf('widget/widget-%s.html.twig', strtolower($name));
}
}

View File

@@ -9,20 +9,19 @@
namespace App\Widget\Type;
use App\Entity\User;
use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityQuery;
use App\Widget\WidgetInterface;
final class TotalsActivity extends SimpleWidget implements UserWidget, AuthorizedWidget
final class TotalsActivity extends AbstractWidget
{
use UserWidgetTrait;
private $activity;
public function __construct(ActivityRepository $activity)
public function __construct(private ActivityRepository $activity)
{
$this->activity = $activity;
$this->setTitle('stats.activityTotal');
}
public function getTitle(): string
{
return 'stats.activityTotal';
}
public function getOptions(array $options = []): array
@@ -30,20 +29,13 @@ final class TotalsActivity extends SimpleWidget implements UserWidget, Authorize
return array_merge([
'route' => 'admin_activity',
'icon' => 'activity',
'color' => 'primary',
'dataType' => 'int',
'color' => WidgetInterface::COLOR_TOTAL,
], parent::getOptions($options));
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$options = $this->getOptions($options);
$user = $options['user'];
if (null === $user || !($user instanceof User)) {
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
}
$user = $this->getUser();
$query = new ActivityQuery();
$query->setCurrentUser($user);
@@ -62,4 +54,9 @@ final class TotalsActivity extends SimpleWidget implements UserWidget, Authorize
{
return 'widget/widget-more.html.twig';
}
public function getId(): string
{
return 'TotalsActivity';
}
}

View File

@@ -9,20 +9,19 @@
namespace App\Widget\Type;
use App\Entity\User;
use App\Repository\CustomerRepository;
use App\Repository\Query\CustomerQuery;
use App\Widget\WidgetInterface;
final class TotalsCustomer extends SimpleWidget implements UserWidget, AuthorizedWidget
final class TotalsCustomer extends AbstractWidget
{
use UserWidgetTrait;
private $customer;
public function __construct(CustomerRepository $customer)
public function __construct(private CustomerRepository $customer)
{
$this->customer = $customer;
$this->setTitle('stats.customerTotal');
}
public function getTitle(): string
{
return 'stats.customerTotal';
}
public function getOptions(array $options = []): array
@@ -30,20 +29,13 @@ final class TotalsCustomer extends SimpleWidget implements UserWidget, Authorize
return array_merge([
'route' => 'admin_customer',
'icon' => 'customer',
'color' => 'primary',
'dataType' => 'int',
'color' => WidgetInterface::COLOR_TOTAL,
], parent::getOptions($options));
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$options = $this->getOptions($options);
$user = $options['user'];
if (null === $user || !($user instanceof User)) {
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
}
$user = $this->getUser();
$query = new CustomerQuery();
$query->setCurrentUser($user);
@@ -62,4 +54,9 @@ final class TotalsCustomer extends SimpleWidget implements UserWidget, Authorize
{
return 'widget/widget-more.html.twig';
}
public function getId(): string
{
return 'TotalsCustomer';
}
}

View File

@@ -9,20 +9,19 @@
namespace App\Widget\Type;
use App\Entity\User;
use App\Repository\ProjectRepository;
use App\Repository\Query\ProjectQuery;
use App\Widget\WidgetInterface;
final class TotalsProject extends SimpleWidget implements UserWidget, AuthorizedWidget
final class TotalsProject extends AbstractWidget
{
use UserWidgetTrait;
private $project;
public function __construct(ProjectRepository $project)
public function __construct(private ProjectRepository $project)
{
$this->project = $project;
$this->setTitle('stats.projectTotal');
}
public function getTitle(): string
{
return 'stats.projectTotal';
}
public function getOptions(array $options = []): array
@@ -30,20 +29,13 @@ final class TotalsProject extends SimpleWidget implements UserWidget, Authorized
return array_merge([
'route' => 'admin_project',
'icon' => 'project',
'color' => 'primary',
'dataType' => 'int',
'color' => WidgetInterface::COLOR_TOTAL,
], parent::getOptions($options));
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$options = $this->getOptions($options);
$user = $options['user'];
if (null === $user || !($user instanceof User)) {
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
}
$user = $this->getUser();
$query = new ProjectQuery();
$query->setCurrentUser($user);
@@ -62,4 +54,9 @@ final class TotalsProject extends SimpleWidget implements UserWidget, Authorized
{
return 'widget/widget-more.html.twig';
}
public function getId(): string
{
return 'TotalsProject';
}
}

View File

@@ -9,20 +9,14 @@
namespace App\Widget\Type;
use App\Entity\User;
use App\Repository\Query\UserQuery;
use App\Repository\UserRepository;
use App\Widget\WidgetInterface;
final class TotalsUser extends SimpleWidget implements UserWidget, AuthorizedWidget
final class TotalsUser extends AbstractWidget
{
use UserWidgetTrait;
private $user;
public function __construct(UserRepository $user)
public function __construct(private UserRepository $repository)
{
$this->user = $user;
$this->setTitle('stats.userTotal');
}
public function getOptions(array $options = []): array
@@ -30,29 +24,24 @@ final class TotalsUser extends SimpleWidget implements UserWidget, AuthorizedWid
return array_merge([
'route' => 'admin_user',
'icon' => 'user',
'color' => 'primary',
'dataType' => 'int',
'color' => WidgetInterface::COLOR_TOTAL,
], parent::getOptions($options));
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$options = $this->getOptions($options);
$user = $options['user'];
if (null === $user || !($user instanceof User)) {
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
}
$user = $this->getUser();
$query = new UserQuery();
$query->setCurrentUser($user);
return $this->user->countUsersForQuery($query);
return $this->repository->countUsersForQuery($query);
}
public function getTitle(): string
{
return 'stats.userTotal';
}
/**
* @return string[]
*/
public function getPermissions(): array
{
return ['view_user'];
@@ -62,4 +51,9 @@ final class TotalsUser extends SimpleWidget implements UserWidget, AuthorizedWid
{
return 'widget/widget-more.html.twig';
}
public function getId(): string
{
return 'TotalsUser';
}
}

View File

@@ -11,7 +11,7 @@ namespace App\Widget\Type;
use App\Widget\WidgetInterface;
final class UserAmountMonth extends AbstractUserAmountPeriod
final class UserAmountMonth extends AbstractUserRevenuePeriod
{
public function getOptions(array $options = []): array
{
@@ -20,14 +20,11 @@ final class UserAmountMonth extends AbstractUserAmountPeriod
public function getId(): string
{
return 'userAmountMonth';
return 'UserAmountMonth';
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$this->setBegin('first day of this month 00:00:00');
$this->setEnd('last day of this month 23:59:59');
return parent::getData($options);
return $this->getRevenue('first day of this month 00:00:00', 'last day of this month 23:59:59', $options);
}
}

View File

@@ -11,7 +11,7 @@ namespace App\Widget\Type;
use App\Widget\WidgetInterface;
final class UserAmountToday extends AbstractUserAmountPeriod
final class UserAmountToday extends AbstractUserRevenuePeriod
{
public function getOptions(array $options = []): array
{
@@ -20,14 +20,11 @@ final class UserAmountToday extends AbstractUserAmountPeriod
public function getId(): string
{
return 'userAmountToday';
return 'UserAmountToday';
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$this->setBegin('00:00:00');
$this->setEnd('23:59:59');
return parent::getData($options);
return $this->getRevenue('00:00:00', '23:59:59', $options);
}
}

View File

@@ -11,7 +11,7 @@ namespace App\Widget\Type;
use App\Widget\WidgetInterface;
final class UserAmountTotal extends AbstractUserAmountPeriod
final class UserAmountTotal extends AbstractUserRevenuePeriod
{
public function getOptions(array $options = []): array
{
@@ -20,6 +20,11 @@ final class UserAmountTotal extends AbstractUserAmountPeriod
public function getId(): string
{
return 'userAmountTotal';
return 'UserAmountTotal';
}
public function getData(array $options = []): mixed
{
return $this->getRevenue(null, null, $options);
}
}

View File

@@ -11,7 +11,7 @@ namespace App\Widget\Type;
use App\Widget\WidgetInterface;
final class UserAmountWeek extends AbstractUserAmountPeriod
final class UserAmountWeek extends AbstractUserRevenuePeriod
{
public function getOptions(array $options = []): array
{
@@ -20,14 +20,11 @@ final class UserAmountWeek extends AbstractUserAmountPeriod
public function getId(): string
{
return 'userAmountWeek';
return 'UserAmountWeek';
}
public function getData(array $options = [])
public function getData(array $options = []): mixed
{
$this->setBegin('monday this week 00:00:00');
$this->setEnd('sunday this week 23:59:59');
return parent::getData($options);
return $this->getRevenue('monday this week 00:00:00', 'sunday this week 23:59:59', $options);
}
}

View File

@@ -11,36 +11,57 @@ namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Event\UserRevenueStatisticEvent;
use App\Model\Revenue;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
final class UserAmountYear extends CounterYear
final class UserAmountYear extends AbstractCounterYear
{
private $dispatcher;
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration, EventDispatcherInterface $dispatcher)
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration, private EventDispatcherInterface $dispatcher)
{
parent::__construct($repository, $systemConfiguration);
$this->dispatcher = $dispatcher;
$this->setId('userAmountYear');
$this->setOption('dataType', 'money');
$this->setOption('icon', 'money');
$this->setOption('color', WidgetInterface::COLOR_YEAR);
$this->setTitle('stats.amountYear');
}
public function getData(array $options = [])
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.amountFinancialYear';
}
public function getId(): string
{
return 'UserAmountYear';
}
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'money',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
public function getData(array $options = []): mixed
{
$this->titleYear = 'stats.amountFinancialYear';
$this->setQuery(TimesheetRepository::STATS_QUERY_RATE);
$this->setQueryWithUser(true);
/** @var array<Revenue> $data */
$data = parent::getData($options);
$event = new UserRevenueStatisticEvent($this->user, $this->begin, $this->end);
if ($data !== null) {
$event->addRevenue($data);
$event = new UserRevenueStatisticEvent($this->getUser(), $this->getBegin(), $this->getEnd());
foreach ($data as $row) {
$event->addRevenue($row->getCurrency(), $row->getAmount());
}
$this->dispatcher->dispatch($event);

View File

@@ -0,0 +1,39 @@
<?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 UserDurationMonth extends AbstractCounterDuration
{
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 'userDurationMonth';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(true);
$this->setBegin('first day of this month 00:00:00');
$this->setEnd('last day of this month 23:59:59');
return parent::getData($options);
}
}

View File

@@ -0,0 +1,39 @@
<?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 UserDurationToday extends AbstractCounterDuration
{
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 'userDurationToday';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(true);
$this->setBegin('00:00:00');
$this->setEnd('23:59:59');
return parent::getData($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 UserDurationTotal extends AbstractCounterDuration
{
public function getOptions(array $options = []): array
{
return array_merge(['color' => WidgetInterface::COLOR_TOTAL], parent::getOptions($options));
}
public function getPermissions(): array
{
return ['view_own_timesheet'];
}
public function getId(): string
{
return 'userDurationTotal';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(true);
return parent::getData($options);
}
}

View File

@@ -0,0 +1,39 @@
<?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 UserDurationWeek extends AbstractCounterDuration
{
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 'userDurationWeek';
}
public function getData(array $options = []): mixed
{
$this->setQueryWithUser(true);
$this->setBegin('monday this week 00:00:00');
$this->setEnd('sunday this week 23:59:59');
return parent::getData($options);
}
}

View File

@@ -9,27 +9,39 @@
namespace App\Widget\Type;
use App\Configuration\SystemConfiguration;
use App\Repository\TimesheetRepository;
use App\Widget\WidgetInterface;
final class UserDurationYear extends CounterYear
final class UserDurationYear extends AbstractCounterYear
{
public function __construct(TimesheetRepository $repository, SystemConfiguration $systemConfiguration)
public function getId(): string
{
parent::__construct($repository, $systemConfiguration);
$this->setId('userDurationYear');
$this->setOption('dataType', 'duration');
$this->setOption('icon', 'duration');
$this->setOption('color', 'yellow');
return 'userDurationYear';
}
public function getData(array $options = [])
public function getTemplateName(): string
{
return 'widget/widget-counter-duration.html.twig';
}
public function getOptions(array $options = []): array
{
return array_merge([
'icon' => 'duration',
'color' => WidgetInterface::COLOR_YEAR,
], parent::getOptions($options));
}
public function getData(array $options = []): mixed
{
$this->setTitle('stats.durationYear');
$this->titleYear = 'stats.durationFinancialYear';
$this->setQuery(TimesheetRepository::STATS_QUERY_DURATION);
$this->setQueryWithUser(true);
return parent::getData($options);
}
protected function getFinancialYearTitle(): string
{
return 'stats.durationFinancialYear';
}
}

View File

@@ -11,42 +11,57 @@ namespace App\Widget\Type;
use App\Entity\Project;
use App\Entity\Team;
use App\Entity\User;
use App\Project\ProjectStatisticService;
use App\Repository\Loader\ProjectLoader;
use App\Repository\Loader\TeamLoader;
use App\Widget\WidgetInterface;
use Doctrine\ORM\EntityManagerInterface;
class UserTeamProjects extends SimpleWidget implements AuthorizedWidget, UserWidget
final class UserTeamProjects extends AbstractWidget
{
private $statisticService;
private $entityManager;
public function __construct(ProjectStatisticService $statisticService, EntityManagerInterface $entityManager)
public function __construct(private ProjectStatisticService $statisticService, private EntityManagerInterface $entityManager)
{
$this->setId('UserTeamProjects');
$this->setTitle('label.my_team_projects');
$this->setOption('id', '');
$this->statisticService = $statisticService;
$this->entityManager = $entityManager;
}
public function getOptions(array $options = []): array
public function getWidth(): int
{
$options = parent::getOptions($options);
if (empty($options['id'])) {
$options['id'] = 'WidgetUserTeamProjects';
}
return $options;
return WidgetInterface::WIDTH_HALF;
}
public function getData(array $options = [])
public function getHeight(): int
{
$options = $this->getOptions($options);
/** @var User $user */
$user = $options['user'];
return WidgetInterface::HEIGHT_LARGE;
}
public function getTitle(): string
{
return 'my_team_projects';
}
public function getTemplateName(): string
{
return 'widget/widget-userteamprojects.html.twig';
}
/**
* @return string[]
*/
public function getPermissions(): array
{
return [
'budget_team_project', 'budget_teamlead_project', 'budget_project',
'time_team_project', 'time_teamlead_project', 'time_project',
];
}
public function getId(): string
{
return 'UserTeamProjects';
}
public function getData(array $options = []): mixed
{
$user = $this->getUser();
$now = new \DateTime('now', new \DateTimeZone($user->getTimezone()));
$loader = new TeamLoader($this->entityManager);
@@ -65,7 +80,7 @@ class UserTeamProjects extends SimpleWidget implements AuthorizedWidget, UserWid
}
}
$loader = new ProjectLoader($this->entityManager);
$loader = new ProjectLoader($this->entityManager, false, false, false);
$loader->loadResults($teamProjects);
foreach ($teamProjects as $id => $project) {
@@ -77,20 +92,4 @@ class UserTeamProjects extends SimpleWidget implements AuthorizedWidget, UserWid
return $this->statisticService->getBudgetStatisticModelForProjects($projects, $now);
}
/**
* @return string[]
*/
public function getPermissions(): array
{
return [
'budget_team_project', 'budget_teamlead_project', 'budget_project',
'time_team_project', 'time_teamlead_project', 'time_project',
];
}
public function setUser(User $user): void
{
$this->setOption('user', $user);
}
}

View File

@@ -9,35 +9,34 @@
namespace App\Widget\Type;
use App\Entity\User;
use App\Repository\Loader\UserLoader;
use App\Widget\WidgetInterface;
use Doctrine\ORM\EntityManagerInterface;
class UserTeams extends SimpleWidget implements AuthorizedWidget, UserWidget
final class UserTeams extends AbstractWidget
{
public function __construct()
public function __construct(private EntityManagerInterface $entityManager)
{
$this->setId('UserTeams');
$this->setTitle('label.my_teams');
$this->setOption('id', '');
}
public function getOptions(array $options = []): array
public function getWidth(): int
{
$options = parent::getOptions($options);
if (empty($options['id'])) {
$options['id'] = 'WidgetUserTeams';
}
return $options;
return WidgetInterface::WIDTH_HALF;
}
public function getData(array $options = [])
public function getHeight(): int
{
$options = $this->getOptions($options);
/** @var User $user */
$user = $options['user'];
return WidgetInterface::HEIGHT_LARGE;
}
return $user->getTeams();
public function getTitle(): string
{
return 'my_teams';
}
public function getTemplateName(): string
{
return 'widget/widget-userteams.html.twig';
}
/**
@@ -48,8 +47,19 @@ class UserTeams extends SimpleWidget implements AuthorizedWidget, UserWidget
return ['view_team_member', 'view_team'];
}
public function setUser(User $user): void
public function getId(): string
{
$this->setOption('user', $user);
return 'UserTeams';
}
public function getData(array $options = []): mixed
{
$user = $this->getUser();
// without this, every user would be lazy loaded
$loader = new UserLoader($this->entityManager, true);
$loader->loadResults([$user->getId()]);
return $user->getTeams();
}
}

View File

@@ -1,22 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
use App\Entity\User;
interface UserWidget
{
/**
* Sets the current user.
*
* @param User $user
*/
public function setUser(User $user): void;
}

View File

@@ -1,24 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
use App\Entity\User;
/**
* Needs to be used on a SimpleWidget
* @internal
*/
trait UserWidgetTrait
{
public function setUser(User $user): void
{
$this->setOption('user', $user);
}
}

View File

@@ -1,14 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget\Type;
final class YearChart extends SimpleStatisticChart
{
}

View File

@@ -1,24 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget;
interface WidgetContainerInterface extends WidgetInterface
{
public function getOrder(): int;
public function setOrder(int $order);
/**
* @return WidgetInterface[]
*/
public function getWidgets(): array;
public function addWidget(WidgetInterface $widget);
}

View File

@@ -9,6 +9,6 @@
namespace App\Widget;
class WidgetException extends \Exception
final class WidgetException extends \Exception
{
}

View File

@@ -9,6 +9,9 @@
namespace App\Widget;
use App\Entity\User;
use Symfony\Component\Form\Form;
interface WidgetInterface
{
public const COLOR_TODAY = 'green';
@@ -17,6 +20,16 @@ interface WidgetInterface
public const COLOR_YEAR = 'yellow';
public const COLOR_TOTAL = 'red';
public const WIDTH_FULL = 4;
public const WIDTH_LARGE = 3;
public const WIDTH_HALF = 2;
public const WIDTH_SMALL = 1;
public const HEIGHT_MAXIMUM = 6;
public const HEIGHT_LARGE = 5;
public const HEIGHT_MEDIUM = 3;
public const HEIGHT_SMALL = 1;
/**
* Returns a unique ID for this widget.
*
@@ -25,16 +38,36 @@ interface WidgetInterface
public function getId(): string;
/**
* Returns the widget title.
*
* If no title is necessary, return an empty string.
* Returns the widget title (must be non-empty).
*
* @return string
*/
public function getTitle(): string;
/**
* Returns the widgets data, to be used in the frontend rendering.
* Returns the height for this widget.
*
* @return int
*/
public function getHeight(): int;
/**
* Returns the width for this widget.
*
* @return int
*/
public function getWidth(): int;
/**
* Injects the current user.
*
* @param User $user
* @return void
*/
public function setUser(User $user): void;
/**
* Returns the widget data, to be used in the frontend rendering.
*
* If your widget relies on options to dynamically change the result data,
* make sure that the given $options will overwrite the internal option for
@@ -65,7 +98,43 @@ interface WidgetInterface
* The given option should be persisted and permanently overwrite the internal option.
*
* @param string $name
* @param mixed $value
* @param string|bool|int $value
*/
public function setOption(string $name, $value): void;
public function setOption(string $name, string|bool|int $value): void;
/**
* Return a list of granted syntax string.
* If ANY of the given permission strings matches, access is granted.
*
* @return string[]
*/
public function getPermissions(): array;
/**
* Returns the template, which is used to render the widget.
*
* @return string
*/
public function getTemplateName(): string;
/**
* Whether this widget can be configured with options.
*
* @return bool
*/
public function hasForm(): bool;
/**
* A form to edit the widget options or null, if it can't be configured.
*
* @return Form|null
*/
public function getForm(): ?Form;
/**
* Whether this is a widget that is supposed to be selectable by the end-user.
*
* @return bool
*/
public function isInternal(): bool;
}

View File

@@ -1,35 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Widget;
interface WidgetRendererInterface
{
/**
* Checks if the given widget can be rendered.
*
* Most renderer check something like:
* return $widget instanceof MyWidgetType;
*
* @param WidgetInterface $widget
* @return bool
*/
public function supports(WidgetInterface $widget): bool;
/**
* Renders the given widget.
*
* The given $options array overwrites the widgets internal options for this call.
*
* @param WidgetInterface $widget
* @param array<string, mixed> $options
* @return string
*/
public function render(WidgetInterface $widget, array $options = []): string;
}

View File

@@ -9,70 +9,41 @@
namespace App\Widget;
use App\Repository\WidgetRepository;
/**
* @final
*/
class WidgetService
{
/**
* @var WidgetRendererInterface[]
*/
private $renderer;
/**
* @var WidgetRepository
*/
private $repository;
private array $widgets = [];
/**
* @param WidgetRepository $repository
* @param WidgetRendererInterface[] $renderer
*/
public function __construct(WidgetRepository $repository, iterable $renderer)
public function hasWidget(string $id): bool
{
$this->renderer = $renderer;
$this->repository = $repository;
return \array_key_exists($id, $this->widgets);
}
public function hasWidget(string $widget): bool
public function registerWidget(WidgetInterface $widget): void
{
return $this->repository->has($widget);
}
public function getWidget(string $widget): WidgetInterface
{
return $this->repository->get($widget);
}
public function addRenderer(WidgetRendererInterface $renderer): WidgetService
{
$this->renderer[] = $renderer;
return $this;
$id = trim($widget->getId());
if ($id === '') {
throw new \InvalidArgumentException('Widget needs a non-empty ID');
}
$this->widgets[$id] = $widget;
}
/**
* @param WidgetInterface $widget
* @return WidgetRendererInterface
* @throws WidgetException
* @return array<string, WidgetInterface>
*/
public function findRenderer(WidgetInterface $widget): WidgetRendererInterface
public function getAllWidgets(): array
{
foreach ($this->renderer as $renderer) {
if ($renderer->supports($widget)) {
return $renderer;
}
return $this->widgets;
}
public function getWidget(string $id): WidgetInterface
{
if (!$this->hasWidget($id)) {
throw new \InvalidArgumentException(sprintf('Cannot find widget: %s', $id));
}
throw new WidgetException(sprintf('No renderer available for widget "%s"', \get_class($widget)));
}
/**
* @return WidgetRendererInterface[]
*/
public function getRenderer(): iterable
{
return $this->renderer;
return $this->widgets[$id];
}
}