users weekly stats as bar-chart in dashboard (#847)
This commit is contained in:
@@ -10,8 +10,10 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Model\DashboardSection;
|
||||
use App\Repository\WidgetRepository;
|
||||
use App\Widget\Type\CompoundChart;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use App\Widget\WidgetContainerInterface;
|
||||
use App\Widget\WidgetService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@@ -29,9 +31,9 @@ class DashboardController extends AbstractController
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
/**
|
||||
* @var WidgetRepository
|
||||
* @var WidgetService
|
||||
*/
|
||||
protected $repository;
|
||||
protected $widgets;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@@ -39,13 +41,13 @@ class DashboardController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param WidgetRepository $repository
|
||||
* @param WidgetService $service
|
||||
* @param array $dashboard
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, WidgetRepository $repository, array $dashboard)
|
||||
public function __construct(EventDispatcherInterface $dispatcher, WidgetService $service, array $dashboard)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->repository = $repository;
|
||||
$this->widgets = $service;
|
||||
$this->dashboard = $dashboard;
|
||||
}
|
||||
|
||||
@@ -61,22 +63,26 @@ class DashboardController extends AbstractController
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->isGranted($widgetRow['permission'])) {
|
||||
if (null !== $widgetRow['permission'] && !$this->isGranted($widgetRow['permission'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = new DashboardSection($widgetRow['title'] ?? null);
|
||||
$row
|
||||
->setOrder($widgetRow['order'])
|
||||
->setType($widgetRow['type'])
|
||||
;
|
||||
// TODO this should be dynamic
|
||||
if ($widgetRow['type'] === 'compoundChart') {
|
||||
$row = new CompoundChart();
|
||||
} else {
|
||||
$row = new CompoundRow();
|
||||
}
|
||||
|
||||
$row->setTitle($widgetRow['title'] ?? '');
|
||||
$row->setOrder($widgetRow['order']);
|
||||
|
||||
foreach ($widgetRow['widgets'] as $widgetName) {
|
||||
if (!$this->repository->has($widgetName)) {
|
||||
throw new \Exception('Unknwon widget: ' . $widgetName);
|
||||
if (!$this->widgets->hasWidget($widgetName)) {
|
||||
throw new \Exception(sprintf('Unknown widget "%s"', $widgetName));
|
||||
}
|
||||
|
||||
$row->addWidget($this->repository->get($widgetName, $event->getUser()));
|
||||
$row->addWidget($this->widgets->getWidget($widgetName));
|
||||
}
|
||||
|
||||
$event->addSection($row);
|
||||
@@ -91,7 +97,7 @@ class DashboardController extends AbstractController
|
||||
|
||||
uasort(
|
||||
$sections,
|
||||
function (DashboardSection $a, DashboardSection $b) {
|
||||
function (WidgetContainerInterface $a, WidgetContainerInterface $b) {
|
||||
if ($a->getOrder() == $b->getOrder()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -101,7 +107,7 @@ class DashboardController extends AbstractController
|
||||
);
|
||||
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'widget_rows' => $sections
|
||||
'widgets' => $sections
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
41
src/DependencyInjection/Compiler/WidgetCompilerPass.php
Normal file
41
src/DependencyInjection/Compiler/WidgetCompilerPass.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\DependencyInjection\Compiler;
|
||||
|
||||
use App\Kernel;
|
||||
use App\Repository\WidgetRepository;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Dynamically adds all widgets to the WidgetRepository.
|
||||
*/
|
||||
class WidgetCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
// always first check if the primary service is defined
|
||||
if (!$container->has(WidgetRepository::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $container->findDefinition(WidgetRepository::class);
|
||||
|
||||
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_WIDGET);
|
||||
foreach ($taggedRenderer as $id => $tags) {
|
||||
$definition->addMethodCall('registerWidget', [new Reference($id)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Model\DashboardSection;
|
||||
use App\Model\Widget;
|
||||
use App\Timesheet\Rounding\RoundingInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
@@ -319,6 +317,15 @@ class Configuration implements ConfigurationInterface
|
||||
->booleanNode('show_about')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->arrayNode('chart')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('background_color')->defaultValue('rgba(0,115,183,0.7)')->end()
|
||||
->scalarNode('border_color')->defaultValue('#3b8bba')->end()
|
||||
->scalarNode('grid_color')->defaultValue('rgba(0,0,0,.05)')->end()
|
||||
->scalarNode('height')->defaultValue('200')->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
@@ -365,12 +372,7 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('end')->end()
|
||||
->scalarNode('icon')->defaultValue('')->end()
|
||||
->scalarNode('color')->defaultValue('')->end()
|
||||
->scalarNode('type')
|
||||
->validate()
|
||||
->ifNotInArray([Widget::TYPE_COUNTER, Widget::TYPE_MORE])->thenInvalid('Unknown widget type')
|
||||
->end()
|
||||
->defaultValue(Widget::TYPE_COUNTER)
|
||||
->end()
|
||||
->scalarNode('type')->defaultValue('counter')->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
@@ -390,15 +392,10 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayPrototype()
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('type')
|
||||
->validate()
|
||||
->ifNotInArray([DashboardSection::TYPE_SIMPLE, DashboardSection::TYPE_CHART])->thenInvalid('Unknown section type')
|
||||
->end()
|
||||
->defaultValue(DashboardSection::TYPE_SIMPLE)
|
||||
->end()
|
||||
->scalarNode('type')->defaultValue('simple')->end()
|
||||
->integerNode('order')->defaultValue(0)->end()
|
||||
->scalarNode('title')->end()
|
||||
->scalarNode('permission')->isRequired()->end()
|
||||
->scalarNode('permission')->defaultNull()->end()
|
||||
->arrayNode('widgets')
|
||||
->isRequired()
|
||||
->performNoDeepMerging()
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait BudgetTrait
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\DashboardSection;
|
||||
use App\Widget\WidgetContainerInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
class DashboardEvent extends Event
|
||||
@@ -22,41 +22,31 @@ class DashboardEvent extends Event
|
||||
*/
|
||||
protected $user;
|
||||
/**
|
||||
* @var DashboardSection[]
|
||||
* @var WidgetContainerInterface[]
|
||||
*/
|
||||
protected $widgetRows = [];
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DashboardSection $row
|
||||
* @return DashboardEvent
|
||||
*/
|
||||
public function addSection(DashboardSection $row)
|
||||
public function addSection(WidgetContainerInterface $container): DashboardEvent
|
||||
{
|
||||
$this->widgetRows[] = $row;
|
||||
$this->widgetRows[] = $container;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DashboardSection[]
|
||||
* @return WidgetContainerInterface[]
|
||||
*/
|
||||
public function getSections()
|
||||
public function getSections(): array
|
||||
{
|
||||
return $this->widgetRows;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Model\DashboardSection;
|
||||
use App\Model\Widget;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use App\Widget\Type\More;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
@@ -79,7 +79,6 @@ class DashboardSubscriber implements EventSubscriberInterface
|
||||
|
||||
/**
|
||||
* @param DashboardEvent $event
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function onDashboardEvent(DashboardEvent $event)
|
||||
{
|
||||
@@ -87,54 +86,68 @@ class DashboardSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addAdminWidgets($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DashboardEvent $event
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
protected function addAdminWidgets(DashboardEvent $event)
|
||||
{
|
||||
$section = new DashboardSection('dashboard.admin');
|
||||
$section = new CompoundRow();
|
||||
$section->setTitle('ROLE_ADMIN');
|
||||
$section->setOrder(100);
|
||||
|
||||
$widget = new Widget('stats.userTotal', $this->user->countUser());
|
||||
$widget
|
||||
->setRoute('admin_user')
|
||||
->setIcon('user')
|
||||
->setColor('green')
|
||||
->setType(Widget::TYPE_MORE)
|
||||
;
|
||||
$section->addWidget($widget);
|
||||
if ($this->security->isGranted('view_user')) {
|
||||
$section->addWidget(
|
||||
(new More())
|
||||
->setId('userTotal')
|
||||
->setTitle('stats.userTotal')
|
||||
->setData($this->user->countUser())
|
||||
->setOptions([
|
||||
'route' => 'admin_user',
|
||||
'icon' => 'user',
|
||||
'color' => 'primary',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$widget = new Widget('stats.customerTotal', $this->customer->countCustomer());
|
||||
$widget
|
||||
->setRoute('admin_customer')
|
||||
->setIcon('customer')
|
||||
->setColor('blue')
|
||||
->setType(Widget::TYPE_MORE)
|
||||
;
|
||||
$section->addWidget($widget);
|
||||
if ($this->security->isGranted('view_customer')) {
|
||||
$section->addWidget(
|
||||
(new More())
|
||||
->setId('customerTotal')
|
||||
->setTitle('stats.customerTotal')
|
||||
->setData($this->customer->countCustomer())
|
||||
->setOptions([
|
||||
'route' => 'admin_customer',
|
||||
'icon' => 'customer',
|
||||
'color' => 'primary',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$widget = new Widget('stats.projectTotal', $this->project->countProject());
|
||||
$widget
|
||||
->setRoute('admin_project')
|
||||
->setIcon('project')
|
||||
->setColor('yellow')
|
||||
->setType(Widget::TYPE_MORE)
|
||||
;
|
||||
$section->addWidget($widget);
|
||||
if ($this->security->isGranted('view_project')) {
|
||||
$section->addWidget(
|
||||
(new More())
|
||||
->setId('projectTotal')
|
||||
->setTitle('stats.projectTotal')
|
||||
->setData($this->project->countProject())
|
||||
->setOptions([
|
||||
'route' => 'admin_project',
|
||||
'icon' => 'project',
|
||||
'color' => 'primary',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$widget = new Widget('stats.activityTotal', $this->activity->countActivity());
|
||||
$widget
|
||||
->setRoute('admin_activity')
|
||||
->setIcon('activity')
|
||||
->setColor('purple')
|
||||
->setType(Widget::TYPE_MORE)
|
||||
;
|
||||
$section->addWidget($widget);
|
||||
if ($this->security->isGranted('view_activity')) {
|
||||
$section->addWidget(
|
||||
(new More())
|
||||
->setId('activityTotal')
|
||||
->setTitle('stats.activityTotal')
|
||||
->setData($this->activity->countActivity())
|
||||
->setOptions([
|
||||
'route' => 'admin_activity',
|
||||
'icon' => 'activity',
|
||||
'color' => 'primary',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$event->addSection($section);
|
||||
if (count($section->getWidgets()) > 0) {
|
||||
$event->addSection($section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\DependencyInjection\AppExtension;
|
||||
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
|
||||
use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
|
||||
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
|
||||
use App\DependencyInjection\Compiler\WidgetCompilerPass;
|
||||
use App\Export\RendererInterface as ExportRendererInterface;
|
||||
use App\Invoice\CalculatorInterface as InvoiceCalculator;
|
||||
use App\Invoice\NumberGeneratorInterface;
|
||||
@@ -20,6 +21,8 @@ use App\Invoice\RendererInterface as InvoiceRendererInterface;
|
||||
use App\Ldap\FormLoginLdapFactory;
|
||||
use App\Plugin\PluginInterface;
|
||||
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetRendererInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
@@ -37,10 +40,13 @@ class Kernel extends BaseKernel
|
||||
public const CONFIG_EXTS = '.{php,xml,yaml,yml}';
|
||||
|
||||
public const TAG_PLUGIN = 'kimai.plugin';
|
||||
public const TAG_WIDGET = 'widget';
|
||||
public const TAG_WIDGET_RENDERER = 'widget.renderer';
|
||||
public const TAG_EXPORT_RENDERER = 'export.renderer';
|
||||
public const TAG_INVOICE_RENDERER = 'invoice.renderer';
|
||||
public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator';
|
||||
public const TAG_INVOICE_CALCULATOR = 'invoice.calculator';
|
||||
public const TAG_TIMESHEET_CALCULATOR = 'timesheet.calculator';
|
||||
|
||||
public function getCacheDir()
|
||||
{
|
||||
@@ -54,12 +60,14 @@ class Kernel extends BaseKernel
|
||||
|
||||
protected function build(ContainerBuilder $container)
|
||||
{
|
||||
$container->registerForAutoconfiguration(TimesheetCalculator::class)->addTag('timesheet.calculator');
|
||||
$container->registerForAutoconfiguration(TimesheetCalculator::class)->addTag(self::TAG_TIMESHEET_CALCULATOR);
|
||||
$container->registerForAutoconfiguration(ExportRendererInterface::class)->addTag(self::TAG_EXPORT_RENDERER);
|
||||
$container->registerForAutoconfiguration(InvoiceRendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
|
||||
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
|
||||
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
|
||||
$container->registerForAutoconfiguration(PluginInterface::class)->addTag(self::TAG_PLUGIN);
|
||||
$container->registerForAutoconfiguration(WidgetRendererInterface::class)->addTag(self::TAG_WIDGET_RENDERER);
|
||||
$container->registerForAutoconfiguration(WidgetInterface::class)->addTag(self::TAG_WIDGET);
|
||||
|
||||
/** @var SecurityExtension $extension */
|
||||
$extension = $container->getExtension('security');
|
||||
@@ -144,6 +152,7 @@ class Kernel extends BaseKernel
|
||||
$container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new InvoiceServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new WidgetCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
}
|
||||
|
||||
protected function configureRoutes(RouteCollectionBuilder $routes)
|
||||
|
||||
@@ -1,106 +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\Model;
|
||||
|
||||
class DashboardSection
|
||||
{
|
||||
public const TYPE_SIMPLE = 'simple';
|
||||
public const TYPE_CHART = 'chart';
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
protected $title;
|
||||
/**
|
||||
* @var Widget[]
|
||||
*/
|
||||
protected $widgets = [];
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $order = 0;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = self::TYPE_SIMPLE;
|
||||
|
||||
/**
|
||||
* @param null|string $title
|
||||
*/
|
||||
public function __construct(?string $title)
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return DashboardSection
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder(): int
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $order
|
||||
* @return DashboardSection
|
||||
*/
|
||||
public function setOrder(int $order)
|
||||
{
|
||||
$this->order = $order;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Widget[]
|
||||
*/
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return $this->widgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Widget $widget
|
||||
* @return DashboardSection
|
||||
*/
|
||||
public function addWidget(Widget $widget)
|
||||
{
|
||||
$this->widgets[] = $widget;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
64
src/Model/Statistic/Day.php
Normal file
64
src/Model/Statistic/Day.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Model\Statistic;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class Day
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $totalDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $totalRate = 0.00;
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
protected $day;
|
||||
|
||||
public function __construct(DateTime $day, int $duration, float $rate)
|
||||
{
|
||||
$this->day = $day;
|
||||
$this->totalDuration = $duration;
|
||||
$this->totalRate = $rate;
|
||||
}
|
||||
|
||||
public function getDay(): DateTime
|
||||
{
|
||||
return $this->day;
|
||||
}
|
||||
|
||||
public function getTotalDuration(): int
|
||||
{
|
||||
return $this->totalDuration;
|
||||
}
|
||||
|
||||
public function setTotalDuration(int $seconds): Day
|
||||
{
|
||||
$this->totalDuration = $seconds;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotalRate(): float
|
||||
{
|
||||
return $this->totalRate;
|
||||
}
|
||||
|
||||
public function setTotalRate(float $totalRate): Day
|
||||
{
|
||||
$this->totalRate = $totalRate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
namespace App\Model\Statistic;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Monthly statistics
|
||||
*/
|
||||
@@ -23,9 +25,9 @@ class Month
|
||||
*/
|
||||
protected $totalDuration = 0;
|
||||
/**
|
||||
* @var int
|
||||
* @var float
|
||||
*/
|
||||
protected $totalRate = 0;
|
||||
protected $totalRate = 0.00;
|
||||
|
||||
/**
|
||||
* @param string $month
|
||||
@@ -34,7 +36,9 @@ class Month
|
||||
{
|
||||
$monthNumber = (int) $month;
|
||||
if ($monthNumber < 1 || $monthNumber > 12) {
|
||||
throw new \InvalidArgumentException('Invalid month given, expected 01-12 but given: ' . $monthNumber);
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('Invalid month given. Expected 1-12, received "%s".', $monthNumber)
|
||||
);
|
||||
}
|
||||
$this->month = $month;
|
||||
}
|
||||
@@ -47,38 +51,24 @@ class Month
|
||||
return $this->month;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalDuration()
|
||||
public function getTotalDuration(): int
|
||||
{
|
||||
return $this->totalDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $totalDuration
|
||||
* @return $this
|
||||
*/
|
||||
public function setTotalDuration($totalDuration)
|
||||
public function setTotalDuration(int $seconds): Month
|
||||
{
|
||||
$this->totalDuration = $totalDuration;
|
||||
$this->totalDuration = $seconds;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalRate()
|
||||
public function getTotalRate(): float
|
||||
{
|
||||
return $this->totalRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $totalRate
|
||||
* @return $this
|
||||
*/
|
||||
public function setTotalRate($totalRate)
|
||||
public function setTotalRate(float $totalRate): Month
|
||||
{
|
||||
$this->totalRate = $totalRate;
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ class Year
|
||||
protected $months = [];
|
||||
|
||||
/**
|
||||
* Year constructor.
|
||||
* @param string $year
|
||||
*/
|
||||
public function __construct($year)
|
||||
@@ -40,22 +39,14 @@ class Year
|
||||
return $this->year;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Month $month
|
||||
* @return $this
|
||||
*/
|
||||
public function setMonth(Month $month)
|
||||
public function setMonth(Month $month): Year
|
||||
{
|
||||
$this->months[(int) $month->getMonth()] = $month;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $month
|
||||
* @return null|Month
|
||||
*/
|
||||
public function getMonth(int $month)
|
||||
public function getMonth(int $month): ?Month
|
||||
{
|
||||
if (isset($this->months[$month])) {
|
||||
return $this->months[$month];
|
||||
@@ -67,7 +58,7 @@ class Year
|
||||
/**
|
||||
* @return Month[]
|
||||
*/
|
||||
public function getMonths()
|
||||
public function getMonths(): array
|
||||
{
|
||||
return array_values($this->months);
|
||||
}
|
||||
|
||||
@@ -1,219 +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\Model;
|
||||
|
||||
class Widget
|
||||
{
|
||||
public const TYPE_COUNTER = 'counter';
|
||||
public const TYPE_MORE = 'more';
|
||||
|
||||
public const DATA_TYPE_INT = 'int';
|
||||
public const DATA_TYPE_MONEY = 'money';
|
||||
public const DATA_TYPE_DURATION = 'duration';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $title;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $icon = '';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $color = '';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = self::TYPE_COUNTER;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $range;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $route;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $routeOptions = [];
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $data;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $dataType = self::DATA_TYPE_INT;
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @param mixed $data
|
||||
*/
|
||||
public function __construct(string $title, $data)
|
||||
{
|
||||
$this->title = $title;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDataType(): string
|
||||
{
|
||||
return $this->dataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dataType
|
||||
* @return Widget
|
||||
*/
|
||||
public function setDataType(string $dataType)
|
||||
{
|
||||
$this->dataType = $dataType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getRouteOptions(): array
|
||||
{
|
||||
return $this->routeOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $routeOptions
|
||||
* @return Widget
|
||||
*/
|
||||
public function setRouteOptions(array $routeOptions)
|
||||
{
|
||||
$this->routeOptions = $routeOptions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRoute(): ?string
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @return Widget
|
||||
*/
|
||||
public function setRoute(string $route)
|
||||
{
|
||||
$this->route = $route;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
* @return Widget
|
||||
*/
|
||||
public function setData($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return Widget
|
||||
*/
|
||||
public function setTitle(string $title)
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $icon
|
||||
* @return Widget
|
||||
*/
|
||||
public function setIcon(string $icon)
|
||||
{
|
||||
$this->icon = $icon;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getColor(): string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $color
|
||||
* @return Widget
|
||||
*/
|
||||
public function setColor(string $color)
|
||||
{
|
||||
$this->color = $color;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return Widget
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace App\Repository;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Day;
|
||||
use App\Model\Statistic\Month;
|
||||
use App\Model\Statistic\Year;
|
||||
use App\Model\TimesheetStatistic;
|
||||
@@ -98,6 +99,9 @@ class TimesheetRepository extends AbstractRepository
|
||||
case self::STATS_QUERY_MONTHLY:
|
||||
return $this->getMonthlyStats($user, $begin, $end);
|
||||
|
||||
case 'daily':
|
||||
return $this->getDailyStats($user, $begin, $end);
|
||||
|
||||
case self::STATS_QUERY_DURATION:
|
||||
$what = 'SUM(t.duration)';
|
||||
break;
|
||||
@@ -253,14 +257,87 @@ class TimesheetRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
$month = new Month($statRow['month']);
|
||||
$month->setTotalDuration($statRow['duration'])
|
||||
->setTotalRate($statRow['rate']);
|
||||
$month->setTotalDuration((int) $statRow['duration'])
|
||||
->setTotalRate((float) $statRow['rate']);
|
||||
$years[$curYear]->setMonth($month);
|
||||
}
|
||||
|
||||
return $years;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @param User|null $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDailyData(DateTime $begin, DateTime $end, ?User $user = null)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
->addSelect('SUM(t.rate) as rate')
|
||||
->addSelect('SUM(t.duration) as duration')
|
||||
->addSelect('MONTH(t.begin) as month')
|
||||
->addSelect('YEAR(t.begin) as year')
|
||||
->addSelect('DAY(t.begin) as day')
|
||||
->from(Timesheet::class, 't')
|
||||
->andWhere($qb->expr()->gte('t.begin', ':from'))
|
||||
->setParameter('from', $begin, Type::DATETIME)
|
||||
->andWhere($qb->expr()->lte('t.end', ':to'))
|
||||
->setParameter('to', $end, Type::DATETIME);
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
$qb
|
||||
->addGroupBy('year')
|
||||
->addGroupBy('month')
|
||||
->addGroupBy('day')
|
||||
->addOrderBy('year', 'DESC')
|
||||
->addOrderBy('month', 'ASC')
|
||||
->addOrderBy('day', 'ASC')
|
||||
;
|
||||
|
||||
return $qb->getQuery()->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @return Day[]
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getDailyStats(User $user, DateTime $begin, DateTime $end): array
|
||||
{
|
||||
$results = $this->getDailyData($begin, $end, $user);
|
||||
|
||||
/** @var Day[] $days */
|
||||
$days = [];
|
||||
|
||||
// prefill the array
|
||||
$tmp = clone $end;
|
||||
$until = (int) $begin->format('Ymd');
|
||||
while ((int) $tmp->format('Ymd') > $until) {
|
||||
$tmp->modify('-1 day');
|
||||
$last = clone $tmp;
|
||||
$days[$last->format('Ymd')] = new Day($last, 0, 0.00);
|
||||
}
|
||||
|
||||
foreach ($results as $statRow) {
|
||||
$dateTime = new DateTime();
|
||||
$dateTime->setDate($statRow['year'], $statRow['month'], $statRow['day']);
|
||||
$days[$dateTime->format('Ymd')] = new Day($dateTime, (int) $statRow['duration'], (float) $statRow['rate']);
|
||||
}
|
||||
|
||||
ksort($days);
|
||||
|
||||
return array_values($days);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return Timesheet[]|null
|
||||
|
||||
@@ -10,8 +10,14 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\Widget;
|
||||
use App\Security\CurrentUser;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\WidgetException;
|
||||
use App\Widget\WidgetInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class WidgetRepository
|
||||
{
|
||||
/**
|
||||
@@ -22,61 +28,398 @@ class WidgetRepository
|
||||
* @var array
|
||||
*/
|
||||
protected $widgets = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $definitions = [];
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @param TimesheetRepository $repository
|
||||
* @param CurrentUser $user
|
||||
* @param array $widgets
|
||||
*/
|
||||
public function __construct(TimesheetRepository $repository, array $widgets)
|
||||
public function __construct(TimesheetRepository $repository, CurrentUser $user, array $widgets)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->widgets = $widgets;
|
||||
$this->user = $user->getUser();
|
||||
$this->definitions = array_merge($this->getDefaultWidgets(), $widgets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $name)
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return isset($this->widgets[$name]);
|
||||
return isset($this->definitions[$id]) || isset($this->widgets[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param User|null $user
|
||||
* @return Widget
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function get(string $name, ?User $user)
|
||||
public function registerWidget(WidgetInterface $widget): WidgetRepository
|
||||
{
|
||||
if (!$this->has($name)) {
|
||||
throw new \InvalidArgumentException('Cannot find widget: ' . $name);
|
||||
if (!empty($widget->getId())) {
|
||||
$this->widgets[$widget->getId()] = $widget;
|
||||
}
|
||||
|
||||
$widget = $this->widgets[$name];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get(string $id): WidgetInterface
|
||||
{
|
||||
if (!$this->has($id)) {
|
||||
throw new \InvalidArgumentException(sprintf('Cannot find widget "%s".', $id));
|
||||
}
|
||||
|
||||
if (isset($this->widgets[$id])) {
|
||||
return $this->widgets[$id];
|
||||
}
|
||||
|
||||
// this code should ONLY be reached for internal (pre-registered) widgets
|
||||
$this->registerWidget($this->create($id, $this->definitions[$id]));
|
||||
|
||||
return $this->widgets[$id];
|
||||
}
|
||||
|
||||
protected function create(string $name, array $widget): WidgetInterface
|
||||
{
|
||||
$user = $this->user;
|
||||
$begin = !empty($widget['begin']) ? new \DateTime($widget['begin']) : null;
|
||||
$end = !empty($widget['end']) ? new \DateTime($widget['end']) : null;
|
||||
$theUser = $widget['user'] ? $user : null;
|
||||
$type = $widget['type'] ?? Widget::TYPE_COUNTER;
|
||||
|
||||
if (!isset($widget['type'])) {
|
||||
@trigger_error('Using a widget definition without a "type" is deprecated', E_USER_DEPRECATED);
|
||||
$widget['type'] = 'counter';
|
||||
}
|
||||
|
||||
$widgetClassName = '\\App\\Widget\\Type\\' . ucfirst($widget['type']);
|
||||
if (!class_exists($widgetClassName)) {
|
||||
throw new WidgetException(sprintf('Unknown widget type "%s"', $widgetClassName));
|
||||
}
|
||||
|
||||
$model = new \ReflectionClass($widgetClassName);
|
||||
if (!$model->isSubclassOf(AbstractWidgetType::class)) {
|
||||
throw new WidgetException(sprintf('Invalid widget type "%s" does not extend AbstractWidgetType', $widgetClassName));
|
||||
}
|
||||
|
||||
$data = $this->repository->getStatistic($widget['query'], $begin, $end, $theUser);
|
||||
|
||||
$model = new Widget($widget['title'], $data);
|
||||
/** @var AbstractWidgetType $model */
|
||||
$model = new $widgetClassName();
|
||||
$model
|
||||
->setColor($widget['color'])
|
||||
->setIcon($widget['icon'])
|
||||
->setType($type)
|
||||
;
|
||||
->setId($name)
|
||||
->setTitle($widget['title'])
|
||||
->setData($data);
|
||||
|
||||
if ($widget['query'] == TimesheetRepository::STATS_QUERY_DURATION) {
|
||||
$model->setDataType(Widget::DATA_TYPE_DURATION);
|
||||
$model->setOption('dataType', 'duration');
|
||||
} elseif ($widget['query'] == TimesheetRepository::STATS_QUERY_RATE) {
|
||||
$model->setDataType(Widget::DATA_TYPE_MONEY);
|
||||
$model->setOption('dataType', 'money');
|
||||
} else {
|
||||
$model->setOption('dataType', 'int');
|
||||
}
|
||||
|
||||
if (isset($widget['color'])) {
|
||||
$model->setOption('color', $widget['color']);
|
||||
}
|
||||
if (isset($widget['icon'])) {
|
||||
$model->setOption('icon', $widget['icon']);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
protected function getDefaultWidgets(): array
|
||||
{
|
||||
return
|
||||
[
|
||||
'userDurationToday' => [
|
||||
'title' => 'stats.durationToday',
|
||||
'query' => 'duration',
|
||||
'user' => true,
|
||||
'begin' => '00:00:00',
|
||||
'end' => '23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'green',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userDurationWeek' => [
|
||||
'title' => 'stats.durationWeek',
|
||||
'query' => 'duration',
|
||||
'user' => true,
|
||||
'begin' => 'monday this week 00:00:00',
|
||||
'end' => 'sunday this week 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'blue',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userDurationMonth' => [
|
||||
'title' => 'stats.durationMonth',
|
||||
'query' => 'duration',
|
||||
'user' => true,
|
||||
'begin' => 'first day of this month 00:00:00',
|
||||
'end' => 'last day of this month 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'purple',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userDurationYear' => [
|
||||
'title' => 'stats.durationYear',
|
||||
'query' => 'duration',
|
||||
'user' => true,
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'yellow',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userDurationTotal' => [
|
||||
'title' => 'stats.durationTotal',
|
||||
'query' => 'duration',
|
||||
'user' => true,
|
||||
'icon' => 'duration',
|
||||
'color' => 'red',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userAmountToday' => [
|
||||
'title' => 'stats.amountToday',
|
||||
'query' => 'rate',
|
||||
'user' => true,
|
||||
'begin' => '00:00:00',
|
||||
'end' => '23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'green',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userAmountWeek' => [
|
||||
'title' => 'stats.amountWeek',
|
||||
'query' => 'rate',
|
||||
'user' => true,
|
||||
'begin' => 'monday this week 00:00:00',
|
||||
'end' => 'sunday this week 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'blue',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userAmountMonth' => [
|
||||
'title' => 'stats.amountMonth',
|
||||
'query' => 'rate',
|
||||
'user' => true,
|
||||
'begin' => 'first day of this month 00:00:00',
|
||||
'end' => 'last day of this month 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'purple',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userAmountYear' => [
|
||||
'title' => 'stats.amountYear',
|
||||
'query' => 'rate',
|
||||
'user' => true,
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'yellow',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userAmountTotal' => [
|
||||
'title' => 'stats.amountTotal',
|
||||
'query' => 'rate',
|
||||
'user' => true,
|
||||
'icon' => 'money',
|
||||
'color' => 'red',
|
||||
'type' => 'counter'
|
||||
],
|
||||
'durationToday' => [
|
||||
'title' => 'stats.durationToday',
|
||||
'query' => 'duration',
|
||||
'begin' => '00:00:00',
|
||||
'end' => '23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'green',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'durationWeek' => [
|
||||
'title' => 'stats.durationWeek',
|
||||
'query' => 'duration',
|
||||
'begin' => 'monday this week 00:00:00',
|
||||
'end' => 'sunday this week 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'blue',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'durationMonth' => [
|
||||
'title' => 'stats.durationMonth',
|
||||
'query' => 'duration',
|
||||
'begin' => 'first day of this month 00:00:00',
|
||||
'end' => 'last day of this month 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'purple',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'durationYear' => [
|
||||
'title' => 'stats.durationYear',
|
||||
'query' => 'duration',
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'duration',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'durationTotal' => [
|
||||
'title' => 'stats.durationTotal',
|
||||
'query' => 'duration',
|
||||
'icon' => 'duration',
|
||||
'color' => 'red',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'amountToday' => [
|
||||
'title' => 'stats.amountToday',
|
||||
'query' => 'rate',
|
||||
'begin' => '00:00:00',
|
||||
'end' => '23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'green',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'amountWeek' => [
|
||||
'title' => 'stats.amountWeek',
|
||||
'query' => 'rate',
|
||||
'begin' => 'monday this week 00:00:00',
|
||||
'end' => 'sunday this week 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'blue',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'amountMonth' => [
|
||||
'title' => 'stats.amountMonth',
|
||||
'query' => 'rate',
|
||||
'begin' => 'first day of this month 00:00:00',
|
||||
'end' => 'last day of this month 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'purple',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'amountYear' => [
|
||||
'title' => 'stats.amountYear',
|
||||
'query' => 'rate',
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'money',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'amountTotal' => [
|
||||
'title' => 'stats.amountTotal',
|
||||
'query' => 'rate',
|
||||
'icon' => 'money',
|
||||
'color' => 'red',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'activeUsersToday' => [
|
||||
'title' => 'stats.userActiveToday',
|
||||
'query' => 'users',
|
||||
'begin' => '00:00:00',
|
||||
'end' => '23:59:59',
|
||||
'icon' => 'user',
|
||||
'color' => 'green',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'activeUsersWeek' => [
|
||||
'title' => 'stats.userActiveWeek',
|
||||
'query' => 'users',
|
||||
'begin' => 'monday this week 00:00:00',
|
||||
'end' => 'sunday this week 23:59:59',
|
||||
'icon' => 'user',
|
||||
'color' => 'blue',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'activeUsersMonth' => [
|
||||
'title' => 'stats.userActiveMonth',
|
||||
'query' => 'users',
|
||||
'begin' => 'first day of this month 00:00:00',
|
||||
'end' => 'last day of this month 23:59:59',
|
||||
'icon' => 'user',
|
||||
'color' => 'purple',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'activeUsersYear' => [
|
||||
'title' => 'stats.userActiveYear',
|
||||
'query' => 'users',
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'icon' => 'user',
|
||||
'color' => 'yellow',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'activeUsersTotal' => [
|
||||
'title' => 'stats.userActiveTotal',
|
||||
'query' => 'users',
|
||||
'icon' => 'user',
|
||||
'color' => 'red',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'activeRecordings' => [
|
||||
'title' => 'stats.activeRecordings',
|
||||
'query' => 'active',
|
||||
'icon' => 'duration',
|
||||
'color' => 'red',
|
||||
'user' => false,
|
||||
'type' => 'counter'
|
||||
],
|
||||
'userRecapThisYear' => [
|
||||
'title' => 'stats.yourWorkingHours',
|
||||
'query' => 'monthly',
|
||||
'user' => true,
|
||||
'begin' => '01 january this year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'color' => '',
|
||||
'icon' => '',
|
||||
'type' => 'yearChart'
|
||||
],
|
||||
'userRecapLastYear' => [
|
||||
'title' => 'stats.yourWorkingHours',
|
||||
'query' => 'monthly',
|
||||
'user' => true,
|
||||
'begin' => '01 january last year 00:00:00',
|
||||
'end' => '31 december last year 23:59:59',
|
||||
'color' => 'rgba(0,115,183,0.7)|#3b8bba',
|
||||
'icon' => '',
|
||||
'type' => 'yearChart'
|
||||
],
|
||||
'userRecapTwoYears' => [
|
||||
'title' => 'stats.yourWorkingHours',
|
||||
'query' => 'monthly',
|
||||
'user' => true,
|
||||
'begin' => '01 january last year 00:00:00',
|
||||
'end' => '31 december this year 23:59:59',
|
||||
'color' => 'rgba(0,115,183,0.6)|#3b8bba;rgba(233,233,233,0.8)|#ccc',
|
||||
'icon' => '',
|
||||
'type' => 'yearChart'
|
||||
],
|
||||
'userRecapThreeYears' => [
|
||||
'title' => 'stats.yourWorkingHours',
|
||||
'query' => 'monthly',
|
||||
'user' => true,
|
||||
'begin' => '2 years ago first day of january 00:00:00',
|
||||
'end' => 'this year last day of december 23:59:59',
|
||||
'color' => 'rgba(0,115,183,0.4)|#3b8bba;rgba(233,233,233,0.7)|#ccc;rgba(210,214,222,0.9)|#c1c7d1',
|
||||
'icon' => '',
|
||||
'type' => 'yearChart'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
65
src/Twig/WidgetExtension.php
Normal file
65
src/Twig/WidgetExtension.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Twig;
|
||||
|
||||
use App\Widget\WidgetException;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetService;
|
||||
use InvalidArgumentException;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class WidgetExtension extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var WidgetService
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
public function __construct(WidgetService $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('render_widget', [$this, 'renderWidget'], ['is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WidgetInterface|string $widget
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @throws WidgetException
|
||||
*/
|
||||
public function renderWidget($widget, array $options = [])
|
||||
{
|
||||
if (!($widget instanceof WidgetInterface) && !is_string($widget)) {
|
||||
throw new InvalidArgumentException('Widget must either implement WidgetInterface or be a string');
|
||||
}
|
||||
|
||||
if (is_string($widget)) {
|
||||
if (!$this->service->hasWidget($widget)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown widget "%s" requested', $widget));
|
||||
}
|
||||
|
||||
$widget = $this->service->getWidget($widget);
|
||||
}
|
||||
|
||||
$renderer = $this->service->findRenderer($widget);
|
||||
|
||||
return $renderer->render($widget, $options);
|
||||
}
|
||||
}
|
||||
42
src/Widget/Renderer/AbstractTwigRenderer.php
Normal file
42
src/Widget/Renderer/AbstractTwigRenderer.php
Normal 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\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);
|
||||
}
|
||||
}
|
||||
29
src/Widget/Renderer/CompoundChartRenderer.php
Normal file
29
src/Widget/Renderer/CompoundChartRenderer.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
29
src/Widget/Renderer/CompoundRowRenderer.php
Normal file
29
src/Widget/Renderer/CompoundRowRenderer.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
src/Widget/Renderer/SimpleWidgetRenderer.php
Normal file
33
src/Widget/Renderer/SimpleWidgetRenderer.php
Normal 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\Renderer;
|
||||
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
use App\Widget\WidgetInterface;
|
||||
use ReflectionClass;
|
||||
|
||||
class SimpleWidgetRenderer extends AbstractTwigRenderer
|
||||
{
|
||||
public function supports(WidgetInterface $widget): bool
|
||||
{
|
||||
return $widget instanceof SimpleWidget;
|
||||
}
|
||||
|
||||
public function render(WidgetInterface $widget, array $options = []): string
|
||||
{
|
||||
$name = (new ReflectionClass($widget))->getShortName();
|
||||
|
||||
return $this->renderTemplate(sprintf('widget/widget-%s.html.twig', strtolower($name)), [
|
||||
'data' => $widget->getData($options),
|
||||
'options' => $widget->getOptions($options),
|
||||
'title' => $widget->getTitle(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
95
src/Widget/Type/AbstractContainer.php
Normal file
95
src/Widget/Type/AbstractContainer.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
109
src/Widget/Type/AbstractWidgetType.php
Normal file
109
src/Widget/Type/AbstractWidgetType.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?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;
|
||||
|
||||
abstract class AbstractWidgetType implements WidgetInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id = '';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $title = '';
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
public function setId(string $id): AbstractWidgetType
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setData($data): AbstractWidgetType
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): AbstractWidgetType
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setOptions(array $options): AbstractWidgetType
|
||||
{
|
||||
foreach ($options as $key => $value) {
|
||||
$this->options[$key] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function setOption(string $name, $value): void
|
||||
{
|
||||
$this->options[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getOption(string $name, $default = null)
|
||||
{
|
||||
if (array_key_exists($name, $this->options)) {
|
||||
return $this->options[$name];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
return array_merge($this->options, $options);
|
||||
}
|
||||
}
|
||||
14
src/Widget/Type/CompoundChart.php
Normal file
14
src/Widget/Type/CompoundChart.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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
|
||||
{
|
||||
}
|
||||
14
src/Widget/Type/CompoundRow.php
Normal file
14
src/Widget/Type/CompoundRow.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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
|
||||
{
|
||||
}
|
||||
18
src/Widget/Type/Counter.php
Normal file
18
src/Widget/Type/Counter.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?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 Counter extends SimpleWidget
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->setOption('dataType', 'int');
|
||||
}
|
||||
}
|
||||
65
src/Widget/Type/DailyWorkingTimeChart.php
Normal file
65
src/Widget/Type/DailyWorkingTimeChart.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Security\CurrentUser;
|
||||
use DateTime;
|
||||
|
||||
class DailyWorkingTimeChart extends SimpleWidget
|
||||
{
|
||||
public const DEFAULT_CHART = 'bar';
|
||||
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
public function __construct(TimesheetRepository $repository, CurrentUser $user)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->setId('DailyWorkingTimeChart');
|
||||
$this->setTitle('stats.yourWorkingHours');
|
||||
$this->setOptions([
|
||||
'begin' => 'monday this week 00:00:00',
|
||||
'end' => 'sunday this week 23:59:59',
|
||||
'color' => '',
|
||||
'user' => $user->getUser(),
|
||||
'type' => self::DEFAULT_CHART,
|
||||
'id' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$options = $this->getOptions($options);
|
||||
|
||||
$user = $options['user'];
|
||||
$begin = new DateTime($options['begin']);
|
||||
$end = new DateTime($options['end']);
|
||||
|
||||
return $this->repository->getDailyStats($user, $begin, $end);
|
||||
}
|
||||
}
|
||||
18
src/Widget/Type/More.php
Normal file
18
src/Widget/Type/More.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?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 More extends SimpleWidget
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->setOption('dataType', 'int');
|
||||
}
|
||||
}
|
||||
14
src/Widget/Type/SimpleWidget.php
Normal file
14
src/Widget/Type/SimpleWidget.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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
|
||||
{
|
||||
}
|
||||
14
src/Widget/Type/YearChart.php
Normal file
14
src/Widget/Type/YearChart.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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 YearChart extends SimpleWidget
|
||||
{
|
||||
}
|
||||
24
src/Widget/WidgetContainerInterface.php
Normal file
24
src/Widget/WidgetContainerInterface.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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);
|
||||
}
|
||||
14
src/Widget/WidgetException.php
Normal file
14
src/Widget/WidgetException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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;
|
||||
|
||||
class WidgetException extends \Exception
|
||||
{
|
||||
}
|
||||
65
src/Widget/WidgetInterface.php
Normal file
65
src/Widget/WidgetInterface.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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 WidgetInterface
|
||||
{
|
||||
/**
|
||||
* Returns a unique ID for this widget.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* Returns the widget title.
|
||||
*
|
||||
* If no title is necessary, return an empty string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* Returns the widgets 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
|
||||
* this one call.
|
||||
*
|
||||
* @param array $options
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getData(array $options = []);
|
||||
|
||||
/**
|
||||
* Returns all widget options to be used in the frontend.
|
||||
*
|
||||
* The given $options are not meant to be persisted, but only to
|
||||
* overwrite the default values one time.
|
||||
*
|
||||
* You can validate the options or simply return:
|
||||
* return array_merge($this->options, $options);
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function getOptions(array $options = []): array;
|
||||
|
||||
/**
|
||||
* Sets one widget option, both for internal use and for frontend rendering.
|
||||
*
|
||||
* The given option should be persisted and permanently overwrite the internal option.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function setOption(string $name, $value): void;
|
||||
}
|
||||
35
src/Widget/WidgetRendererInterface.php
Normal file
35
src/Widget/WidgetRendererInterface.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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 $options
|
||||
* @return string
|
||||
*/
|
||||
public function render(WidgetInterface $widget, array $options = []): string;
|
||||
}
|
||||
81
src/Widget/WidgetService.php
Normal file
81
src/Widget/WidgetService.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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;
|
||||
|
||||
use App\Repository\WidgetRepository;
|
||||
|
||||
class WidgetService
|
||||
{
|
||||
/**
|
||||
* @var WidgetRendererInterface[]
|
||||
*/
|
||||
protected $renderer = [];
|
||||
/**
|
||||
* @var WidgetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @param WidgetRepository $repository
|
||||
* @param WidgetRendererInterface[] $renderer
|
||||
*/
|
||||
public function __construct(WidgetRepository $repository, iterable $renderer)
|
||||
{
|
||||
foreach ($renderer as $render) {
|
||||
$this->addRenderer($render);
|
||||
}
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $widget
|
||||
* @return bool
|
||||
*/
|
||||
public function hasWidget(string $widget): bool
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WidgetInterface $widget
|
||||
* @return WidgetRendererInterface
|
||||
* @throws WidgetException
|
||||
*/
|
||||
public function findRenderer(WidgetInterface $widget): WidgetRendererInterface
|
||||
{
|
||||
foreach ($this->renderer as $renderer) {
|
||||
if ($renderer->supports($widget)) {
|
||||
return $renderer;
|
||||
}
|
||||
}
|
||||
|
||||
throw new WidgetException(sprintf('No renderer available for widget "%s"', get_class($widget)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WidgetRendererInterface[]
|
||||
*/
|
||||
public function getRenderer(): array
|
||||
{
|
||||
return $this->renderer;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user