users weekly stats as bar-chart in dashboard (#847)
This commit is contained in:
12
README.md
12
README.md
@@ -27,12 +27,12 @@ It is built with modern technologies such as Symfony, Bootstrap, RESTful API, re
|
||||
|
||||
### About
|
||||
|
||||
This new version of the open source timetracker Kimai. It is in a stable development phase, usable in production and
|
||||
with most advanced features from Kimai 1 and many new ones:
|
||||
This is new version of the open source timetracker Kimai. It is in a stable development phase, usable in production and
|
||||
with most advanced features from Kimai 1 and many new ones, including but not limited to:
|
||||
|
||||
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
||||
LDAP and built-in authentication, customizable role permissions, responsive and usable on your mobile device,
|
||||
hourly and fixed rates, advanced filtering, support for plugins and many more.
|
||||
LDAP and built-in authentication, customizable role permissions, responsive and ready for your mobile device,
|
||||
hourly and fixed rates, advanced filtering, money and time budgets and report, support for plugins and many more.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -40,7 +40,7 @@ hourly and fixed rates, advanced filtering, support for plugins and many more.
|
||||
- [Docker](https://www.kimai.org/documentation/docker.html) - containerized
|
||||
- [Development](https://www.kimai.org/documentation/installation.html#development-installation) - on your local machine
|
||||
- [1-click installer](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations) - hosted environments
|
||||
- [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation) - unfortunately still widely used
|
||||
- [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation) - unfortunately still widely used ;-)
|
||||
|
||||
### Updating Kimai
|
||||
|
||||
@@ -49,7 +49,7 @@ hourly and fixed rates, advanced filtering, support for plugins and many more.
|
||||
|
||||
### Plugins
|
||||
|
||||
- [Plugin marketplace](https://www.kimai.org/store/) - users find existing plugins here
|
||||
- [Plugin marketplace](https://www.kimai.org/store/) - find existing plugins here
|
||||
- [Developer documentation](https://www.kimai.org/documentation/developers.html) - how to create a plugin
|
||||
|
||||
## Roadmap and releases
|
||||
|
||||
@@ -35,6 +35,7 @@ Removed permission:
|
||||
- API: Format for queries including a datetime object fixed to use HTML5 format (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00`)
|
||||
- **Permission config**: the `permissions` definition in your `local.yaml` needs to be verified/changed, as the internal structure was highly optimized to simplify the definition.
|
||||
Thanks to the new structure, you should be able to remove almost everything from your `local.yaml` (tip: start over from scratch!). Please read [the updated permission docu](https://www.kimai.org/documentation/permissions.html).
|
||||
- default widgets were removed from `kimai.yaml`, that shouldn't cause any issues ... but if something is odd, now you know [where to look for help](https://www.kimai.org/documentation/dashboard.html)
|
||||
|
||||
## [0.9](https://github.com/kevinpapst/kimai2/releases/tag/0.9)
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ export default class KimaiAlert extends KimaiPlugin {
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
timer: 1500,
|
||||
timer: 2000,
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
showConfirmButton: false,
|
||||
type: 'success',
|
||||
|
||||
30
composer.lock
generated
30
composer.lock
generated
@@ -2971,33 +2971,37 @@
|
||||
},
|
||||
{
|
||||
"name": "kevinpapst/adminlte-bundle",
|
||||
"version": "2.7.3",
|
||||
"version": "2.8.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kevinpapst/AdminLTEBundle.git",
|
||||
"reference": "bbe650bda5319ac55637457e23d60bcafb734da1"
|
||||
"reference": "62df10d0565f2ed5971b1a28ef30195ad1893807"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/bbe650bda5319ac55637457e23d60bcafb734da1",
|
||||
"reference": "bbe650bda5319ac55637457e23d60bcafb734da1",
|
||||
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/62df10d0565f2ed5971b1a28ef30195ad1893807",
|
||||
"reference": "62df10d0565f2ed5971b1a28ef30195ad1893807",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"symfony/config": ">3.4",
|
||||
"symfony/dependency-injection": ">3.4",
|
||||
"symfony/event-dispatcher": ">3.4",
|
||||
"symfony/http-foundation": ">3.4",
|
||||
"symfony/http-kernel": ">3.4",
|
||||
"symfony/options-resolver": ">3.4",
|
||||
"symfony/security-core": ">3.4",
|
||||
"symfony/config": ">4.0",
|
||||
"symfony/dependency-injection": ">4.0",
|
||||
"symfony/event-dispatcher": ">4.0",
|
||||
"symfony/http-foundation": ">4.0",
|
||||
"symfony/http-kernel": ">4.0",
|
||||
"symfony/options-resolver": ">4.0",
|
||||
"symfony/security-core": ">4.0",
|
||||
"twig/twig": ">2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.10",
|
||||
"knplabs/knp-menu-bundle": "^2.2",
|
||||
"phpspec/prophecy": "^1.6",
|
||||
"phpunit/phpunit": "^7.3"
|
||||
"phpstan/phpstan": "^0.11.8",
|
||||
"phpstan/phpstan-phpunit": "^0.11.2",
|
||||
"phpunit/phpunit": "^7.3",
|
||||
"symfony/framework-bundle": ">4.0"
|
||||
},
|
||||
"suggest": {
|
||||
"friendsofsymfony/user-bundle": "Allows easy user management and security support",
|
||||
@@ -3028,7 +3032,7 @@
|
||||
}
|
||||
],
|
||||
"description": "Admin theme bundle for Symfony 4 based on AdminLTE 2.4.8 with FOSUserBundle support",
|
||||
"time": "2019-05-13T16:17:08+00:00"
|
||||
"time": "2019-06-13T11:25:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "kimai/kimai2-composer",
|
||||
|
||||
@@ -13,5 +13,12 @@ jms_serializer:
|
||||
App:
|
||||
namespace_prefix: "App"
|
||||
path: "%kernel.root_dir%/../config/serializer/App"
|
||||
warmup:
|
||||
paths:
|
||||
included:
|
||||
- "%kernel.root_dir%/Entity/"
|
||||
- "%kernel.root_dir%/API/Model/"
|
||||
- "%kernel.root_dir%/../vendor/friendsofsymfony/user-bundle/Model"
|
||||
excluded: []
|
||||
property_naming:
|
||||
id: 'jms_serializer.identical_property_naming_strategy'
|
||||
|
||||
@@ -169,7 +169,8 @@ kimai:
|
||||
title: dashboard.you
|
||||
order: 10
|
||||
permission: ROLE_USER
|
||||
widgets: [userDurationToday, userDurationWeek, userDurationMonth, userDurationYear]
|
||||
type: 'compoundChart'
|
||||
widgets: [DailyWorkingTimeChart, userDurationToday, userDurationWeek, userDurationMonth, userDurationYear]
|
||||
user_rates:
|
||||
title: ~
|
||||
order: 20
|
||||
@@ -190,34 +191,6 @@ kimai:
|
||||
order: 50
|
||||
permission: view_rate_other_timesheet
|
||||
widgets: [amountToday, amountWeek, amountMonth, amountYear]
|
||||
|
||||
widgets:
|
||||
userDurationToday: { title: stats.durationToday, query: duration, user: true, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
userDurationTotal: { title: stats.durationTotal, query: duration, user: true, icon: duration, color: red }
|
||||
userAmountToday: { title: stats.amountToday, query: rate, user: true, begin: '00:00:00', end: '23:59:59', icon: money, color: green }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
userAmountTotal: { title: stats.amountTotal, query: rate, user: true, icon: money, color: red }
|
||||
durationToday: { title: stats.durationToday, query: duration, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
|
||||
durationWeek: { title: stats.durationWeek, query: duration, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: duration, color: blue }
|
||||
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 }
|
||||
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 }
|
||||
durationTotal: { title: stats.durationTotal, query: duration, icon: duration, color: red }
|
||||
amountToday: { title: stats.amountToday, query: rate, begin: '00:00:00', end: '23:59:59', icon: money, color: green }
|
||||
amountWeek: { title: stats.amountWeek, query: rate, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: money, color: blue }
|
||||
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 }
|
||||
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 }
|
||||
amountTotal: { title: stats.amountTotal, query: rate, icon: money, color: red }
|
||||
activeUsersToday: { title: stats.userActiveToday, query: users, begin: '00:00:00', end: '23:59:59', icon: user, color: green }
|
||||
activeUsersWeek: { title: stats.userActiveWeek, query: users, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: user, color: blue }
|
||||
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 }
|
||||
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 }
|
||||
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
|
||||
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -77,6 +77,10 @@ services:
|
||||
App\Export\ServiceExport:
|
||||
arguments: [!tagged export.renderer]
|
||||
|
||||
App\Widget\WidgetService:
|
||||
arguments:
|
||||
$renderer: !tagged widget.renderer
|
||||
|
||||
# ================================================================================
|
||||
# DATABASE
|
||||
# ================================================================================
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
"build/runtime.784b5d0d.js",
|
||||
"build/0.66b5fd0b.js",
|
||||
"build/1.93f0cf2b.js",
|
||||
"build/app.26c26669.js"
|
||||
"build/app.0ac6f178.js"
|
||||
],
|
||||
"css": [
|
||||
"build/app.7f4a18a1.css"
|
||||
@@ -35,7 +35,7 @@
|
||||
"build/runtime.784b5d0d.js": "sha384-+AsC7zoefPga1AkczlNSCXcbGwD0TwokkldvNISHf1nMlOpafanLjtKvlZy9cVsm",
|
||||
"build/0.66b5fd0b.js": "sha384-ncT/BKhCsqH6jhxwdsSG95m1ei7ZZjeZtzH1262h+OPUU80TSFFE3dt+abcHHMok",
|
||||
"build/1.93f0cf2b.js": "sha384-pmnwzvj+UJhDD3uDf+ekLARMBRtMDxWGbnJDrJvwIoRZd3MW/tOGzAW5Ybhbvw20",
|
||||
"build/app.26c26669.js": "sha384-SyN1k+3kOWKnJSOyY+atWLSXNubTbsgz1DZ47jH8PrgLFj9WjXB9GRhO6E7tpJWf",
|
||||
"build/app.0ac6f178.js": "sha384-eORpGh3VKK5IISACV4rmp8CpkZ+s47G8aKaZnXVpswgZccU/YcJPkSa5+jhcI88T",
|
||||
"build/app.7f4a18a1.css": "sha384-p8Dw7CP1RxxJ2K5yuaNpCw8r1lKd64DlVA2IQeEGxBAC2QxOjweafd8w9CSMk0GC",
|
||||
"build/2.c06cd055.js": "sha384-txR0QG+838LKYtPQ99Gx4OU7WmgN9J3joZEyGwIskSz74EN1T4/IBVnmNaKiFN1q",
|
||||
"build/chart.2bb71e2e.js": "sha384-I57c9DtU3AOG2kzKqIZkIu0hi1aGYHRZ5QG4LKC9+9slzJnAMttPGXoL2cQG3m6y",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"build/1.93f0cf2b.js": "build/1.93f0cf2b.js",
|
||||
"build/2.c06cd055.js": "build/2.c06cd055.js",
|
||||
"build/app.css": "build/app.7f4a18a1.css",
|
||||
"build/app.js": "build/app.26c26669.js",
|
||||
"build/app.js": "build/app.0ac6f178.js",
|
||||
"build/calendar.css": "build/calendar.b0551848.css",
|
||||
"build/calendar.js": "build/calendar.59417f4c.js",
|
||||
"build/chart.js": "build/chart.2bb71e2e.js",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -35,13 +35,8 @@
|
||||
{% set currency = activity.project.customer.currency %}
|
||||
{% endif %}
|
||||
|
||||
{% if activity.budget > 0 %}
|
||||
{{ progress.progressbar(activity.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(currency) ~ ' / ' ~ activity.budget|money(currency) ) }}
|
||||
{% endif %}
|
||||
|
||||
{% if activity.timeBudget > 0 %}
|
||||
{{ progress.progressbar(activity.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ activity.timeBudget|duration ) }}
|
||||
{% endif %}
|
||||
{{ progress.progressbar(activity.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(currency) ~ ' / ' ~ activity.budget|money(currency) ) }}
|
||||
{{ progress.progressbar(activity.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ activity.timeBudget|duration ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
|
||||
@@ -24,13 +24,8 @@
|
||||
{{ 'admin_customer.short_stats'|trans(params)|raw }}
|
||||
</p>
|
||||
|
||||
{% if customer.budget > 0 %}
|
||||
{{ progress.progressbar(customer.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(customer.currency) ~ ' / ' ~ customer.budget|money(customer.currency) ) }}
|
||||
{% endif %}
|
||||
|
||||
{% if customer.timeBudget > 0 %}
|
||||
{{ progress.progressbar(customer.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ customer.timeBudget|duration ) }}
|
||||
{% endif %}
|
||||
{{ progress.progressbar(customer.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(customer.currency) ~ ' / ' ~ customer.budget|money(customer.currency) ) }}
|
||||
{{ progress.progressbar(customer.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ customer.timeBudget|duration ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
|
||||
@@ -18,12 +18,8 @@
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% for row in widget_rows %}
|
||||
{% if row.type == constant('App\\Model\\DashboardSection::TYPE_CHART') %}
|
||||
{% embed 'dashboard/section-chart.html.twig' with { section: row } %}{% endembed %}
|
||||
{% else %}
|
||||
{% embed 'dashboard/section-simple.html.twig' with { section: row } %}{% endembed %}
|
||||
{% endif %}
|
||||
{% for row in widgets %}
|
||||
{{ render_widget(row) }}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% if section.title %}
|
||||
{{ widgets.page_header(section.title) }}
|
||||
{% endif %}
|
||||
|
||||
{% set width = section.widgets|length %}
|
||||
{% set rawWidth = 12 / width %}
|
||||
{% set columnWidth = rawWidth|round(0, 'floor') %}
|
||||
<div class="row">
|
||||
{% for widget in section.widgets %}
|
||||
{% set columnSize = columnWidth %}
|
||||
{% if width == 5 and (loop.first or loop.last) %}
|
||||
{% set columnSize = columnWidth + 1 %}
|
||||
{% endif %}
|
||||
<div class="col-md-{{ columnSize }} col-sm-{{ columnSize * 2 }} col-xs-{{ columnSize * 4 }}">
|
||||
|
||||
{% set data = widget.data %}
|
||||
{% if widget.dataType == constant('App\\Model\\Widget::DATA_TYPE_DURATION') %}
|
||||
{% set data = widget.data|duration %}
|
||||
{% elseif widget.dataType == constant('App\\Model\\Widget::DATA_TYPE_MONEY') %}
|
||||
{% set data = widget.data|money %}
|
||||
{% endif %}
|
||||
|
||||
{% set url = null %}
|
||||
{% if widget.route %}
|
||||
{% set url = path(widget.route, widget.routeOptions) %}
|
||||
{% endif %}
|
||||
|
||||
{% set embedOptions = {data: data, title: widget.title, icon: widget.icon, color: widget.color, url: url} %}
|
||||
{% embed 'embeds/widget-'~widget.type~'.html.twig' with embedOptions %}{% endembed %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-{{ color|default('green') }}"><i class="{{ icon|icon(icon) }}"></i></span>
|
||||
|
||||
<div class="info-box-content">
|
||||
{% if url %}<a href="{{ url }}" class="small-box-footer">{% endif %}
|
||||
<span class="info-box-text">{{ title|trans }}</span>
|
||||
<span class="info-box-number">{{ data }}</span>
|
||||
{% if url %}</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="small-box bg-{{ color|default('green') }}">
|
||||
<div class="inner">
|
||||
<h3>{{ data }}<sup style="font-size: 20px">{{ unit|default('') }}</sup></h3>
|
||||
<p>{{ title|trans }}</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="{{ icon|icon(icon) }}"></i>
|
||||
</div>
|
||||
{% if url %}
|
||||
<a href="{{ url }}" class="small-box-footer">
|
||||
{{ 'more.info.link'|trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,5 +1,8 @@
|
||||
{% macro progressbar(max, current, title, subTitle) %}
|
||||
{% set percentReached = (current / (max / 100)) %}
|
||||
{% set percentReached = 0 %}
|
||||
{% if max > 0 %}
|
||||
{% set percentReached = (current / (max / 100)) %}
|
||||
{% endif %}
|
||||
{% set class = "progress-bar-info" %}
|
||||
{% set width = percentReached|number_format(1, '.', '') %}
|
||||
|
||||
@@ -18,7 +21,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="progress-group">
|
||||
<span class="progress-text">{{ title }} – {{ percentReached|number_format(2) }}%</span>
|
||||
<span class="progress-text">{{ title }}{% if percentReached > 0 %} – {{ percentReached|number_format(2) }}%{% endif %}</span>
|
||||
<span class="progress-number">{{ subTitle }}</span>
|
||||
|
||||
<div class="progress">
|
||||
|
||||
@@ -254,29 +254,17 @@
|
||||
{% endif %}
|
||||
|
||||
<a class="{{ class | trim }}" href="{{ url }}"
|
||||
{%- if disabled is same as (true) -%}
|
||||
disabled="disabled"
|
||||
{%- endif -%}
|
||||
{%- if id is not empty -%}
|
||||
id="{{ id }}"
|
||||
{%- endif -%}
|
||||
{%- if toggle is not empty -%}
|
||||
data-toggle="{{ toggle }}"
|
||||
{%- endif -%}
|
||||
{%- if modal is not empty -%}
|
||||
data-toggle="modal" data-target="{{ modal }}"
|
||||
{%- endif -%}
|
||||
{%- if onclick is not empty -%}
|
||||
onclick="{{ onclick }}"
|
||||
{%- endif -%}
|
||||
{%- if target is not empty -%}
|
||||
target="{{ target }}"
|
||||
{%- endif -%}
|
||||
{%- if attr is not empty -%}
|
||||
{%- for name, value in attr %}
|
||||
{{- ' ' ~ name }}={% if '"' in value %}'{{ value|raw }}'{% else %}"{{ value|raw }}"{% endif %}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- if disabled is same as (true) %} disabled="disabled"{% endif %}
|
||||
{%- if id is not empty %} id="{{ id }}"{% endif %}
|
||||
{%- if toggle is not empty %} data-toggle="{{ toggle }}"{% endif %}
|
||||
{%- if modal is not empty %} data-toggle="modal" data-target="{{ modal }}"{% endif %}
|
||||
{%- if onclick is not empty %} onclick="{{ onclick }}"{% endif %}
|
||||
{%- if target is not empty %} target="{{ target }}"{% endif %}
|
||||
{%- if attr is not empty %}
|
||||
{% for name, value in attr %}
|
||||
{{ ' ' ~ name }}={% if '"' in value %}'{{ value|raw }}'{% else %}"{{ value|raw }}"{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>{% if title is not null %}{{ title }}{% else %}{{ macro.icon(icon) }}{% endif %}</a>
|
||||
{% endapply %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -23,13 +23,8 @@
|
||||
{{ 'admin_project.short_stats'|trans(params)|raw }}
|
||||
</p>
|
||||
|
||||
{% if project.budget > 0 %}
|
||||
{{ progress.progressbar(project.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(project.customer.currency) ~ ' / ' ~ project.budget|money(project.customer.currency) ) }}
|
||||
{% endif %}
|
||||
|
||||
{% if project.timeBudget > 0 %}
|
||||
{{ progress.progressbar(project.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ project.timeBudget|duration ) }}
|
||||
{% endif %}
|
||||
{{ progress.progressbar(project.budget, stats.recordRate, 'label.budget'|trans, stats.recordRate|money(project.customer.currency) ~ ' / ' ~ project.budget|money(project.customer.currency) ) }}
|
||||
{{ progress.progressbar(project.timeBudget, stats.recordDuration, 'label.timeBudget'|trans, stats.recordDuration|duration ~ ' / ' ~ project.timeBudget|duration ) }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
|
||||
@@ -33,12 +33,22 @@
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
},
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: 'rgba(0,0,0,.05)',
|
||||
color: '{{ kimai_context.chart.grid_color }}',
|
||||
lineWidth: 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function(tooltipItem, data) {
|
||||
return data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -51,7 +61,7 @@
|
||||
{% block box_body %}
|
||||
|
||||
<div class="chart">
|
||||
<canvas id="userProfileChart{{ year }}" style="height: 200px;"></canvas>
|
||||
<canvas id="userProfileChart{{ year }}" style="height: {{ kimai_context.chart.height }}px;"></canvas>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
@@ -60,11 +70,17 @@
|
||||
datasets: [
|
||||
{
|
||||
label: '{{ year }}',
|
||||
backgroundColor : '#00a65a',
|
||||
borderColor : '#00a65a',
|
||||
backgroundColor: '{{ kimai_context.chart.background_color }}',
|
||||
borderColor: '{{ kimai_context.chart.border_color }}',
|
||||
data: [
|
||||
{% for month in yearStat.months %}
|
||||
{{ (month.totalDuration / 3600)|round }}
|
||||
{{ (month.totalDuration / 3600)|number_format(2, '.', '') }}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
realData : [
|
||||
{% for month in yearStat.months %}
|
||||
'{{ month.totalDuration|duration }}'
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
{% set chartWidget = section.widgets.0 %}
|
||||
{% set colors = chartWidget.color|split(';') %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box">
|
||||
{#
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{{ section.title|trans }}</h3>
|
||||
<h3 class="box-title">{{ title|trans }}</h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i>
|
||||
</button>
|
||||
@@ -27,25 +25,7 @@
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{#
|
||||
<p class="text-center">
|
||||
<strong>
|
||||
{% for yearName, year in chartWidget.data|sort %}
|
||||
{% if loop.first or loop.last %}
|
||||
{% for id, month in year.months %}
|
||||
{% if loop.first %}
|
||||
moment.months({{ loop.index }}) {{ yearName }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if loop.first %} - {% endif %}
|
||||
{% endfor %}
|
||||
</strong>
|
||||
</p>
|
||||
#}
|
||||
<div class="chart">
|
||||
<canvas id="timeChart" style="height: 180px;"></canvas>
|
||||
</div>
|
||||
{{ render_widget(widgets.0) }}
|
||||
</div>
|
||||
{#
|
||||
<div class="col-md-4">
|
||||
@@ -85,17 +65,17 @@
|
||||
#}
|
||||
</div>
|
||||
</div>
|
||||
{% if section.widgets|length > 1 %}
|
||||
{% if widgets|length > 1 %}
|
||||
<div class="box-footer">
|
||||
<div class="row">
|
||||
{% set width = (section.widgets|length) - 1 %}
|
||||
{% set width = (widgets|length) - 1 %}
|
||||
{% set rawWidth = 12 / width %}
|
||||
{% set columnWidth = rawWidth|round(0, 'floor') %}
|
||||
{% for widget in section.widgets|slice(1, width) %}
|
||||
{% for widget in widgets|slice(1, width) %}
|
||||
{% set data = widget.data %}
|
||||
{% if widget.dataType == constant('App\\Model\\Widget::DATA_TYPE_DURATION') %}
|
||||
{% if widget.option('dataType') == 'duration' %}
|
||||
{% set data = widget.data|duration %}
|
||||
{% elseif widget.dataType == constant('App\\Model\\Widget::DATA_TYPE_MONEY') %}
|
||||
{% elseif widget.option('dataType') == 'money' %}
|
||||
{% set data = widget.data|money %}
|
||||
{% endif %}
|
||||
<div class="col-sm-{{ columnWidth }} col-xs-{{ columnWidth * 2 }}">
|
||||
@@ -113,63 +93,5 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
$(function () {
|
||||
'use strict';
|
||||
var timeChartData = {
|
||||
labels: moment.months(),
|
||||
datasets: [
|
||||
{% for yearName, year in chartWidget.data %}
|
||||
{% set yearColors = colors[loop.index-1]|split('|') %}
|
||||
{
|
||||
label : '{{ yearName }}',
|
||||
backgroundColor : '{{ yearColors.1 }}',
|
||||
borderColor : '{{ yearColors.0 }}',
|
||||
pointRadius : 2,
|
||||
borderWidth : 1,
|
||||
pointHitRadius : 10,
|
||||
lineTension : 0.3,
|
||||
data : [
|
||||
{% for month in year.months %}
|
||||
{{ (month.totalDuration / 3600)|round }}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
};
|
||||
|
||||
var timeChartOptions = {
|
||||
maintainAspectRatio : true,
|
||||
responsive : true,
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: 'rgba(0,0,0,.05)',
|
||||
lineWidth: 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
var timeChartCanvas = $('#timeChart').get(0).getContext('2d');
|
||||
|
||||
var timeChart = new Chart(
|
||||
timeChartCanvas, {
|
||||
type: 'line',
|
||||
data: timeChartData,
|
||||
options: timeChartOptions
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
20
templates/widget/section-simple.html.twig
Normal file
20
templates/widget/section-simple.html.twig
Normal file
@@ -0,0 +1,20 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% if not title is empty %}
|
||||
{{ widgets.page_header(title) }}
|
||||
{% endif %}
|
||||
|
||||
{% set width = widgets|length %}
|
||||
{% set rawWidth = 12 / width %}
|
||||
{% set columnWidth = rawWidth|round(0, 'floor') %}
|
||||
<div class="row">
|
||||
{% for widget in widgets %}
|
||||
{% set columnSize = columnWidth %}
|
||||
{% if width == 5 and (loop.first or loop.last) %}
|
||||
{% set columnSize = columnWidth + 1 %}
|
||||
{% endif %}
|
||||
<div class="col-md-{{ columnSize }} col-sm-{{ columnSize * 2 }} col-xs-{{ columnSize * 4 }}">
|
||||
{{ render_widget(widget) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
28
templates/widget/widget-counter.html.twig
Normal file
28
templates/widget/widget-counter.html.twig
Normal file
@@ -0,0 +1,28 @@
|
||||
{% set url = null %}
|
||||
{% if options.route is defined %}
|
||||
{% set url = path(options.route, options.routeOptions|default([])) %}
|
||||
{% endif %}
|
||||
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-{{ options.color|default('green') }}"><i class="{{ options.icon|icon(options.icon) }}"></i></span>
|
||||
|
||||
<div class="info-box-content">
|
||||
{% if not url is empty %}<a href="{{ url }}" class="small-box-footer">{% endif %}
|
||||
<span class="info-box-text">{{ title|trans }}</span>
|
||||
<span class="info-box-number">
|
||||
{% if data is iterable %}
|
||||
{{ 'Invalid data' }}
|
||||
{% else %}
|
||||
{% block widget_data %}
|
||||
{% if options.dataType == 'duration' %}
|
||||
{% set data = data|duration %}
|
||||
{% elseif options.dataType == 'money' %}
|
||||
{% set data = data|money %}
|
||||
{% endif %}
|
||||
{{ data }}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if not url is empty %}</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
95
templates/widget/widget-dailyworkingtimechart.html.twig
Normal file
95
templates/widget/widget-dailyworkingtimechart.html.twig
Normal file
@@ -0,0 +1,95 @@
|
||||
{% set type = options.type|default('bar') %}
|
||||
{% set chart_id = options.id %}
|
||||
{% set backgroundColor = kimai_context.chart.background_color %}
|
||||
{% set borderColor = kimai_context.chart.border_color %}
|
||||
{% set gridColor = kimai_context.chart.grid_color %}
|
||||
{% set colors = options.color|default('')|split(';') %}
|
||||
{% if colors.0 is defined and not colors.0 is empty %}
|
||||
{% set backgroundColor = colors.0 %}
|
||||
{% set borderColor = colors.0 %}
|
||||
{% if colors.1 is defined and not colors.1 is empty %}
|
||||
{% set borderColor = colors.1 %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{{ encore_entry_link_tags('chart') }}
|
||||
{{ encore_entry_script_tags('chart') }}
|
||||
|
||||
{% if not title is empty %}
|
||||
<p class="text-center">
|
||||
<strong>
|
||||
{{ title|trans }}
|
||||
</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="chart">
|
||||
<canvas id="{{ chart_id }}" style="height: {{ kimai_context.chart.height }}px;"></canvas>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
new Chart(
|
||||
document.getElementById('{{ chart_id }}').getContext('2d'), {
|
||||
type: '{{ type }}',
|
||||
data: {
|
||||
labels: [
|
||||
{% for day in data -%}
|
||||
moment('{{ day.day|date_format(constant('\DateTime::ISO8601')) }}').format('ll')
|
||||
{% if not loop.last %},{% endif -%}
|
||||
{%- endfor %}
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: '{{ backgroundColor }}',
|
||||
borderColor: '{{ borderColor }}',
|
||||
data: [
|
||||
{% for day in data -%}
|
||||
{{ (day.totalDuration / 3600)|number_format(2, '.', '') }}
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor %}
|
||||
],
|
||||
realData: [
|
||||
{% for day in data -%}
|
||||
'{{ day.totalDuration|duration }}'
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor %}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: true,
|
||||
responsive: true,
|
||||
legend: false,
|
||||
barPercentage: 0.5,
|
||||
categoryPercentage: 0.9,
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
},
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: '{{ gridColor }}',
|
||||
lineWidth: 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function(tooltipItem, data) {
|
||||
return data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
33
templates/widget/widget-more.html.twig
Normal file
33
templates/widget/widget-more.html.twig
Normal file
@@ -0,0 +1,33 @@
|
||||
{% set url = null %}
|
||||
{% if options.route is defined %}
|
||||
{% set url = path(options.route, options.routeOptions|default([])) %}
|
||||
{% endif %}
|
||||
|
||||
<div class="small-box bg-{{ options.color|default('green') }}">
|
||||
<div class="inner">
|
||||
<h3>
|
||||
{% if data is iterable %}
|
||||
{{ 'Invalid data' }}
|
||||
{% else %}
|
||||
{% block widget_data %}
|
||||
{% if options.dataType == 'duration' %}
|
||||
{% set data = data|duration %}
|
||||
{% elseif options.dataType == 'money' %}
|
||||
{% set data = data|money %}
|
||||
{% endif %}
|
||||
{{ data }}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
<sup style="font-size: 20px">{{ unit|default('') }}</sup>
|
||||
</h3>
|
||||
<p>{{ title|trans }}</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="{{ options.icon|icon(options.icon) }}"></i>
|
||||
</div>
|
||||
{% if not url is empty %}
|
||||
<a href="{{ url }}" class="small-box-footer">
|
||||
{{ 'more.info.link'|trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
98
templates/widget/widget-yearchart.html.twig
Normal file
98
templates/widget/widget-yearchart.html.twig
Normal file
@@ -0,0 +1,98 @@
|
||||
{% set backgroundColor = kimai_context.chart.background_color %}
|
||||
{% set borderColor = kimai_context.chart.border_color %}
|
||||
{% set gridColor = kimai_context.chart.grid_color %}
|
||||
{% set colors = options.color|default('')|split(';') %}
|
||||
{% set type = options.type|default('line') %}
|
||||
{% if type not in ['line', 'bar'] %}
|
||||
{% set type = 'line' %}
|
||||
{% endif %}
|
||||
|
||||
{% if not title is empty %}
|
||||
<p class="text-center">
|
||||
<strong>
|
||||
{{ title|trans }}
|
||||
</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="chart">
|
||||
<canvas id="timeChart" style="height: {{ kimai_context.chart.height }}px;"></canvas>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
new Chart(
|
||||
document.getElementById('timeChart').getContext('2d'), {
|
||||
type: '{{ type }}',
|
||||
data: {
|
||||
labels: moment.months(),
|
||||
datasets: [
|
||||
{% for yearName, year in data %}
|
||||
{% set yearBackgroundColor = backgroundColor %}
|
||||
{% set yearBorderColor = borderColor %}
|
||||
|
||||
{% if colors[loop.index-1] is defined %}
|
||||
{% set yearColors = colors[loop.index-1]|split('|') %}
|
||||
{% if yearColors.0 is defined and not yearColors.0 is empty %}
|
||||
{% set yearBackgroundColor = yearColors.0 %}
|
||||
{% set yearBorderColor = yearColors.0 %}
|
||||
{% if yearColors.1 is defined and not yearColors.1 is empty %}
|
||||
{% set yearBorderColor = yearColors.1 %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{
|
||||
label : '{{ yearName }}',
|
||||
backgroundColor : '{{ yearBackgroundColor }}',
|
||||
borderColor : '{{ yearBorderColor }}',
|
||||
pointRadius : 2,
|
||||
borderWidth : 1,
|
||||
pointHitRadius : 10,
|
||||
lineTension : 0.3,
|
||||
data : [
|
||||
{% for month in year.months %}
|
||||
{{ (month.totalDuration / 3600)|number_format(2, '.', '') }}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
realData : [
|
||||
{% for month in year.months %}
|
||||
'{{ month.totalDuration|duration }}'
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: true,
|
||||
responsive: true,
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
display: true,
|
||||
color: '{{ gridColor }}',
|
||||
lineWidth: 1
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function(tooltipItem, data) {
|
||||
return data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
@@ -97,6 +97,12 @@ class AppExtensionTest extends TestCase
|
||||
'box_color' => 'green',
|
||||
'select_type' => null,
|
||||
'show_about' => true,
|
||||
'chart' => [
|
||||
'background_color' => 'rgba(0,115,183,0.7)',
|
||||
'border_color' => '#3b8bba',
|
||||
'grid_color' => 'rgba(0,0,0,.05)',
|
||||
'height' => '200'
|
||||
]
|
||||
],
|
||||
'kimai.theme.select_type' => null,
|
||||
'kimai.theme.show_about' => true,
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Model\DashboardSection;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
@@ -29,7 +29,7 @@ class DashboardEventTest extends TestCase
|
||||
$this->assertEquals($user, $sut->getUser());
|
||||
$this->assertEquals([], $sut->getSections());
|
||||
|
||||
$section = new DashboardSection('foo');
|
||||
$section = new CompoundRow();
|
||||
$sut->addSection($section);
|
||||
|
||||
$this->assertEquals([$section], $sut->getSections());
|
||||
|
||||
@@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
* @covers \App\Export\Renderer\CsvRenderer
|
||||
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
|
||||
* @covers \App\Export\Renderer\RendererTrait
|
||||
* @group integration
|
||||
*/
|
||||
class CsvRendererTest extends AbstractRendererTest
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ use Twig\Environment;
|
||||
/**
|
||||
* @covers \App\Export\Renderer\HtmlRenderer
|
||||
* @covers \App\Export\Renderer\RendererTrait
|
||||
* @group integration
|
||||
*/
|
||||
class HtmlRendererTest extends AbstractRendererTest
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
* @covers \App\Export\Renderer\OdsRenderer
|
||||
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
|
||||
* @covers \App\Export\Renderer\RendererTrait
|
||||
* @group integration
|
||||
*/
|
||||
class OdsRendererTest extends AbstractRendererTest
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ use Twig\Environment;
|
||||
/**
|
||||
* @covers \App\Export\Renderer\PDFRenderer
|
||||
* @covers \App\Export\Renderer\RendererTrait
|
||||
* @group integration
|
||||
*/
|
||||
class PdfRendererTest extends AbstractRendererTest
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
* @covers \App\Export\Renderer\XlsxRenderer
|
||||
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
|
||||
* @covers \App\Export\Renderer\RendererTrait
|
||||
* @group integration
|
||||
*/
|
||||
class XlsxRendererTest extends AbstractRendererTest
|
||||
{
|
||||
|
||||
@@ -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\Tests\Model;
|
||||
|
||||
use App\Model\DashboardSection;
|
||||
use App\Model\Widget;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Model\DashboardSection
|
||||
*/
|
||||
class DashboardSectionTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = new DashboardSection('test');
|
||||
$this->assertEquals('test', $sut->getTitle());
|
||||
$this->assertEquals(0, $sut->getOrder());
|
||||
$this->assertEquals([], $sut->getWidgets());
|
||||
$this->assertEquals(DashboardSection::TYPE_SIMPLE, $sut->getType());
|
||||
}
|
||||
|
||||
public function testSetter()
|
||||
{
|
||||
$sut = new DashboardSection('hello-world');
|
||||
$sut->setType(DashboardSection::TYPE_CHART);
|
||||
$sut->setOrder(13);
|
||||
$sut->addWidget(new Widget('bar', []));
|
||||
|
||||
$this->assertCount(1, $sut->getWidgets());
|
||||
$this->assertEquals(DashboardSection::TYPE_CHART, $sut->getType());
|
||||
$this->assertEquals(13, $sut->getOrder());
|
||||
$this->assertEquals('bar', $sut->getWidgets()[0]->getTitle());
|
||||
}
|
||||
}
|
||||
42
tests/Model/Statistic/DayTest.php
Normal file
42
tests/Model/Statistic/DayTest.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\Tests\Model\Statistic;
|
||||
|
||||
use App\Model\Statistic\Day;
|
||||
use DateTime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Model\Statistic\Day
|
||||
*/
|
||||
class DayTest extends TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$date = new DateTime('-8 hours');
|
||||
$sut = new Day($date, 12340, 197.25956);
|
||||
|
||||
$this->assertSame($date, $sut->getDay());
|
||||
$this->assertEquals(12340, $sut->getTotalDuration());
|
||||
$this->assertEquals(197.25956, $sut->getTotalRate());
|
||||
}
|
||||
|
||||
public function testAllowedMonths()
|
||||
{
|
||||
$date = new DateTime('-8 hours');
|
||||
$sut = new Day($date, 12340, 197.25956);
|
||||
|
||||
$sut->setTotalDuration(999.27);
|
||||
$sut->setTotalRate(0.123456789);
|
||||
|
||||
$this->assertEquals(999, $sut->getTotalDuration());
|
||||
$this->assertEquals(0.123456789, $sut->getTotalRate());
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
namespace App\Tests\Model\Statistic;
|
||||
|
||||
use App\Model\Statistic\Month;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
@@ -42,12 +44,12 @@ class MonthTest extends TestCase
|
||||
$ex = null;
|
||||
try {
|
||||
new Month($month);
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
$ex = $e;
|
||||
}
|
||||
$this->assertInstanceOf(\InvalidArgumentException::class, $ex);
|
||||
$this->assertInstanceOf(InvalidArgumentException::class, $ex);
|
||||
$this->assertEquals(
|
||||
'Invalid month given, expected 01-12 but given: ' . ((int) $month),
|
||||
'Invalid month given. Expected 1-12, received "' . ((int) $month) . '".',
|
||||
$ex->getMessage()
|
||||
);
|
||||
}
|
||||
@@ -59,7 +61,7 @@ class MonthTest extends TestCase
|
||||
$sut->setTotalDuration(999.27);
|
||||
$sut->setTotalRate(0.123456789);
|
||||
|
||||
$this->assertEquals(999.27, $sut->getTotalDuration());
|
||||
$this->assertEquals(999, $sut->getTotalDuration());
|
||||
$this->assertEquals(0.123456789, $sut->getTotalRate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Model\Widget;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\WidgetRepository;
|
||||
use App\Security\CurrentUser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
@@ -22,8 +22,9 @@ class WidgetRepositoryTest extends TestCase
|
||||
public function testHasWidget()
|
||||
{
|
||||
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$userMock = $this->getMockBuilder(CurrentUser::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$sut = new WidgetRepository($repoMock, ['test' => []]);
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => []]);
|
||||
|
||||
$this->assertFalse($sut->has('foo'));
|
||||
$this->assertTrue($sut->has('test'));
|
||||
@@ -31,14 +32,41 @@ class WidgetRepositoryTest extends TestCase
|
||||
|
||||
/**
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage Cannot find widget: foo
|
||||
* @expectedExceptionMessage Cannot find widget "foo".
|
||||
*/
|
||||
public function testGetWidgetThrowsExceptionOnNonExistingWidget()
|
||||
{
|
||||
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$userMock = $this->getMockBuilder(CurrentUser::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$sut = new WidgetRepository($repoMock, ['test' => []]);
|
||||
$sut->get('foo', null);
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => []]);
|
||||
$sut->get('foo');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \App\Widget\WidgetException
|
||||
* @expectedExceptionMessage Unknown widget type "\App\Widget\Type\FooBar"
|
||||
*/
|
||||
public function testGetWidgetThrowsExceptionOnInvalidType()
|
||||
{
|
||||
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$userMock = $this->getMockBuilder(CurrentUser::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => ['type' => 'FooBar', 'user' => false]]);
|
||||
$sut->get('test');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \App\Widget\WidgetException
|
||||
* @expectedExceptionMessage Invalid widget type "\App\Widget\Type\CompoundChart" does not extend AbstractWidgetType
|
||||
*/
|
||||
public function testGetWidgetTriggersExceptionOnWrongClass()
|
||||
{
|
||||
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$userMock = $this->getMockBuilder(CurrentUser::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => ['type' => 'CompoundChart', 'user' => false]]);
|
||||
$sut->get('test');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,6 +77,8 @@ class WidgetRepositoryTest extends TestCase
|
||||
$repoMock = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$repoMock->method('getStatistic')->willReturn($data);
|
||||
|
||||
$userMock = $this->getMockBuilder(CurrentUser::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$widget = [
|
||||
'color' => 'sunny',
|
||||
'icon' => 'far fa-test',
|
||||
@@ -59,24 +89,25 @@ class WidgetRepositoryTest extends TestCase
|
||||
'title' => 'Test widget',
|
||||
];
|
||||
|
||||
$sut = new WidgetRepository($repoMock, ['test' => $widget]);
|
||||
$widget = $sut->get('test', null);
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => $widget]);
|
||||
$widget = $sut->get('test');
|
||||
|
||||
$options = $widget->getOptions();
|
||||
$this->assertEquals('Test widget', $widget->getTitle());
|
||||
$this->assertEquals($data, $widget->getData());
|
||||
$this->assertEquals('sunny', $widget->getColor());
|
||||
$this->assertEquals('far fa-test', $widget->getIcon());
|
||||
$this->assertEquals($dataType, $widget->getDataType());
|
||||
$this->assertEquals('sunny', $options['color']);
|
||||
$this->assertEquals('far fa-test', $options['icon']);
|
||||
$this->assertEquals($dataType, $options['dataType']);
|
||||
}
|
||||
|
||||
public function getWidgetData()
|
||||
{
|
||||
return [
|
||||
[12, TimesheetRepository::STATS_QUERY_DURATION, Widget::DATA_TYPE_DURATION],
|
||||
[112233, TimesheetRepository::STATS_QUERY_AMOUNT, Widget::DATA_TYPE_INT],
|
||||
[37, TimesheetRepository::STATS_QUERY_ACTIVE, Widget::DATA_TYPE_INT],
|
||||
[375, TimesheetRepository::STATS_QUERY_RATE, Widget::DATA_TYPE_MONEY],
|
||||
[['test' => 'foo'], TimesheetRepository::STATS_QUERY_USER, Widget::DATA_TYPE_INT],
|
||||
[12, TimesheetRepository::STATS_QUERY_DURATION, 'duration'],
|
||||
[112233, TimesheetRepository::STATS_QUERY_AMOUNT, 'int'],
|
||||
[37, TimesheetRepository::STATS_QUERY_ACTIVE, 'int'],
|
||||
[375, TimesheetRepository::STATS_QUERY_RATE, 'money'],
|
||||
[['test' => 'foo'], TimesheetRepository::STATS_QUERY_USER, 'int'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
107
tests/Twig/WidgetExtensionTest.php
Normal file
107
tests/Twig/WidgetExtensionTest.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?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\Tests\Twig;
|
||||
|
||||
use App\Twig\WidgetExtension;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetRendererInterface;
|
||||
use App\Widget\WidgetService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* @covers \App\Twig\WidgetExtension
|
||||
*/
|
||||
class WidgetExtensionTest extends TestCase
|
||||
{
|
||||
protected function getSut($hasWidget = null, $getWidget = null, $renderer = null): WidgetExtension
|
||||
{
|
||||
$service = $this->getMockBuilder(WidgetService::class)->disableOriginalConstructor()->setMethods(['hasWidget', 'getWidget', 'findRenderer'])->getMock();
|
||||
if (null !== $hasWidget) {
|
||||
$service->expects($this->once())->method('hasWidget')->willReturn($hasWidget);
|
||||
}
|
||||
if (null !== $getWidget) {
|
||||
$service->expects($this->once())->method('getWidget')->willReturn($getWidget);
|
||||
}
|
||||
if (null !== $renderer) {
|
||||
$service->expects($this->once())->method('findRenderer')->willReturn($renderer);
|
||||
}
|
||||
|
||||
return new WidgetExtension($service);
|
||||
}
|
||||
|
||||
public function testGetFunctions()
|
||||
{
|
||||
$functions = ['render_widget'];
|
||||
$sut = $this->getSut();
|
||||
$twigFunctions = $sut->getFunctions();
|
||||
$this->assertCount(count($functions), $twigFunctions);
|
||||
$i = 0;
|
||||
/** @var TwigFunction $function */
|
||||
foreach ($twigFunctions as $function) {
|
||||
$this->assertInstanceOf(TwigFunction::class, $function);
|
||||
$this->assertEquals($functions[$i++], $function->getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage Widget must either implement WidgetInterface or be a string
|
||||
*/
|
||||
public function testRenderWidgetForInvalidValue()
|
||||
{
|
||||
$sut = $this->getSut();
|
||||
$sut->renderWidget(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \InvalidArgumentException
|
||||
* @expectedExceptionMessage Unknown widget "test" requested
|
||||
*/
|
||||
public function testRenderWidgetForUnknownWidget()
|
||||
{
|
||||
$sut = $this->getSut(false);
|
||||
$sut->renderWidget('test');
|
||||
}
|
||||
|
||||
public function testRenderWidgetByString()
|
||||
{
|
||||
$widget = new Counter();
|
||||
$sut = $this->getSut(true, $widget, new TestRenderer());
|
||||
$options = ['foo' => 'bar', 'dataType' => 'blub'];
|
||||
$result = $sut->renderWidget('test', $options);
|
||||
$data = json_decode($result, true);
|
||||
$this->assertEquals($options, $data);
|
||||
}
|
||||
|
||||
public function testRenderWidgetObject()
|
||||
{
|
||||
$widget = new Counter();
|
||||
$sut = $this->getSut(null, null, new TestRenderer());
|
||||
$options = ['foo' => 'bar', 'dataType' => 'blub'];
|
||||
$result = $sut->renderWidget($widget, $options);
|
||||
$data = json_decode($result, true);
|
||||
$this->assertEquals($options, $data);
|
||||
}
|
||||
}
|
||||
|
||||
class TestRenderer implements WidgetRendererInterface
|
||||
{
|
||||
public function supports(WidgetInterface $widget): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function render(WidgetInterface $widget, array $options = []): string
|
||||
{
|
||||
return json_encode($widget->getOptions($options));
|
||||
}
|
||||
}
|
||||
54
tests/Widget/Renderer/CompoundChartRendererTest.php
Normal file
54
tests/Widget/Renderer/CompoundChartRendererTest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Widget\Renderer;
|
||||
|
||||
use App\Widget\Renderer\CompoundChartRenderer;
|
||||
use App\Widget\Type\CompoundChart;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use App\Widget\Type\Counter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Renderer\CompoundChartRenderer
|
||||
* @covers \App\Widget\Renderer\AbstractTwigRenderer
|
||||
*/
|
||||
class CompoundChartRendererTest extends TestCase
|
||||
{
|
||||
public function testSupports()
|
||||
{
|
||||
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
|
||||
$sut = new CompoundChartRenderer($twig);
|
||||
self::assertTrue($sut->supports(new CompoundChart()));
|
||||
self::assertFalse($sut->supports(new CompoundRow()));
|
||||
}
|
||||
|
||||
public function testRenderWithCounter()
|
||||
{
|
||||
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->setMethods(['render'])->getMock();
|
||||
$twig->expects($this->once())->method('render')->willReturnCallback(function ($name, $options) {
|
||||
return json_encode([$name, $options]);
|
||||
});
|
||||
|
||||
$sut = new CompoundChartRenderer($twig);
|
||||
$row = new CompoundChart();
|
||||
$row->setTitle('foo-bar');
|
||||
$row->addWidget(new Counter());
|
||||
|
||||
$result = $sut->render($row);
|
||||
$result = json_decode($result, true);
|
||||
self::assertEquals('widget/section-chart.html.twig', $result[0]);
|
||||
self::assertArrayHasKey('title', $result[1]);
|
||||
self::assertEquals('foo-bar', $result[1]['title']);
|
||||
self::assertArrayHasKey('widgets', $result[1]);
|
||||
self::assertIsArray($result[1]['widgets']);
|
||||
self::assertCount(1, $result[1]['widgets']);
|
||||
}
|
||||
}
|
||||
54
tests/Widget/Renderer/CompoundRowRendererTest.php
Normal file
54
tests/Widget/Renderer/CompoundRowRendererTest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Widget\Renderer;
|
||||
|
||||
use App\Widget\Renderer\CompoundRowRenderer;
|
||||
use App\Widget\Type\CompoundChart;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use App\Widget\Type\Counter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Renderer\CompoundRowRenderer
|
||||
* @covers \App\Widget\Renderer\AbstractTwigRenderer
|
||||
*/
|
||||
class CompoundRowRendererTest extends TestCase
|
||||
{
|
||||
public function testSupports()
|
||||
{
|
||||
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
|
||||
$sut = new CompoundRowRenderer($twig);
|
||||
self::assertTrue($sut->supports(new CompoundRow()));
|
||||
self::assertFalse($sut->supports(new CompoundChart()));
|
||||
}
|
||||
|
||||
public function testRenderWithCounter()
|
||||
{
|
||||
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->setMethods(['render'])->getMock();
|
||||
$twig->expects($this->once())->method('render')->willReturnCallback(function ($name, $options) {
|
||||
return json_encode([$name, $options]);
|
||||
});
|
||||
|
||||
$sut = new CompoundRowRenderer($twig);
|
||||
$row = new CompoundRow();
|
||||
$row->setTitle('foo-bar');
|
||||
$row->addWidget(new Counter());
|
||||
|
||||
$result = $sut->render($row);
|
||||
$result = json_decode($result, true);
|
||||
self::assertEquals('widget/section-simple.html.twig', $result[0]);
|
||||
self::assertArrayHasKey('title', $result[1]);
|
||||
self::assertEquals('foo-bar', $result[1]['title']);
|
||||
self::assertArrayHasKey('widgets', $result[1]);
|
||||
self::assertIsArray($result[1]['widgets']);
|
||||
self::assertCount(1, $result[1]['widgets']);
|
||||
}
|
||||
}
|
||||
67
tests/Widget/Renderer/SimpleWidgetRendererTest.php
Normal file
67
tests/Widget/Renderer/SimpleWidgetRendererTest.php
Normal 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\Tests\Widget\Renderer;
|
||||
|
||||
use App\Widget\Renderer\SimpleWidgetRenderer;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\Type\More;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Renderer\SimpleWidgetRenderer
|
||||
* @covers \App\Widget\Renderer\AbstractTwigRenderer
|
||||
*/
|
||||
class SimpleWidgetRendererTest extends TestCase
|
||||
{
|
||||
public function testSupports()
|
||||
{
|
||||
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock();
|
||||
$sut = new SimpleWidgetRenderer($twig);
|
||||
self::assertTrue($sut->supports(new SimpleWidget()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getSimpleWidgetsData
|
||||
*/
|
||||
public function testRenderWithCounter(SimpleWidget $widget, $template, $color)
|
||||
{
|
||||
$twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->setMethods(['render'])->getMock();
|
||||
$twig->expects($this->once())->method('render')->willReturnCallback(function ($name, $options) {
|
||||
return json_encode([$name, $options]);
|
||||
});
|
||||
|
||||
$sut = new SimpleWidgetRenderer($twig);
|
||||
|
||||
$data = uniqid(get_class($widget));
|
||||
$widget->setData($data);
|
||||
$result = $sut->render($widget, ['color' => $color]);
|
||||
$result = json_decode($result, true);
|
||||
|
||||
self::assertEquals($template, $result[0]);
|
||||
self::assertArrayHasKey('data', $result[1]);
|
||||
self::assertEquals($data, $result[1]['data']);
|
||||
self::assertArrayHasKey('title', $result[1]);
|
||||
self::assertArrayHasKey('options', $result[1]);
|
||||
self::assertIsArray($result[1]['options']);
|
||||
self::assertArrayHasKey('color', $result[1]['options']);
|
||||
self::assertEquals($color, $result[1]['options']['color']);
|
||||
}
|
||||
|
||||
public function getSimpleWidgetsData()
|
||||
{
|
||||
return [
|
||||
[new SimpleWidget(), 'widget/widget-simplewidget.html.twig', 'yellow'],
|
||||
[new Counter(), 'widget/widget-counter.html.twig', 'asdfgh'],
|
||||
[new More(), 'widget/widget-more.html.twig', '#123456'],
|
||||
];
|
||||
}
|
||||
}
|
||||
67
tests/Widget/Type/AbstractContainerTest.php
Normal file
67
tests/Widget/Type/AbstractContainerTest.php
Normal 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\Tests\Widget\Type;
|
||||
|
||||
use App\Widget\Type\AbstractContainer;
|
||||
use App\Widget\Type\More;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\AbstractContainer
|
||||
*/
|
||||
abstract class AbstractContainerTest extends TestCase
|
||||
{
|
||||
abstract public function createSut(): AbstractContainer;
|
||||
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
self::assertInstanceOf(AbstractContainer::class, $sut);
|
||||
|
||||
self::assertEquals('', $sut->getId());
|
||||
self::assertEquals('', $sut->getTitle());
|
||||
|
||||
self::assertEquals(0, $sut->getOrder());
|
||||
self::assertEquals([], $sut->getOptions());
|
||||
self::assertEquals([], $sut->getWidgets());
|
||||
self::assertEquals([], $sut->getData());
|
||||
}
|
||||
|
||||
public function testSetter()
|
||||
{
|
||||
$widget = (new More())->setTitle('bar')->setId('foo');
|
||||
|
||||
$sut = $this->createSut();
|
||||
$sut->setTitle('hello-world');
|
||||
$sut->setOrder(13);
|
||||
$sut->addWidget($widget);
|
||||
|
||||
self::assertEquals('hello-world', $sut->getTitle());
|
||||
self::assertEquals('hello-world', $sut->getId());
|
||||
|
||||
self::assertCount(1, $sut->getWidgets());
|
||||
self::assertCount(1, $sut->getData());
|
||||
self::assertEquals([$widget], $sut->getWidgets());
|
||||
self::assertEquals([$widget], $sut->getData());
|
||||
|
||||
self::assertEquals(13, $sut->getOrder());
|
||||
self::assertEquals('bar', $sut->getWidgets()[0]->getTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \BadMethodCallException
|
||||
*/
|
||||
public function testSetOptionNotImplemented()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$sut->setOption('dfsdf', []);
|
||||
}
|
||||
}
|
||||
69
tests/Widget/Type/AbstractWidgetTypeTest.php
Normal file
69
tests/Widget/Type/AbstractWidgetTypeTest.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\AbstractWidgetType
|
||||
*/
|
||||
abstract class AbstractWidgetTypeTest extends TestCase
|
||||
{
|
||||
abstract public function createSut(): AbstractWidgetType;
|
||||
|
||||
abstract public function getDefaultOptions(): array;
|
||||
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut);
|
||||
self::assertEquals('', $sut->getId());
|
||||
self::assertEquals('', $sut->getTitle());
|
||||
self::assertEquals($this->getDefaultOptions(), $sut->getOptions());
|
||||
self::assertNull($sut->getData());
|
||||
self::assertEquals('bar', $sut->getOption('foo', 'bar'));
|
||||
}
|
||||
|
||||
public function testFluentInterface()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setOptions([]));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setId(''));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setTitle(''));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setData(''));
|
||||
}
|
||||
|
||||
public function testSetter()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
// options
|
||||
$sut->setOption('föööö', 'trääääää');
|
||||
self::assertEquals('trääääää', $sut->getOption('föööö', 'tröööö'));
|
||||
self::assertEquals('trääääää', $sut->getOption('föööö', 'tröööö'));
|
||||
self::assertEquals(array_merge($this->getDefaultOptions(), ['föööö' => 'trääääää']), $sut->getOptions());
|
||||
|
||||
$sut->setOptions(['blub' => 'blab', 'dataType' => 'money']);
|
||||
self::assertEquals(['blub' => 'blab', 'dataType' => 'money', 'föööö' => 'trääääää'], $sut->getOptions());
|
||||
|
||||
// id
|
||||
$sut->setId('cvbnmyx');
|
||||
self::assertEquals('cvbnmyx', $sut->getId());
|
||||
|
||||
// data
|
||||
$sut->setData('slkudfhalksjdhfkljsahdf');
|
||||
self::assertEquals('slkudfhalksjdhfkljsahdf', $sut->getData());
|
||||
|
||||
$data = new \stdClass();
|
||||
$sut->setData($data);
|
||||
self::assertSame($data, $sut->getData());
|
||||
}
|
||||
}
|
||||
25
tests/Widget/Type/CompoundChartTest.php
Normal file
25
tests/Widget/Type/CompoundChartTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Widget\Type\AbstractContainer;
|
||||
use App\Widget\Type\CompoundChart;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\CompoundChart
|
||||
* @covers \App\Widget\Type\AbstractContainer
|
||||
*/
|
||||
class CompoundChartTest extends AbstractContainerTest
|
||||
{
|
||||
public function createSut(): AbstractContainer
|
||||
{
|
||||
return new CompoundChart();
|
||||
}
|
||||
}
|
||||
25
tests/Widget/Type/CompoundRowTest.php
Normal file
25
tests/Widget/Type/CompoundRowTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Widget\Type\AbstractContainer;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\CompoundRow
|
||||
* @covers \App\Widget\Type\AbstractContainer
|
||||
*/
|
||||
class CompoundRowTest extends AbstractContainerTest
|
||||
{
|
||||
public function createSut(): AbstractContainer
|
||||
{
|
||||
return new CompoundRow();
|
||||
}
|
||||
}
|
||||
36
tests/Widget/Type/CounterTest.php
Normal file
36
tests/Widget/Type/CounterTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\Counter
|
||||
*/
|
||||
class CounterTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return new Counter();
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return ['dataType' => 'int'];
|
||||
}
|
||||
|
||||
public function testExtendsSimpleWidget()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleWidget::class, $sut);
|
||||
}
|
||||
}
|
||||
117
tests/Widget/Type/DailyWorkingTimeChartTest.php
Normal file
117
tests/Widget/Type/DailyWorkingTimeChartTest.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Day;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Security\CurrentUser;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\DailyWorkingTimeChart;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\DailyWorkingTimeChart
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
* @covers \App\Widget\Type\AbstractWidgetType
|
||||
* @covers \App\Repository\TimesheetRepository
|
||||
*/
|
||||
class DailyWorkingTimeChartTest extends TestCase
|
||||
{
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
$repository = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
$user = $this->getMockBuilder(CurrentUser::class)->disableOriginalConstructor()->setMethods(['getUser'])->getMock();
|
||||
$user->expects($this->once())->method('getUser')->willReturn(new User());
|
||||
|
||||
return new DailyWorkingTimeChart($repository, $user);
|
||||
}
|
||||
|
||||
public function testExtendsSimpleWidget()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleWidget::class, $sut);
|
||||
}
|
||||
|
||||
public function testDefaultValues()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut);
|
||||
self::assertEquals('DailyWorkingTimeChart', $sut->getId());
|
||||
self::assertEquals('stats.yourWorkingHours', $sut->getTitle());
|
||||
self::assertEquals('monday this week 00:00:00', $sut->getOption('begin', 'xxx'));
|
||||
self::assertEquals('sunday this week 23:59:59', $sut->getOption('end', 'xxx'));
|
||||
self::assertEquals('', $sut->getOption('color', 'xxx'));
|
||||
self::assertInstanceOf(User::class, $sut->getOption('user', 'xxx'));
|
||||
self::assertEquals('bar', $sut->getOption('type', 'xxx'));
|
||||
}
|
||||
|
||||
public function testFluentInterface()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setOptions([]));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setId(''));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setTitle(''));
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setData(''));
|
||||
}
|
||||
|
||||
public function testSetter()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
// options
|
||||
$sut->setOption('föööö', 'trääääää');
|
||||
self::assertEquals('trääääää', $sut->getOption('föööö', 'tröööö'));
|
||||
|
||||
// check default values
|
||||
self::assertEquals('xxxxx', $sut->getOption('blub', 'xxxxx'));
|
||||
self::assertEquals('xxxxx', $sut->getOption('dataType', 'xxxxx'));
|
||||
|
||||
$sut->setOptions(['blub' => 'blab', 'dataType' => 'money']);
|
||||
// check option still exists
|
||||
self::assertEquals('trääääää', $sut->getOption('föööö', 'tröööö'));
|
||||
// check options are now existing
|
||||
self::assertEquals('blab', $sut->getOption('blub', 'xxxxx'));
|
||||
self::assertEquals('money', $sut->getOption('dataType', 'xxxxx'));
|
||||
|
||||
// id
|
||||
$sut->setId('cvbnmyx');
|
||||
self::assertEquals('cvbnmyx', $sut->getId());
|
||||
}
|
||||
|
||||
public function testGetOptions()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
$options = $sut->getOptions(['type' => 'xxx']);
|
||||
self::assertStringStartsWith('DailyWorkingTimeChart_', $options['id']);
|
||||
self::assertEquals('bar', $options['type']);
|
||||
}
|
||||
|
||||
public function testGetData()
|
||||
{
|
||||
$repository = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->setMethods(['getDailyData'])->getMock();
|
||||
$repository->expects($this->once())->method('getDailyData')->willReturnCallback(function ($user, $begin, $end) {
|
||||
return [
|
||||
['year' => '2019', 'month' => '1', 'day' => 1, 'rate' => 13.75, 'duration' => 1234]
|
||||
];
|
||||
});
|
||||
$user = $this->getMockBuilder(CurrentUser::class)->disableOriginalConstructor()->setMethods(['getUser'])->getMock();
|
||||
$user->expects($this->once())->method('getUser')->willReturn((new User())->setUsername('tralalala'));
|
||||
|
||||
$sut = new DailyWorkingTimeChart($repository, $user);
|
||||
$data = $sut->getData([]);
|
||||
self::assertCount(7, $data);
|
||||
foreach ($data as $statObj) {
|
||||
self::assertInstanceOf(Day::class, $statObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
tests/Widget/Type/MoreTest.php
Normal file
36
tests/Widget/Type/MoreTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Tests\Widget\Type;
|
||||
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\More;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\More
|
||||
*/
|
||||
class MoreTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return new More();
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return ['dataType' => 'int'];
|
||||
}
|
||||
|
||||
public function testExtendsSimpleWidget()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(SimpleWidget::class, $sut);
|
||||
}
|
||||
}
|
||||
29
tests/Widget/Type/SimpleWidgetTest.php
Normal file
29
tests/Widget/Type/SimpleWidgetTest.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\Tests\Widget\Type;
|
||||
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
*/
|
||||
class SimpleWidgetTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return new SimpleWidget();
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
29
tests/Widget/Type/YearChartTest.php
Normal file
29
tests/Widget/Type/YearChartTest.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\Tests\Widget\Type;
|
||||
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\YearChart;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\YearChart
|
||||
*/
|
||||
class YearChartTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return new YearChart();
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
25
tests/Widget/WidgetExceptionTest.php
Normal file
25
tests/Widget/WidgetExceptionTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Tests\Widget;
|
||||
|
||||
use App\Widget\WidgetException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\WidgetException
|
||||
*/
|
||||
class WidgetExceptionTest extends TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$ex = new WidgetException();
|
||||
self::assertInstanceOf(\Exception::class, $ex);
|
||||
}
|
||||
}
|
||||
77
tests/Widget/WidgetServiceTest.php
Normal file
77
tests/Widget/WidgetServiceTest.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Widget;
|
||||
|
||||
use App\Repository\WidgetRepository;
|
||||
use App\Widget\Renderer\SimpleWidgetRenderer;
|
||||
use App\Widget\Type\More;
|
||||
use App\Widget\WidgetService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\WidgetService
|
||||
*/
|
||||
class WidgetServiceTest extends TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$sut = new WidgetService($repository, []);
|
||||
self::assertFalse($sut->hasWidget('sdfsdf'));
|
||||
self::assertCount(0, $sut->getRenderer());
|
||||
|
||||
$sut = new WidgetService($repository, [
|
||||
new SimpleWidgetRenderer(new Environment(new FilesystemLoader()))
|
||||
]);
|
||||
self::assertCount(1, $sut->getRenderer());
|
||||
}
|
||||
|
||||
public function testFindRenderer()
|
||||
{
|
||||
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$renderer = new SimpleWidgetRenderer(new Environment(new FilesystemLoader()));
|
||||
$sut = new WidgetService($repository, [$renderer]);
|
||||
$sut->addRenderer(new SimpleWidgetRenderer(new Environment(new FilesystemLoader())));
|
||||
|
||||
self::assertCount(2, $sut->getRenderer());
|
||||
|
||||
$found = $sut->findRenderer(new More());
|
||||
self::assertSame($renderer, $found);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \App\Widget\WidgetException
|
||||
* @expectedExceptionMessage No renderer available for widget "App\Widget\Type\More"
|
||||
*/
|
||||
public function testFindRendererThrowsException()
|
||||
{
|
||||
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$sut = new WidgetService($repository, []);
|
||||
$sut->findRenderer(new More());
|
||||
}
|
||||
|
||||
public function testHasAndGetWidget()
|
||||
{
|
||||
$widget = new More();
|
||||
|
||||
$repository = $this->getMockBuilder(WidgetRepository::class)->disableOriginalConstructor()->setMethods(['has', 'get'])->getMock();
|
||||
$repository->expects($this->once())->method('has')->willReturn(true);
|
||||
$repository->expects($this->once())->method('get')->willReturn($widget);
|
||||
|
||||
$sut = new WidgetService($repository, []);
|
||||
self::assertTrue($sut->hasWidget('sdfsdf'));
|
||||
self::assertSame($widget, $sut->getWidget('sdfsdf'));
|
||||
}
|
||||
}
|
||||
@@ -257,10 +257,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>جميع المستخدمين</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>مدير</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -308,10 +308,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>Alle Benutzer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>Administrator</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -308,10 +308,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>All user</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>Administrator</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -305,10 +305,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>Resumen de todos los usuarios</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>Administrador</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -308,10 +308,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>Tous les usagers</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>Administrateur</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -277,10 +277,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>Minden felhasználó</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>Adminisztrátor</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -257,10 +257,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>Tutti gli utenti</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>Amministratore</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -297,10 +297,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>全ユーザーの統計情報</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>管理情報</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -257,10 +257,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>Todos usuários</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>Administrador</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -257,10 +257,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target>Все пользователи</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target>Администратор</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
@@ -290,10 +290,6 @@
|
||||
<source>dashboard.all</source>
|
||||
<target state="translated">Alla användare</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="dashboard.admin">
|
||||
<source>dashboard.admin</source>
|
||||
<target state="translated">Administratör</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Widgets
|
||||
|
||||
Reference in New Issue
Block a user