dashboard widgets are configurable via config (#269)
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Event\DashboardEvent;
|
||||
use App\Model\DashboardSection;
|
||||
use App\Repository\WidgetRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
@@ -28,13 +30,25 @@ class DashboardController extends Controller
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
/**
|
||||
* @var WidgetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dashboard;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param WidgetRepository $repository
|
||||
* @param array $dashboard
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher)
|
||||
public function __construct(EventDispatcherInterface $dispatcher, WidgetRepository $repository, array $dashboard)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->repository = $repository;
|
||||
$this->dashboard = $dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,13 +59,52 @@ class DashboardController extends Controller
|
||||
{
|
||||
$event = new DashboardEvent($this->getUser());
|
||||
|
||||
foreach ($this->dashboard as $widgetRow) {
|
||||
if (empty($widgetRow['widgets'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->isGranted($widgetRow['permission'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = new DashboardSection($widgetRow['title'] ?? null);
|
||||
$row
|
||||
->setOrder($widgetRow['order'])
|
||||
->setType($widgetRow['type'])
|
||||
;
|
||||
|
||||
foreach ($widgetRow['widgets'] as $widgetName) {
|
||||
if (!$this->repository->has($widgetName)) {
|
||||
throw new \Exception('Unknwon widget: ' . $widgetName);
|
||||
}
|
||||
|
||||
$row->addWidget($this->repository->get($widgetName, $event->getUser()));
|
||||
}
|
||||
|
||||
$event->addSection($row);
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(
|
||||
DashboardEvent::DASHBOARD,
|
||||
$event
|
||||
);
|
||||
|
||||
$sections = $event->getSections();
|
||||
|
||||
uasort(
|
||||
$sections,
|
||||
function (DashboardSection $a, DashboardSection $b) {
|
||||
if ($a->getOrder() == $b->getOrder()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
|
||||
}
|
||||
);
|
||||
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'widget_rows' => $event->getWidgetRows()
|
||||
'widget_rows' => $sections
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
public const MAX_TIMESHEETS_TOTAL = 5000;
|
||||
public const MIN_RUNNING_TIMESHEETS_PER_USER = 0;
|
||||
public const MAX_RUNNING_TIMESHEETS_PER_USER = 3;
|
||||
public const TIMERANGE_DAYS = 1095; // 3 years
|
||||
public const MIN_MINUTES_PER_ENTRY = 15;
|
||||
public const MAX_MINUTES_PER_ENTRY = 840; // 14h
|
||||
|
||||
public const BATCH_SIZE = 100;
|
||||
|
||||
@@ -77,9 +80,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$description,
|
||||
round($i / 2),
|
||||
true
|
||||
$description
|
||||
);
|
||||
|
||||
$manager->persist($entry);
|
||||
@@ -97,7 +98,8 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
null
|
||||
null,
|
||||
false
|
||||
);
|
||||
$manager->persist($entry);
|
||||
}
|
||||
@@ -140,32 +142,29 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
return $all;
|
||||
}
|
||||
|
||||
private function createTimesheetEntry(User $user, Activity $activity, $description, $startDay = 0, $setEndDate = false)
|
||||
private function createTimesheetEntry(User $user, Activity $activity, $description, $setEndDate = true)
|
||||
{
|
||||
$start = new \DateTime();
|
||||
if ($startDay > 0) {
|
||||
$start = $start->modify('- ' . (rand(1, $startDay)) . ' days');
|
||||
}
|
||||
$start = $start->modify('- ' . (rand(1, self::TIMERANGE_DAYS)) . ' days');
|
||||
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');
|
||||
|
||||
$end = clone $start;
|
||||
$end = $end->modify('+ ' . (rand(1, 43200)) . ' seconds');
|
||||
|
||||
//$duration = $end->modify('- ' . $start->getTimestamp() . ' seconds')->getTimestamp();
|
||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||
$rate = $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
->setActivity($activity)
|
||||
->setDescription($description)
|
||||
->setUser($user)
|
||||
->setRate(round(($duration / 3600) * $rate))
|
||||
->setBegin($start);
|
||||
|
||||
if ($setEndDate) {
|
||||
$end = clone $start;
|
||||
$end = $end->modify('+ ' . (rand(self::MIN_MINUTES_PER_ENTRY, self::MAX_MINUTES_PER_ENTRY)) . ' minutes');
|
||||
|
||||
$duration = $end->getTimestamp() - $start->getTimestamp();
|
||||
$rate = $user->getPreferenceValue(UserPreference::HOURLY_RATE);
|
||||
|
||||
$entry
|
||||
->setEnd($end)
|
||||
->setRate(round(($duration / 3600) * $rate))
|
||||
->setDuration($duration);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
||||
$container->setParameter('kimai.languages', $config['languages']);
|
||||
$container->setParameter('kimai.calendar', $config['calendar']);
|
||||
$container->setParameter('kimai.theme', $config['theme']);
|
||||
$container->setParameter('kimai.dashboard', $config['dashboard']);
|
||||
$container->setParameter('kimai.widgets', $config['widgets']);
|
||||
|
||||
$this->createUserParameter($config, $container);
|
||||
$this->createTimesheetParameter($config, $container);
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Model\DashboardSection;
|
||||
use App\Model\Widget;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
@@ -29,156 +31,282 @@ class Configuration implements ConfigurationInterface
|
||||
|
||||
$rootNode
|
||||
->children()
|
||||
->arrayNode('theme')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->integerNode('active_warning')
|
||||
->defaultValue(3)
|
||||
->end()
|
||||
->scalarNode('box_color')
|
||||
->defaultValue('green')
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('user')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->booleanNode('registration')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->booleanNode('password_reset')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('timesheet')
|
||||
->children()
|
||||
->booleanNode('duration_only')
|
||||
->defaultValue(false)
|
||||
->end()
|
||||
->arrayNode('rounding')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->integerNode('begin')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->integerNode('end')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->integerNode('duration')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
|
||||
->arrayNode('rates')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->floatNode('factor')
|
||||
->isRequired()
|
||||
->defaultValue(1)
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('invoice')
|
||||
->children()
|
||||
->arrayNode('renderer')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Controller\InvoiceController::invoiceAction',
|
||||
])
|
||||
->end()
|
||||
->arrayNode('calculator')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Invoice\DefaultCalculator',
|
||||
])
|
||||
->end()
|
||||
->arrayNode('number_generator')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Invoice\DateNumberGenerator',
|
||||
])
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('languages')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('date_short')->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('calendar')
|
||||
->children()
|
||||
->booleanNode('week_numbers')->defaultTrue()->end()
|
||||
->integerNode('day_limit')->defaultValue(4)->end()
|
||||
->arrayNode('businessHours')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->prototype('integer')->end()
|
||||
->defaultValue([1, 2, 3, 4, 5])
|
||||
->end()
|
||||
->scalarNode('begin')->defaultValue('08:00')->end()
|
||||
->scalarNode('end')->defaultValue('20:00')->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('google')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('api_key')->defaultNull()->end()
|
||||
->arrayNode('sources')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('id')->isRequired()->end()
|
||||
->scalarNode('color')->defaultValue('#ccc')->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->append($this->getUserNode())
|
||||
->append($this->getTimesheetNode())
|
||||
->append($this->getInvoiceNode())
|
||||
->append($this->getLanguagesNode())
|
||||
->append($this->getCalendarNode())
|
||||
->append($this->getThemeNode())
|
||||
->append($this->getDashboardNode())
|
||||
->append($this->getWidgetsNode())
|
||||
->end()
|
||||
->end();
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
|
||||
protected function getTimesheetNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('timesheet');
|
||||
|
||||
$node
|
||||
->children()
|
||||
->booleanNode('duration_only')
|
||||
->defaultValue(false)
|
||||
->end()
|
||||
->arrayNode('rounding')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->integerNode('begin')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->integerNode('end')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->integerNode('duration')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
|
||||
->arrayNode('rates')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->floatNode('factor')
|
||||
->isRequired()
|
||||
->defaultValue(1)
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->defaultValue([])
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getInvoiceNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('invoice');
|
||||
|
||||
$node
|
||||
->children()
|
||||
->arrayNode('renderer')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Controller\InvoiceController::invoiceAction',
|
||||
])
|
||||
->end()
|
||||
->arrayNode('calculator')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Invoice\DefaultCalculator',
|
||||
])
|
||||
->end()
|
||||
->arrayNode('number_generator')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue([
|
||||
'default' => 'App\Invoice\DateNumberGenerator',
|
||||
])
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getLanguagesNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('languages');
|
||||
|
||||
$node
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('date_short')->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getCalendarNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('calendar');
|
||||
|
||||
$node
|
||||
->children()
|
||||
->booleanNode('week_numbers')->defaultTrue()->end()
|
||||
->integerNode('day_limit')->defaultValue(4)->end()
|
||||
->arrayNode('businessHours')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->arrayNode('days')
|
||||
->requiresAtLeastOneElement()
|
||||
->prototype('integer')->end()
|
||||
->defaultValue([1, 2, 3, 4, 5])
|
||||
->end()
|
||||
->scalarNode('begin')->defaultValue('08:00')->end()
|
||||
->scalarNode('end')->defaultValue('20:00')->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('google')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('api_key')->defaultNull()->end()
|
||||
->arrayNode('sources')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->children()
|
||||
->scalarNode('id')->isRequired()->end()
|
||||
->scalarNode('color')->defaultValue('#ccc')->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getThemeNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('theme');
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->integerNode('active_warning')
|
||||
->defaultValue(3)
|
||||
->end()
|
||||
->scalarNode('box_color')
|
||||
->defaultValue('green')
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getUserNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('user');
|
||||
|
||||
$node
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->booleanNode('registration')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->booleanNode('password_reset')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getWidgetsNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('widgets');
|
||||
|
||||
$node
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('title')->isRequired()->end()
|
||||
->scalarNode('query')->isRequired()->end()
|
||||
->booleanNode('user')->defaultFalse()->end()
|
||||
->scalarNode('begin')->end()
|
||||
->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()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getDashboardNode()
|
||||
{
|
||||
$builder = new TreeBuilder();
|
||||
$node = $builder->root('dashboard');
|
||||
|
||||
$node
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->arrayPrototype()
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->scalarNode('type')
|
||||
->validate()
|
||||
->ifNotInArray([DashboardSection::TYPE_SIMPLE, DashboardSection::TYPE_CHART])->thenInvalid('Unknown section type')
|
||||
->end()
|
||||
->defaultValue(DashboardSection::TYPE_SIMPLE)
|
||||
->end()
|
||||
->integerNode('order')->defaultValue(0)->end()
|
||||
->scalarNode('title')->end()
|
||||
->scalarNode('permission')->isRequired()->end()
|
||||
->arrayNode('widgets')
|
||||
->isRequired()
|
||||
->performNoDeepMerging()
|
||||
->scalarPrototype()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\TimesheetGlobalStatistic;
|
||||
use App\Model\WidgetRow;
|
||||
use App\Model\DashboardSection;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
class DashboardEvent extends Event
|
||||
@@ -23,13 +22,12 @@ class DashboardEvent extends Event
|
||||
*/
|
||||
protected $user;
|
||||
/**
|
||||
* @var WidgetRow[]
|
||||
* @var DashboardSection[]
|
||||
*/
|
||||
protected $widgetRows = [];
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param TimesheetGlobalStatistic $timesheetStatistic
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
@@ -44,15 +42,21 @@ class DashboardEvent extends Event
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function addWidgetRow(WidgetRow $row)
|
||||
/**
|
||||
* @param DashboardSection $row
|
||||
* @return DashboardEvent
|
||||
*/
|
||||
public function addSection(DashboardSection $row)
|
||||
{
|
||||
$this->widgetRows[] = $row;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WidgetRow[]
|
||||
* @return DashboardSection[]
|
||||
*/
|
||||
public function getWidgetRows()
|
||||
public function getSections()
|
||||
{
|
||||
return $this->widgetRows;
|
||||
}
|
||||
|
||||
@@ -9,23 +9,19 @@
|
||||
|
||||
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 App\Model\DashboardSection;
|
||||
use App\Model\Widget;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* Used to add Dashboard widgets for a user.
|
||||
* Used to add Dashboard widgets for users with ROLE_ADMIN.
|
||||
*/
|
||||
class DashboardSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
@@ -34,18 +30,41 @@ class DashboardSubscriber implements EventSubscriberInterface
|
||||
*/
|
||||
protected $security;
|
||||
/**
|
||||
* @var ManagerRegistry
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $registry;
|
||||
protected $user;
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
protected $activity;
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
protected $project;
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
protected $customer;
|
||||
|
||||
/**
|
||||
* MenuSubscriber constructor.
|
||||
* @param AuthorizationCheckerInterface $security
|
||||
* @param UserRepository $user
|
||||
* @param ActivityRepository $activity
|
||||
* @param ProjectRepository $project
|
||||
* @param CustomerRepository $customer
|
||||
*/
|
||||
public function __construct(AuthorizationCheckerInterface $security, ManagerRegistry $registry)
|
||||
{
|
||||
public function __construct(
|
||||
AuthorizationCheckerInterface $security,
|
||||
UserRepository $user,
|
||||
ActivityRepository $activity,
|
||||
ProjectRepository $project,
|
||||
CustomerRepository $customer
|
||||
) {
|
||||
$this->security = $security;
|
||||
$this->registry = $registry;
|
||||
$this->user = $user;
|
||||
$this->activity = $activity;
|
||||
$this->project = $project;
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,106 +83,57 @@ class DashboardSubscriber implements EventSubscriberInterface
|
||||
*/
|
||||
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')) {
|
||||
if (!$this->security->isGranted(User::ROLE_ADMIN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addTeamleadWidgets($event, $timesheetGlobal, $userStats);
|
||||
|
||||
if (!$this->security->isGranted('ROLE_ADMIN')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addAdminWidgets($event, $timesheetGlobal, $userStats);
|
||||
$this->addAdminWidgets($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
protected function addAdminWidgets(DashboardEvent $event)
|
||||
{
|
||||
$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);
|
||||
$section = new DashboardSection('dashboard.admin');
|
||||
$section->setOrder(100);
|
||||
|
||||
$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') }}")
|
||||
$widget = new Widget('stats.userTotal', $this->user->countUser());
|
||||
$widget
|
||||
->setRoute('admin_user')
|
||||
->setIcon('user')
|
||||
->setType(Widget::TYPE_MORE)
|
||||
;
|
||||
$event->addWidgetRow($row);
|
||||
$section->addWidget($widget);
|
||||
|
||||
$widget = new Widget('stats.customerTotal', $this->customer->countCustomer());
|
||||
$widget
|
||||
->setRoute('admin_customer')
|
||||
->setIcon('customer')
|
||||
->setColor('blue')
|
||||
->setType(Widget::TYPE_MORE)
|
||||
;
|
||||
$section->addWidget($widget);
|
||||
|
||||
$widget = new Widget('stats.projectTotal', $this->project->countProject());
|
||||
$widget
|
||||
->setRoute('admin_project')
|
||||
->setIcon('project')
|
||||
->setColor('yellow')
|
||||
->setType(Widget::TYPE_MORE)
|
||||
;
|
||||
$section->addWidget($widget);
|
||||
|
||||
$widget = new Widget('stats.activityTotal', $this->activity->countActivity());
|
||||
$widget
|
||||
->setRoute('admin_activity')
|
||||
->setIcon('activity')
|
||||
->setColor('purple')
|
||||
->setType(Widget::TYPE_MORE)
|
||||
;
|
||||
$section->addWidget($widget);
|
||||
|
||||
$event->addSection($section);
|
||||
}
|
||||
}
|
||||
|
||||
105
src/Model/DashboardSection.php
Normal file
105
src/Model/DashboardSection.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
class DashboardSection
|
||||
{
|
||||
const TYPE_SIMPLE = 'simple';
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +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;
|
||||
|
||||
/**
|
||||
* Timesheet statistics for all user.
|
||||
*/
|
||||
class TimesheetGlobalStatistic extends TimesheetStatistic
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $activeThisMonth = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $activeTotal = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $activeCurrently = 0;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getActiveCurrently()
|
||||
{
|
||||
return $this->activeCurrently;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $activeCurrently
|
||||
*/
|
||||
public function setActiveCurrently($activeCurrently)
|
||||
{
|
||||
$this->activeCurrently = (int) $activeCurrently;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getActiveThisMonth()
|
||||
{
|
||||
return $this->activeThisMonth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $activeThisMonth
|
||||
*/
|
||||
public function setActiveThisMonth($activeThisMonth)
|
||||
{
|
||||
$this->activeThisMonth = (int) $activeThisMonth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getActiveTotal()
|
||||
{
|
||||
return $this->activeTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $activeTotal
|
||||
*/
|
||||
public function setActiveTotal($activeTotal)
|
||||
{
|
||||
$this->activeTotal = (int) $activeTotal;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +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;
|
||||
|
||||
/**
|
||||
* User statistics
|
||||
*/
|
||||
class UserStatistic
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $totalAmount = 0;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalAmount()
|
||||
{
|
||||
return $this->totalAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $totalAmount
|
||||
*/
|
||||
public function setTotalAmount($totalAmount)
|
||||
{
|
||||
$this->totalAmount = (int) $totalAmount;
|
||||
}
|
||||
}
|
||||
219
src/Model/Widget.php
Normal file
219
src/Model/Widget.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +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 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;
|
||||
}
|
||||
}
|
||||
@@ -76,21 +76,11 @@ class ActivityRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Return global statistic data for all user.
|
||||
*
|
||||
* @return ActivityStatistic
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
* @return int
|
||||
*/
|
||||
public function getGlobalStatistics()
|
||||
public function countActivity()
|
||||
{
|
||||
$countAll = $this->getEntityManager()
|
||||
->createQuery('SELECT COUNT(a.id) FROM ' . Activity::class . ' a')
|
||||
->getSingleScalarResult();
|
||||
|
||||
$stats = new ActivityStatistic();
|
||||
$stats->setCount($countAll);
|
||||
|
||||
return $stats;
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,21 +32,11 @@ class CustomerRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Return statistic data for all customer.
|
||||
*
|
||||
* @return CustomerStatistic
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
* @return int
|
||||
*/
|
||||
public function getGlobalStatistics()
|
||||
public function countCustomer()
|
||||
{
|
||||
$countAll = $this->getEntityManager()
|
||||
->createQuery('SELECT COUNT(c.id) FROM ' . Customer::class . ' c')
|
||||
->getSingleScalarResult();
|
||||
|
||||
$stats = new CustomerStatistic();
|
||||
$stats->setCount($countAll);
|
||||
|
||||
return $stats;
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,21 +31,11 @@ class ProjectRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Return statistic data for all user.
|
||||
*
|
||||
* @return ProjectStatistic
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
* @return int
|
||||
*/
|
||||
public function getGlobalStatistics()
|
||||
public function countProject()
|
||||
{
|
||||
$countAll = $this->getEntityManager()
|
||||
->createQuery('SELECT COUNT(p.id) FROM ' . Project::class . ' p')
|
||||
->getSingleScalarResult();
|
||||
|
||||
$stats = new ProjectStatistic();
|
||||
$stats->setCount($countAll);
|
||||
|
||||
return $stats;
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Month;
|
||||
use App\Model\Statistic\Year;
|
||||
use App\Model\TimesheetGlobalStatistic;
|
||||
use App\Model\TimesheetStatistic;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use DateTime;
|
||||
@@ -22,11 +21,15 @@ use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
/**
|
||||
* Class TimesheetRepository
|
||||
*/
|
||||
class TimesheetRepository extends AbstractRepository
|
||||
{
|
||||
public const STATS_QUERY_DURATION = 'duration';
|
||||
public const STATS_QUERY_RATE = 'rate';
|
||||
public const STATS_QUERY_USER = 'users';
|
||||
public const STATS_QUERY_AMOUNT = 'amount';
|
||||
public const STATS_QUERY_ACTIVE = 'active';
|
||||
public const STATS_QUERY_MONTHLY = 'monthly';
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @return bool
|
||||
@@ -67,45 +70,91 @@ class TimesheetRepository extends AbstractRepository
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param DateTime|null $begin
|
||||
* @param DateTime|null $end
|
||||
* @param User|null $user
|
||||
* @return int|mixed
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getStatistic(string $type, ?DateTime $begin, ?DateTime $end, ?User $user)
|
||||
{
|
||||
switch ($type) {
|
||||
case self::STATS_QUERY_ACTIVE:
|
||||
return count($this->getActiveEntries($user));
|
||||
break;
|
||||
case self::STATS_QUERY_MONTHLY:
|
||||
return $this->getMonthlyStats($user, $begin, $end);
|
||||
break;
|
||||
case self::STATS_QUERY_DURATION:
|
||||
$what = 'SUM(t.duration)';
|
||||
break;
|
||||
case self::STATS_QUERY_RATE:
|
||||
$what = 'SUM(t.rate)';
|
||||
break;
|
||||
case self::STATS_QUERY_USER:
|
||||
$what = 'COUNT(DISTINCT(t.user))';
|
||||
break;
|
||||
case self::STATS_QUERY_AMOUNT:
|
||||
$what = 'COUNT(t.id)';
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid query type: ' . $type);
|
||||
}
|
||||
|
||||
return $this->queryTimeRange($what, $begin, $end, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $select
|
||||
* @param User|null $user
|
||||
* @return \Doctrine\ORM\QueryBuilder
|
||||
* @return int
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
protected function queryThisMonth($select, User $user = null)
|
||||
protected function queryThisMonth($select, ?User $user)
|
||||
{
|
||||
$end = new DateTime('last day of this month');
|
||||
$end->setTime(23, 59, 59);
|
||||
$begin = new DateTime('first day of this month');
|
||||
$begin->setTime(0, 0, 0);
|
||||
$begin = new DateTime('first day of this month 00:00:00');
|
||||
$end = new DateTime('last day of this month 23:59:59');
|
||||
|
||||
return $this->queryTimeRange($select, $begin, $end, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $select
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @param string $select
|
||||
* @param DateTime|null $begin
|
||||
* @param DateTime|null $end
|
||||
* @param User|null $user
|
||||
* @return \Doctrine\ORM\QueryBuilder
|
||||
* @return int|mixed
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
protected function queryTimeRange($select, DateTime $begin, DateTime $end, User $user = null)
|
||||
protected function queryTimeRange(string $select, ?DateTime $begin, ?DateTime $end, ?User $user)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select($select)
|
||||
->from(Timesheet::class, 't')
|
||||
->where($qb->expr()->gt('t.begin', ':from'))
|
||||
->andWhere($qb->expr()->lt('t.end', ':to'))
|
||||
->setParameter('from', $begin, Type::DATETIME)
|
||||
->setParameter('to', $end, Type::DATETIME);
|
||||
->from(Timesheet::class, 't');
|
||||
|
||||
if (!empty($begin)) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->gt('t.begin', ':from'))
|
||||
->setParameter('from', $begin, Type::DATETIME);
|
||||
}
|
||||
|
||||
if (!empty($end)) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->lt('t.end', ':to'))
|
||||
->setParameter('to', $end, Type::DATETIME);
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
$result = $qb->getQuery()->getSingleScalarResult();
|
||||
|
||||
return empty($result) ? 0 : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,24 +166,11 @@ class TimesheetRepository extends AbstractRepository
|
||||
*/
|
||||
public function getUserStatistics(User $user)
|
||||
{
|
||||
$durationTotal = $this->getEntityManager()
|
||||
->createQuery('SELECT SUM(t.duration) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
|
||||
->setParameter('user', $user)
|
||||
->getSingleScalarResult();
|
||||
$recordsTotal = $this->getEntityManager()
|
||||
->createQuery('SELECT COUNT(t.id) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
|
||||
->setParameter('user', $user)
|
||||
->getSingleScalarResult();
|
||||
$rateTotal = $this->getEntityManager()
|
||||
->createQuery('SELECT SUM(t.rate) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
|
||||
->setParameter('user', $user)
|
||||
->getSingleScalarResult();
|
||||
$amountMonth = $this->queryThisMonth('SUM(t.rate)', $user)
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
$durationMonth = $this->queryThisMonth('SUM(t.duration)', $user)
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
$durationTotal = $this->getStatistic(self::STATS_QUERY_DURATION, null, null, $user);
|
||||
$recordsTotal = $this->getStatistic(self::STATS_QUERY_AMOUNT, null, null, $user);
|
||||
$rateTotal = $this->getStatistic(self::STATS_QUERY_RATE, null, null, $user);
|
||||
$amountMonth = $this->queryThisMonth('SUM(t.rate)', $user);
|
||||
$durationMonth = $this->queryThisMonth('SUM(t.duration)', $user);
|
||||
$firstEntry = $this->getEntityManager()
|
||||
->createQuery('SELECT MIN(t.begin) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
|
||||
->setParameter('user', $user)
|
||||
@@ -155,23 +191,40 @@ class TimesheetRepository extends AbstractRepository
|
||||
* Returns an array of Year statistics.
|
||||
*
|
||||
* @param User|null $user
|
||||
* @param DateTime|null $begin
|
||||
* @param DateTime|null $end
|
||||
* @return Year[]
|
||||
*/
|
||||
public function getMonthlyStats(User $user = null)
|
||||
public function getMonthlyStats(User $user = null, ?DateTime $begin = null, ?DateTime $end = null)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('SUM(t.rate) as rate, SUM(t.duration) as duration, MONTH(t.begin) as month, YEAR(t.begin) as year')
|
||||
->from(Timesheet::class, 't')
|
||||
->where($qb->expr()->gt('t.begin', '0'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->where($qb->expr()->gt('t.begin', ':from'))
|
||||
;
|
||||
|
||||
if (!empty($begin)) {
|
||||
$qb->setParameter('from', $begin, Type::DATETIME);
|
||||
} else {
|
||||
$qb->setParameter('from', 0);
|
||||
}
|
||||
|
||||
if (!empty($end)) {
|
||||
$qb->andWhere($qb->expr()->lt('t.end', ':to'))
|
||||
->setParameter('to', $end, Type::DATETIME);
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
}
|
||||
|
||||
$qb
|
||||
->orderBy('year', 'DESC')
|
||||
->addOrderBy('month', 'ASC')
|
||||
->groupBy('year')
|
||||
->addGroupBy('month');
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->where('t.user = :user')
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
@@ -198,52 +251,6 @@ class TimesheetRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch statistic data for all user.
|
||||
*
|
||||
* @return TimesheetGlobalStatistic
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getGlobalStatistics()
|
||||
{
|
||||
$durationTotal = $this->getEntityManager()
|
||||
->createQuery('SELECT SUM(t.duration) FROM ' . Timesheet::class . ' t')
|
||||
->getSingleScalarResult();
|
||||
$recordsTotal = $this->getEntityManager()
|
||||
->createQuery('SELECT COUNT(t.id) FROM ' . Timesheet::class . ' t')
|
||||
->getSingleScalarResult();
|
||||
$rateTotal = $this->getEntityManager()
|
||||
->createQuery('SELECT SUM(t.rate) FROM ' . Timesheet::class . ' t')
|
||||
->getSingleScalarResult();
|
||||
$userTotal = $this->getEntityManager()
|
||||
->createQuery('SELECT COUNT(DISTINCT(t.user)) FROM ' . Timesheet::class . ' t')
|
||||
->getSingleScalarResult();
|
||||
$activeNow = $this->getActiveEntries();
|
||||
$amountMonth = $this->queryThisMonth('SUM(t.rate)')
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
$durationMonth = $this->queryThisMonth('SUM(t.duration)')
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
$activeMonth = $this->queryThisMonth('COUNT(DISTINCT(t.user))')
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
|
||||
$stats = new TimesheetGlobalStatistic();
|
||||
$stats->setAmountTotal($rateTotal);
|
||||
$stats->setDurationTotal($durationTotal);
|
||||
$stats->setActiveTotal($userTotal);
|
||||
$stats->setActiveCurrently(count($activeNow));
|
||||
$stats->setActiveThisMonth($activeMonth);
|
||||
$stats->setAmountThisMonth($amountMonth);
|
||||
$stats->setDurationThisMonth($durationMonth);
|
||||
$stats->setRecordsTotal($recordsTotal);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO replace me by a findByQuery() call
|
||||
*
|
||||
* @param User $user
|
||||
* @return Timesheet[]|null
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\UserStatistic;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
||||
|
||||
@@ -29,21 +28,11 @@ class UserRepository extends AbstractRepository implements UserLoaderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Return statistic data for all user.
|
||||
*
|
||||
* @return UserStatistic
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
* @return int
|
||||
*/
|
||||
public function getGlobalStatistics()
|
||||
public function countUser()
|
||||
{
|
||||
$countAll = $this->getEntityManager()
|
||||
->createQuery('SELECT COUNT(u.id) FROM ' . User::class . ' u')
|
||||
->getSingleScalarResult();
|
||||
|
||||
$stats = new UserStatistic();
|
||||
$stats->setTotalAmount($countAll);
|
||||
|
||||
return $stats;
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
82
src/Repository/WidgetRepository.php
Normal file
82
src/Repository/WidgetRepository.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Model\Widget;
|
||||
|
||||
class WidgetRepository
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $widgets = [];
|
||||
|
||||
/**
|
||||
* @param TimesheetRepository $repository
|
||||
* @param array $widgets
|
||||
*/
|
||||
public function __construct(TimesheetRepository $repository, array $widgets)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->widgets = $widgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $name)
|
||||
{
|
||||
return isset($this->widgets[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param User|null $user
|
||||
* @return Widget
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function get(string $name, ?User $user)
|
||||
{
|
||||
if (!$this->has($name)) {
|
||||
throw new \InvalidArgumentException('Cannot find widget: ' . $name);
|
||||
}
|
||||
|
||||
$widget = $this->widgets[$name];
|
||||
|
||||
$begin = !empty($widget['begin']) ? new \DateTime($widget['begin']) : null;
|
||||
$end = !empty($widget['end']) ? new \DateTime($widget['end']) : null;
|
||||
$theUser = $widget['user'] ? $user : null;
|
||||
$type = isset($widget['type']) ? $widget['type'] : Widget::TYPE_COUNTER;
|
||||
|
||||
$data = $this->repository->getStatistic($widget['query'], $begin, $end, $theUser);
|
||||
|
||||
$model = new Widget($widget['title'], $data);
|
||||
$model
|
||||
->setColor($widget['color'])
|
||||
->setIcon($widget['icon'])
|
||||
->setType($type)
|
||||
;
|
||||
|
||||
if ($widget['query'] == TimesheetRepository::STATS_QUERY_DURATION) {
|
||||
$model->setDataType(Widget::DATA_TYPE_DURATION);
|
||||
} elseif ($widget['query'] == TimesheetRepository::STATS_QUERY_AMOUNT) {
|
||||
$model->setDataType(Widget::DATA_TYPE_MONEY);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,8 @@ class Extensions extends \Twig_Extension
|
||||
'trash' => 'far fa-trash-alt',
|
||||
'user' => 'fas fa-user',
|
||||
'visibility' => 'far fa-eye',
|
||||
'money' => 'far fa-money-bill-alt',
|
||||
'duration' => 'far fa-hourglass',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user