dashboard widgets from event (#246)

This commit is contained in:
Kevin Papst
2018-07-28 23:18:28 +02:00
committed by GitHub
parent e6f8bc8ae2
commit 0996eca3c5
10 changed files with 384 additions and 129 deletions

View File

@@ -9,16 +9,12 @@
namespace App\Controller;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Repository\Query\TimesheetQuery;
use App\Event\DashboardEvent;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Dashboard controller for the admin area.
@@ -28,120 +24,34 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
*/
class DashboardController extends Controller
{
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(EventDispatcherInterface $dispatcher)
{
$this->eventDispatcher = $dispatcher;
}
/**
* @Route("/", defaults={}, name="dashboard")
* @Method("GET")
*/
public function indexAction()
{
$user = $this->getUser();
$event = new DashboardEvent($this->getUser());
$userStats = $this->getDoctrine()->getRepository(User::class)->getGlobalStatistics();
// FIXME move the other widgets to the Kimai, the inheritence is wrong as Kimai
// shouldn't know about Timesheets
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
$timesheetUserStats = $timesheetRepo->getUserStatistics($user);
$timesheetGlobalStats = $timesheetRepo->getGlobalStatistics();
$activityStats = $this->getDoctrine()->getRepository(Activity::class)->getGlobalStatistics();
$projectStats = $this->getDoctrine()->getRepository(Project::class)->getGlobalStatistics();
$customerStats = $this->getDoctrine()->getRepository(Customer::class)->getGlobalStatistics();
$this->eventDispatcher->dispatch(
DashboardEvent::DASHBOARD,
$event
);
return $this->render('dashboard/index.html.twig', [
'dashboard_widgets' => $this->getWidgets(),
'timesheetGlobal' => $timesheetGlobalStats,
'timesheetUser' => $timesheetUserStats,
'activity' => $activityStats,
'project' => $projectStats,
'customer' => $customerStats,
'user' => $userStats,
'widget_rows' => $event->getWidgetRows()
]);
}
/**
* colors: blue / yellow / purple / green / black
* icons: bar-chart / line-chart / calendar / clock
*
* @return array
*/
protected function getWidgets()
{
// @codingStandardsIgnoreStart
$widgets = [
/*
[
'header' => 'dashboard.you',
'widgets' => [
"{{ widgets.info_box_progress('Bewilligte Stunden', 'Stunden zur Abrechnung bewilligt', 120, 10, 'star') }}",
"{{ widgets.info_box_progress('Umsatz / Monat', '70% Increase in 30 Days', 6830, 30, 'credit-card', 'black') }}",
"{{ widgets.info_box_progress('Stunden persönlich', 'Das ist noch nicht genug', 135, 60, 'hourglass') }}",
"{{ widgets.info_box_progress('Anzahl Benutzer', 'Mehr ist besser!', 5, 90, 'user') }}",
],
],
*/
[
'id' => 'profile.stats',
'header' => 'dashboard.you',
'widgets' => [
"{{ widgets.info_box_counter('stats.durationThisMonth', timesheetUser.durationThisMonth|duration(true), 'far fa-hourglass', 'green') }}",
//"{{ widgets.info_box_counter('stats.amountThisMonth', timesheetUser.amountThisMonth|money, 'money', 'blue') }}",
"{{ widgets.info_box_counter('stats.durationTotal', timesheetUser.durationTotal|duration(true), 'far fa-hourglass', 'red') }}",
//"{{ widgets.info_box_counter('stats.amountTotal', timesheetUser.amountTotal|money, 'money', 'yellow') }}",
],
],
];
if (!$this->isGranted('ROLE_TEAMLEAD', null)) {
return $widgets;
}
$widgets[] = [
'id' => 'alluser.stats',
'header' => 'dashboard.all',
'widgets' => [
"{{ widgets.info_box_counter('stats.durationThisMonth', timesheetGlobal.durationThisMonth|duration(true), 'far fa-hourglass', 'blue') }}",
"{{ widgets.info_box_counter('stats.durationTotal', timesheetGlobal.durationTotal|duration(true), 'far fa-hourglass', 'yellow') }}",
"{{ widgets.info_box_counter('stats.activeRecordings', timesheetGlobal.activeCurrently, 'far fa-hourglass', 'red', path('admin_timesheet', {'state': " . TimesheetQuery::STATE_RUNNING . '})) }}',
],
];
$widgets[] = [
'id' => 'user.stats',
'header' => '',
'widgets' => [
"{{ widgets.info_box_counter('stats.userTotal', user.totalAmount, 'user', 'red') }}",
"{{ widgets.info_box_counter('stats.userActiveThisMoth', timesheetGlobal.activeThisMonth, 'user', 'yellow') }}",
"{{ widgets.info_box_counter('stats.userActiveEver', timesheetGlobal.activeTotal, 'user', 'blue') }}",
],
];
if (!$this->isGranted('ROLE_ADMIN', null)) {
return $widgets;
}
$widgets[] = [
'id' => 'alluser.money_stats',
'header' => '',
'widgets' => [
"{{ widgets.info_box_counter('stats.amountThisMonth', timesheetGlobal.amountThisMonth|money, 'far fa-money-bill-alt', 'green') }}",
"{{ widgets.info_box_counter('stats.amountTotal', timesheetGlobal.amountTotal|money, 'far fa-money-bill-alt', 'red') }}",
],
];
$widgets[] = [
'id' => 'admin.stats',
'header' => 'dashboard.admin',
'widgets' => [
"{{ widgets.info_box_more('stats.userTotal', user.totalAmount, ' ', path('admin_user'), 'user') }}",
"{{ widgets.info_box_more('stats.customerTotal', customer.count, '', path('admin_customer'), 'customer', 'blue') }}",
"{{ widgets.info_box_more('stats.projectsTotal', project.count, '', path('admin_project'), 'project', 'yellow') }}",
"{{ widgets.info_box_more('stats.activitiesTotal', activity.count, '', path('admin_activity'), 'activity', 'purple') }}",
],
];
// @codingStandardsIgnoreEnd
return $widgets;
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Event;
use App\Entity\User;
use App\Model\TimesheetGlobalStatistic;
use App\Model\WidgetRow;
use Symfony\Component\EventDispatcher\Event;
class DashboardEvent extends Event
{
public const DASHBOARD = 'app.dashboard';
/**
* @var User
*/
protected $user;
/**
* @var WidgetRow[]
*/
protected $widgetRows = [];
/**
* @param User $user
* @param TimesheetGlobalStatistic $timesheetStatistic
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
public function addWidgetRow(WidgetRow $row)
{
$this->widgetRows[] = $row;
}
/**
* @return WidgetRow[]
*/
public function getWidgetRows()
{
return $this->widgetRows;
}
}

View File

@@ -0,0 +1,169 @@
<?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\EventSubscriber;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Event\DashboardEvent;
use App\Model\TimesheetGlobalStatistic;
use App\Model\TimesheetStatistic;
use App\Model\UserStatistic;
use App\Model\WidgetRow;
use App\Repository\Query\TimesheetQuery;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* Used to add Dashboard widgets for a user.
*/
class DashboardSubscriber implements EventSubscriberInterface
{
/**
* @var AuthorizationCheckerInterface
*/
protected $security;
/**
* @var ManagerRegistry
*/
protected $registry;
/**
* MenuSubscriber constructor.
* @param AuthorizationCheckerInterface $security
*/
public function __construct(AuthorizationCheckerInterface $security, ManagerRegistry $registry)
{
$this->security = $security;
$this->registry = $registry;
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
DashboardEvent::DASHBOARD => ['onDashboardEvent', 100],
];
}
/**
* @param DashboardEvent $event
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function onDashboardEvent(DashboardEvent $event)
{
$timesheetRepo = $this->registry->getRepository(Timesheet::class);
$timesheetGlobal = $timesheetRepo->getGlobalStatistics();
$timesheetUser = $timesheetRepo->getUserStatistics($event->getUser());
$userStats = $this->registry->getRepository(User::class)->getGlobalStatistics();
$this->addUserWidgets($event, $timesheetUser);
if (!$this->security->isGranted('ROLE_TEAMLEAD')) {
return;
}
$this->addTeamleadWidgets($event, $timesheetGlobal, $userStats);
if (!$this->security->isGranted('ROLE_ADMIN')) {
return;
}
$this->addAdminWidgets($event, $timesheetGlobal, $userStats);
}
/**
* @param DashboardEvent $event
* @param TimesheetStatistic $timesheet
*/
protected function addUserWidgets(DashboardEvent $event, TimesheetStatistic $timesheet)
{
/*
$row = new WidgetRow('dashboard.you');
$widgets = [
[
'widgets' => [
"{{ widgets.info_box_progress('Bewilligte Stunden', 'Stunden zur Abrechnung bewilligt', 120, 10, 'star') }}",
"{{ widgets.info_box_progress('Umsatz / Monat', '70% Increase in 30 Days', 6830, 30, 'credit-card', 'black') }}",
"{{ widgets.info_box_progress('Stunden persönlich', 'Das ist noch nicht genug', 135, 60, 'hourglass') }}",
"{{ widgets.info_box_progress('Anzahl Benutzer', 'Mehr ist besser!', 5, 90, 'user') }}",
],
],
$event->addWidgetRow($row);
*/
$row = new WidgetRow('profile.stats', 'dashboard.you');
$row
->add("{{ widgets.info_box_counter('stats.durationThisMonth', " . $timesheet->getDurationThisMonth() . "|duration(true), 'far fa-hourglass', 'green') }}")
//->add("{{ widgets.info_box_counter('stats.amountThisMonth', ".$timesheet->getAmountThisMonth()."|money, 'money', 'blue') }}")
->add("{{ widgets.info_box_counter('stats.durationTotal', " . $timesheet->getDurationTotal() . "|duration(true), 'far fa-hourglass', 'red') }}")
//->add("{{ widgets.info_box_counter('stats.amountTotal', ".$timesheet->getAmountTotal()."|money, 'money', 'yellow') }}")
;
$event->addWidgetRow($row);
}
/**
* @param DashboardEvent $event
* @param TimesheetGlobalStatistic $timesheet
* @param UserStatistic $userStats
*/
protected function addTeamleadWidgets(DashboardEvent $event, TimesheetGlobalStatistic $timesheet, UserStatistic $userStats)
{
$row = new WidgetRow('alluser.stats', 'dashboard.all');
$row
->add("{{ widgets.info_box_counter('stats.durationThisMonth', " . $timesheet->getDurationThisMonth() . "|duration(true), 'far fa-hourglass', 'blue') }}")
->add("{{ widgets.info_box_counter('stats.durationTotal', " . $timesheet->getDurationTotal() . "|duration(true), 'far fa-hourglass', 'yellow') }}")
->add("{{ widgets.info_box_counter('stats.activeRecordings', " . $timesheet->getActiveCurrently() . ", 'far fa-hourglass', 'red', path('admin_timesheet', {'state': " . TimesheetQuery::STATE_RUNNING . '})) }}')
;
$event->addWidgetRow($row);
$row = new WidgetRow('user.stats');
$row
->add("{{ widgets.info_box_counter('stats.userTotal', " . $userStats->getTotalAmount() . ", 'user', 'red') }}")
->add("{{ widgets.info_box_counter('stats.userActiveThisMoth', " . $timesheet->getActiveThisMonth() . ", 'user', 'yellow') }}")
->add("{{ widgets.info_box_counter('stats.userActiveEver', " . $timesheet->getActiveTotal() . ", 'user', 'blue') }}")
;
$event->addWidgetRow($row);
}
/**
* @param DashboardEvent $event
* @param TimesheetGlobalStatistic $timesheet
* @param UserStatistic $user
* @throws \Doctrine\ORM\NonUniqueResultException
*/
protected function addAdminWidgets(DashboardEvent $event, TimesheetGlobalStatistic $timesheet, UserStatistic $user)
{
$row = new WidgetRow('alluser.money_stats');
$row
->add("{{ widgets.info_box_counter('stats.amountThisMonth', " . $timesheet->getAmountThisMonth() . "|money, 'far fa-money-bill-alt', 'green') }}")
->add("{{ widgets.info_box_counter('stats.amountTotal', " . $timesheet->getAmountTotal() . "|money, 'far fa-money-bill-alt', 'red') }}")
;
$event->addWidgetRow($row);
$activity = $this->registry->getRepository(Activity::class)->getGlobalStatistics();
$project = $this->registry->getRepository(Project::class)->getGlobalStatistics();
$customer = $this->registry->getRepository(Customer::class)->getGlobalStatistics();
$row = new WidgetRow('admin.stats', 'dashboard.admin');
$row
->add("{{ widgets.info_box_more('stats.userTotal', " . $user->getTotalAmount() . ", ' ', path('admin_user'), 'user') }}")
->add("{{ widgets.info_box_more('stats.customerTotal', " . $customer->getCount() . ", '', path('admin_customer'), 'customer', 'blue') }}")
->add("{{ widgets.info_box_more('stats.projectsTotal', " . $project->getCount() . ", '', path('admin_project'), 'project', 'yellow') }}")
->add("{{ widgets.info_box_more('stats.activitiesTotal', " . $activity->getCount() . ", '', path('admin_activity'), 'activity', 'purple') }}")
;
$event->addWidgetRow($row);
}
}

View File

@@ -40,7 +40,7 @@ class TimesheetGlobalStatistic extends TimesheetStatistic
*/
public function setActiveCurrently($activeCurrently)
{
$this->activeCurrently = $activeCurrently;
$this->activeCurrently = (int) $activeCurrently;
}
/**
@@ -56,7 +56,7 @@ class TimesheetGlobalStatistic extends TimesheetStatistic
*/
public function setActiveThisMonth($activeThisMonth)
{
$this->activeThisMonth = $activeThisMonth;
$this->activeThisMonth = (int) $activeThisMonth;
}
/**
@@ -72,6 +72,6 @@ class TimesheetGlobalStatistic extends TimesheetStatistic
*/
public function setActiveTotal($activeTotal)
{
$this->activeTotal = $activeTotal;
$this->activeTotal = (int) $activeTotal;
}
}

View File

@@ -50,7 +50,7 @@ class TimesheetStatistic
*/
public function setDurationThisMonth($durationThisMonth)
{
$this->durationThisMonth = $durationThisMonth;
$this->durationThisMonth = (int) $durationThisMonth;
}
/**
@@ -66,7 +66,7 @@ class TimesheetStatistic
*/
public function setAmountTotal($amountTotal)
{
$this->amountTotal = $amountTotal;
$this->amountTotal = (int) $amountTotal;
}
/**
@@ -82,7 +82,7 @@ class TimesheetStatistic
*/
public function setDurationTotal($durationTotal)
{
$this->durationTotal = $durationTotal;
$this->durationTotal = (int) $durationTotal;
}
/**
@@ -98,7 +98,7 @@ class TimesheetStatistic
*/
public function setAmountThisMonth($amountThisMonth)
{
$this->amountThisMonth = $amountThisMonth;
$this->amountThisMonth = (int) $amountThisMonth;
}
/**

View File

@@ -32,6 +32,6 @@ class UserStatistic
*/
public function setTotalAmount($totalAmount)
{
$this->totalAmount = $totalAmount;
$this->totalAmount = (int) $totalAmount;
}
}

71
src/Model/WidgetRow.php Normal file
View File

@@ -0,0 +1,71 @@
<?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 WidgetRow
{
/**
* @var string
*/
protected $id;
/**
* @var string
*/
protected $title;
/**
* @var string[]
*/
protected $widgets = [];
/**
* @param string $id
* @param string $title
*/
public function __construct(string $id, string $title = '')
{
$this->id = $id;
$this->title = $title;
}
/**
* @return string
*/
public function getId(): string
{
return $this->id;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @return string[]
*/
public function getWidgets(): array
{
return $this->widgets;
}
/**
* @param string $templateString
* @return $this
*/
public function add(string $templateString)
{
$this->widgets[] = $templateString;
return $this;
}
}

View File

@@ -8,19 +8,24 @@
{% block main %}
{% for settings in dashboard_widgets %}
{% if settings.header %}
{{ widgets.page_header(settings.header) }}
{% for row in widget_rows %}
{% if row.title %}
{{ widgets.page_header(row.title) }}
{% endif %}
{% set width = settings.widgets|length %}
{% set width = row.widgets|length %}
{% set rawWidth = 12 / width %}
{% set columnWidth = rawWidth|round(0, 'floor') %}
<div class="row">
{% for widgetTemplate in settings.widgets %}
{% for widgetTemplate in row.widgets %}
<div class="col-md-{{ columnWidth }} col-sm-{{ columnWidth * 2 }} col-xs-{{ columnWidth * 4 }}">
{% set widgetString = '{% import "macros/widgets.html.twig" as widgets %}' ~ widgetTemplate %}
{{ include(template_from_string(widgetString)) }}
{{
include(
template_from_string(
'{% import "macros/widgets.html.twig" as widgets %}' ~ widgetTemplate
)
)
}}
</div>
{% endfor %}
</div>

View File

@@ -9,6 +9,8 @@
namespace App\Tests\Controller;
use App\Entity\User;
/**
* @coversDefaultClass \App\Controller\DashboardController
* @group integration
@@ -27,4 +29,12 @@ class DashboardControllerTest extends ControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertMainContentClass($client, 'dashboard');
}
public function testIndexActionForAdmin()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/dashboard/');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertMainContentClass($client, 'dashboard');
}
}

View File

@@ -100,8 +100,8 @@ The files in `translations/` as a quick overview:
If you want to add your own entries in the navigation bar, you can subscribe to these events:
- `App\EventConfigureMainMenuEvent::CONFIGURE`
- `App\ConfigureAdminMenuEvent::CONFIGURE`
- `App\Event\ConfigureMainMenuEvent::CONFIGURE`
- `App\Event\ConfigureAdminMenuEvent::CONFIGURE`
And that's how to use it:
@@ -111,7 +111,7 @@ use App\Event\ConfigureAdminMenuEvent;
use Avanzu\AdminThemeBundle\Model\MenuItemModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class MySubscriber implements EventSubscriberInterface
class MyMenuSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
@@ -138,6 +138,37 @@ class MySubscriber implements EventSubscriberInterface
```
For more details check the [official menu subscriber](../../src/EventSubscriber/MenuSubscriber.php).
## Extending the dashboard with widgets
If you want to add your own widget rows to the dashboard, you can subscribe to the event:
- `App\Event\DashboardEvent::DASHBOARD`
And that's how to use it:
```php
use App\Event\DashboardEvent;
use App\Model\WidgetRow;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class MyDashboardSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [DashboardEvent::DASHBOARD => ['onDashboardEvent', 200]];
}
public function onDashboardEvent(DashboardEvent $event)
{
$row = new WidgetRow('my_id', 'optional.row.title');
// this needs to be a valid twig template string
$row->add("{{ widgets.info_box_counter('a title', 100, 'far fa-hourglass', 'green') }}");
$event->addWidgetRow($row);
}
}
```
For more details check the [official dashboard subscriber](../../src/EventSubscriber/DashboardSubscriber.php).
## Adding tabs to the "control sidebar"
We use twig globals to render the control sidebar tabs, so adding one is as easy as adding a new config entry: