stacked bars in dashboard widget (#1893)
* cleanup global context usage in widgets * allow empty strings as time format * fix order and update of user settings form * added link to reporting documentation
This commit is contained in:
@@ -13,6 +13,7 @@ use App\Event\DashboardEvent;
|
||||
use App\Widget\Type\AbstractContainer;
|
||||
use App\Widget\Type\AuthorizedWidget;
|
||||
use App\Widget\Type\CompoundRow;
|
||||
use App\Widget\Type\UserWidget;
|
||||
use App\Widget\WidgetContainerInterface;
|
||||
use App\Widget\WidgetException;
|
||||
use App\Widget\WidgetService;
|
||||
@@ -31,15 +32,15 @@ class DashboardController extends AbstractController
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
private $eventDispatcher;
|
||||
/**
|
||||
* @var WidgetService
|
||||
*/
|
||||
protected $widgets;
|
||||
private $widgets;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dashboard;
|
||||
private $dashboard;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
@@ -58,8 +59,9 @@ class DashboardController extends AbstractController
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$event = new DashboardEvent($this->getUser());
|
||||
$user = $this->getUser();
|
||||
|
||||
$event = new DashboardEvent($user);
|
||||
foreach ($this->dashboard as $widgetRow) {
|
||||
if (empty($widgetRow['widgets'])) {
|
||||
continue;
|
||||
@@ -110,6 +112,10 @@ class DashboardController extends AbstractController
|
||||
$add = $tmp;
|
||||
}
|
||||
|
||||
if ($widget instanceof UserWidget) {
|
||||
$widget->setUser($user);
|
||||
}
|
||||
|
||||
if ($add) {
|
||||
$row->addWidget($widget);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ use App\Form\UserTeamsType;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Utils\LocaleSettings;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@@ -212,13 +211,6 @@ class ProfileController extends AbstractController
|
||||
$event = new PrepareUserEvent($profile);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
/** @var \ArrayIterator $iterator */
|
||||
$iterator = $profile->getPreferences()->getIterator();
|
||||
$iterator->uasort(function (UserPreference $a, UserPreference $b) {
|
||||
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
|
||||
});
|
||||
$profile->setPreferences(new ArrayCollection(iterator_to_array($iterator)));
|
||||
|
||||
$original = [];
|
||||
foreach ($profile->getPreferences() as $preference) {
|
||||
$original[$preference->getName()] = $preference;
|
||||
@@ -227,48 +219,59 @@ class ProfileController extends AbstractController
|
||||
$form = $this->createPreferencesForm($profile);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$preferences = $profile->getPreferences();
|
||||
if ($form->isSubmitted()) {
|
||||
if ($form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$preferences = $profile->getPreferences();
|
||||
|
||||
// do not allow to add unknown preferences
|
||||
foreach ($preferences as $preference) {
|
||||
if (!isset($original[$preference->getName()])) {
|
||||
$preferences->removeElement($preference);
|
||||
// do not allow to add unknown preferences
|
||||
foreach ($preferences as $preference) {
|
||||
if (!isset($original[$preference->getName()])) {
|
||||
$preferences->removeElement($preference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// but allow to delete already saved settings
|
||||
foreach ($original as $name => $preference) {
|
||||
if (false === $profile->getPreferences()->contains($preference)) {
|
||||
$entityManager->remove($preference);
|
||||
// but allow to delete already saved settings
|
||||
foreach ($original as $name => $preference) {
|
||||
if (false === $profile->getPreferences()->contains($preference)) {
|
||||
$entityManager->remove($preference);
|
||||
}
|
||||
}
|
||||
|
||||
$profile->setPreferences($preferences);
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
// switch locale ONLY if updated profile is the current user
|
||||
$locale = $request->getLocale();
|
||||
if ($this->getUser()->getId() === $profile->getId()) {
|
||||
$locale = $profile->getPreferenceValue('language', $locale);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('user_profile_preferences', [
|
||||
'_locale' => $locale,
|
||||
'username' => $profile->getUsername()
|
||||
]);
|
||||
} else {
|
||||
$this->flashError('action.update.error', ['%reason%' => 'Validation failed']);
|
||||
}
|
||||
|
||||
$profile->setPreferences($preferences);
|
||||
$entityManager->persist($profile);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
// switch locale ONLY if updated profile is the current user
|
||||
$locale = $request->getLocale();
|
||||
if ($this->getUser()->getId() === $profile->getId()) {
|
||||
$locale = $profile->getPreferenceValue('language', $locale);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('user_profile_preferences', [
|
||||
'_locale' => $locale,
|
||||
'username' => $profile->getUsername()
|
||||
]);
|
||||
}
|
||||
|
||||
// prepare ordered preferences
|
||||
$sections = [];
|
||||
|
||||
/** @var \ArrayIterator $iterator */
|
||||
$iterator = $profile->getPreferences()->getIterator();
|
||||
$iterator->uasort(function (UserPreference $a, UserPreference $b) {
|
||||
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
|
||||
});
|
||||
|
||||
/** @var UserPreference $pref */
|
||||
foreach ($profile->getPreferences() as $pref) {
|
||||
foreach ($iterator as $pref) {
|
||||
if ($pref->isEnabled()) {
|
||||
$sections[$pref->getSection()] = $pref->getSection();
|
||||
$sections[$pref->getSection()][] = $pref->getName();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ class User extends BaseUser implements UserInterface
|
||||
}
|
||||
|
||||
foreach ($this->preferences as $preference) {
|
||||
if ($preference->getName() == $name) {
|
||||
if ($preference->getName() === $name) {
|
||||
return $preference;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\Entity\UserPreference;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event should be used, if further user preferences should added dynamically
|
||||
* This event should be used, if further user preferences should be added dynamically.
|
||||
*/
|
||||
final class UserPreferenceEvent extends Event
|
||||
{
|
||||
@@ -26,11 +26,11 @@ final class UserPreferenceEvent extends Event
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
private $user;
|
||||
/**
|
||||
* @var UserPreference[]
|
||||
*/
|
||||
protected $preferences;
|
||||
private $preferences = [];
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
@@ -44,9 +44,10 @@ final class UserPreferenceEvent extends Event
|
||||
|
||||
/**
|
||||
* Do not set the preferences directly to the user object, but ONLY via addPreference()
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ final class UserPreferenceEvent extends Event
|
||||
/**
|
||||
* @return UserPreference[]
|
||||
*/
|
||||
public function getPreferences()
|
||||
public function getPreferences(): array
|
||||
{
|
||||
return $this->preferences;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraints\Range;
|
||||
|
||||
@@ -38,19 +37,14 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $voter;
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
protected $formConfig;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage, AuthorizationCheckerInterface $voter, FormConfiguration $formConfig)
|
||||
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter, FormConfiguration $formConfig)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->storage = $storage;
|
||||
$this->voter = $voter;
|
||||
$this->formConfig = $formConfig;
|
||||
}
|
||||
|
||||
@@ -17,31 +17,23 @@ use Symfony\Component\HttpKernel\Event\KernelEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class UserProfileSubscriber implements EventSubscriberInterface
|
||||
final class UserProfileSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
private $eventDispatcher;
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
private $storage;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param TokenStorageInterface $storage
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->storage = $storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -49,10 +41,7 @@ class UserProfileSubscriber implements EventSubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
*/
|
||||
public function prepareUserProfile(KernelEvent $event)
|
||||
public function prepareUserProfile(KernelEvent $event): void
|
||||
{
|
||||
if (!$this->canHandleEvent($event)) {
|
||||
return;
|
||||
@@ -65,11 +54,7 @@ class UserProfileSubscriber implements EventSubscriberInterface
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelEvent $event
|
||||
* @return bool
|
||||
*/
|
||||
protected function canHandleEvent(KernelEvent $event): bool
|
||||
private function canHandleEvent(KernelEvent $event): bool
|
||||
{
|
||||
// Ignore sub-requests
|
||||
if (!$event->isMasterRequest()) {
|
||||
|
||||
@@ -34,10 +34,6 @@ class UserPreferenceType extends AbstractType
|
||||
$this->translate = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addEventListener(
|
||||
@@ -56,7 +52,7 @@ class UserPreferenceType extends AbstractType
|
||||
}
|
||||
|
||||
$required = true;
|
||||
if (CheckboxType::class == $preference->getType()) {
|
||||
if (CheckboxType::class === $preference->getType()) {
|
||||
$required = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -416,7 +416,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$newDateBegin = clone $endTmp;
|
||||
}
|
||||
|
||||
// make sure to exclude entries that are outside the requested timerange:
|
||||
// make sure to exclude entries that are outside the requested time-range:
|
||||
// these entries can exist if you have long running entries that started before $begin
|
||||
// for statistical reasons we have to include everything between $begin and $end while
|
||||
// excluding everything that is outside of that range
|
||||
@@ -449,6 +449,7 @@ class TimesheetRepository extends EntityRepository
|
||||
. '_' . $result->getProject()->getId()
|
||||
. '_' . $result->getActivity()->getId()
|
||||
;
|
||||
|
||||
if (!isset($results[$dateKey]['details'][$detailsId])) {
|
||||
$results[$dateKey]['details'][$detailsId] = [
|
||||
'project' => $result->getProject(),
|
||||
@@ -456,10 +457,10 @@ class TimesheetRepository extends EntityRepository
|
||||
'duration' => 0,
|
||||
'rate' => 0,
|
||||
];
|
||||
|
||||
$results[$dateKey]['details'][$detailsId]['duration'] += $duration;
|
||||
$results[$dateKey]['details'][$detailsId]['rate'] += $rate;
|
||||
}
|
||||
|
||||
$results[$dateKey]['details'][$detailsId]['duration'] += $duration;
|
||||
$results[$dateKey]['details'][$detailsId]['rate'] += $rate;
|
||||
}
|
||||
|
||||
$beginTmp = $newDateBegin;
|
||||
|
||||
@@ -9,10 +9,8 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\CurrentUser;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\Type\SimpleStatisticChart;
|
||||
use App\Widget\Type\YearChart;
|
||||
use App\Widget\WidgetException;
|
||||
use App\Widget\WidgetInterface;
|
||||
@@ -25,29 +23,19 @@ class WidgetRepository
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $widgets = [];
|
||||
private $widgets = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $definitions = [];
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
protected $user;
|
||||
private $definitions = [];
|
||||
|
||||
/**
|
||||
* @param TimesheetRepository $repository
|
||||
* @param CurrentUser $user
|
||||
* @param array $widgets
|
||||
*/
|
||||
public function __construct(TimesheetRepository $repository, CurrentUser $user, array $widgets)
|
||||
public function __construct(TimesheetRepository $repository, array $widgets)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->user = $user->getUser();
|
||||
$this->definitions = array_merge($this->getDefaultWidgets(), $widgets);
|
||||
}
|
||||
|
||||
@@ -89,12 +77,6 @@ class WidgetRepository
|
||||
*/
|
||||
protected function create(string $name, array $widget): WidgetInterface
|
||||
{
|
||||
$user = $this->user;
|
||||
$timezone = new \DateTimeZone($user->getTimezone());
|
||||
$begin = !empty($widget['begin']) ? new \DateTime($widget['begin'], $timezone) : null;
|
||||
$end = !empty($widget['end']) ? new \DateTime($widget['end'], $timezone) : null;
|
||||
$theUser = $widget['user'] ? $user : null;
|
||||
|
||||
if (!isset($widget['type'])) {
|
||||
@trigger_error('Using a widget definition without a "type" is deprecated', E_USER_DEPRECATED);
|
||||
$widget['type'] = Counter::class;
|
||||
@@ -104,30 +86,25 @@ class WidgetRepository
|
||||
throw new WidgetException(sprintf('Unknown widget type "%s"', $widgetClassName));
|
||||
}
|
||||
|
||||
/** @var AbstractWidgetType $model */
|
||||
$model = new $widgetClassName();
|
||||
if (!($model instanceof AbstractWidgetType)) {
|
||||
/** @var SimpleStatisticChart $model */
|
||||
$model = new $widgetClassName($this->repository);
|
||||
if (!($model instanceof SimpleStatisticChart)) {
|
||||
throw new WidgetException(
|
||||
sprintf(
|
||||
'Widget type "%s" is not an instance of "%s"',
|
||||
$widgetClassName,
|
||||
AbstractWidgetType::class
|
||||
SimpleStatisticChart::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->repository->getStatistic($widget['query'], $begin, $end, $theUser);
|
||||
} catch (\Exception $ex) {
|
||||
throw new WidgetException(
|
||||
'Failed loading widget data: ' . $ex->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$model
|
||||
->setQuery($widget['query'])
|
||||
->setBegin($widget['begin'])
|
||||
->setEnd($widget['end'])
|
||||
->setId($name)
|
||||
->setTitle($widget['title'])
|
||||
->setData($data);
|
||||
;
|
||||
|
||||
if ($widget['query'] == TimesheetRepository::STATS_QUERY_DURATION) {
|
||||
$model->setOption('dataType', 'duration');
|
||||
|
||||
@@ -9,75 +9,19 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\ThemeEvent;
|
||||
use App\Security\CurrentUser;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use App\Twig\Runtime\ThemeEventExtension;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class EventExtensions extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param CurrentUser $user
|
||||
*/
|
||||
public function __construct(EventDispatcherInterface $dispatcher, CurrentUser $user)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->user = $user->getUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('trigger', [$this, 'triggerEvent']),
|
||||
new TwigFunction('trigger', [ThemeEventExtension::class, 'trigger']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EventDispatcherInterface
|
||||
*/
|
||||
protected function getDispatcher()
|
||||
{
|
||||
return $this->eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $eventName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasListener($eventName)
|
||||
{
|
||||
return $this->getDispatcher()->hasListeners($eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $eventName
|
||||
* @param mixed $payload
|
||||
* @return ThemeEvent
|
||||
*/
|
||||
public function triggerEvent(string $eventName, $payload = null)
|
||||
{
|
||||
$themeEvent = new ThemeEvent($this->user, $payload);
|
||||
|
||||
if ($this->hasListener($eventName)) {
|
||||
$this->getDispatcher()->dispatch($themeEvent, $eventName);
|
||||
}
|
||||
|
||||
return $themeEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ final class MarkdownExtension extends AbstractExtension
|
||||
/**
|
||||
* Transforms the entities comment (customer, project, activity ...) into HTML.
|
||||
*
|
||||
* @param string $content
|
||||
* @param string|null $content
|
||||
* @param bool $fullLength
|
||||
* @return string
|
||||
*/
|
||||
|
||||
49
src/Twig/Runtime/ThemeEventExtension.php
Normal file
49
src/Twig/Runtime/ThemeEventExtension.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\Runtime;
|
||||
|
||||
use App\Event\ThemeEvent;
|
||||
use App\Security\CurrentUser;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
final class ThemeEventExtension implements RuntimeExtensionInterface
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
/**
|
||||
* @var CurrentUser
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, CurrentUser $user)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $eventName
|
||||
* @param mixed|null $payload
|
||||
* @return ThemeEvent
|
||||
*/
|
||||
public function trigger(string $eventName, $payload = null): ThemeEvent
|
||||
{
|
||||
$themeEvent = new ThemeEvent($this->user->getUser(), $payload);
|
||||
|
||||
if ($this->eventDispatcher->hasListeners($eventName)) {
|
||||
$this->eventDispatcher->dispatch($themeEvent, $eventName);
|
||||
}
|
||||
|
||||
return $themeEvent;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,10 @@ class TimeFormatValidator extends ConstraintValidator
|
||||
throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\TimeFormat');
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
|
||||
throw new UnexpectedValueException($value, 'string');
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ abstract class AbstractWidgetType implements WidgetInterface
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
public function setId(string $id): AbstractWidgetType
|
||||
public function setId(string $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
@@ -42,7 +42,7 @@ abstract class AbstractWidgetType implements WidgetInterface
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setData($data): AbstractWidgetType
|
||||
public function setData($data): self
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
@@ -58,7 +58,7 @@ abstract class AbstractWidgetType implements WidgetInterface
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): AbstractWidgetType
|
||||
public function setTitle(string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
@@ -70,7 +70,7 @@ abstract class AbstractWidgetType implements WidgetInterface
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setOptions(array $options): AbstractWidgetType
|
||||
public function setOptions(array $options): self
|
||||
{
|
||||
foreach ($options as $key => $value) {
|
||||
$this->options[$key] = $value;
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
class Counter extends SimpleWidget
|
||||
use App\Repository\TimesheetRepository;
|
||||
|
||||
final class Counter extends SimpleStatisticChart
|
||||
{
|
||||
public function __construct()
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
{
|
||||
parent::__construct($repository);
|
||||
$this->setOption('dataType', 'int');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Security\CurrentUser;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use DateTime;
|
||||
|
||||
class DailyWorkingTimeChart extends SimpleWidget
|
||||
class DailyWorkingTimeChart extends SimpleWidget implements UserWidget
|
||||
{
|
||||
public const DEFAULT_CHART = 'bar';
|
||||
|
||||
@@ -22,27 +23,26 @@ class DailyWorkingTimeChart extends SimpleWidget
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
private $dateTimeFactory;
|
||||
|
||||
public function __construct(TimesheetRepository $repository, CurrentUser $user, UserDateTimeFactory $dateTime)
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->dateTimeFactory = $dateTime;
|
||||
$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 setUser(User $user): void
|
||||
{
|
||||
$this->setOption('user', $user);
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
$options = parent::getOptions($options);
|
||||
@@ -63,18 +63,44 @@ class DailyWorkingTimeChart extends SimpleWidget
|
||||
$options = $this->getOptions($options);
|
||||
|
||||
$user = $options['user'];
|
||||
if (null === $user || !($user instanceof User)) {
|
||||
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
|
||||
}
|
||||
|
||||
if ($options['begin'] instanceof DateTime) {
|
||||
$begin = $options['begin'];
|
||||
} else {
|
||||
$begin = new DateTime($options['begin'], $this->dateTimeFactory->getTimezone());
|
||||
$begin = new DateTime($options['begin'], new \DateTimeZone($user->getTimezone()));
|
||||
}
|
||||
|
||||
if ($options['end'] instanceof DateTime) {
|
||||
$end = $options['end'];
|
||||
} else {
|
||||
$end = new DateTime($options['end'], $this->dateTimeFactory->getTimezone());
|
||||
$end = new DateTime($options['end'], new \DateTimeZone($user->getTimezone()));
|
||||
}
|
||||
|
||||
return $this->repository->getDailyStats($user, $begin, $end);
|
||||
$activities = [];
|
||||
$statistics = $this->repository->getDailyStats($user, $begin, $end);
|
||||
|
||||
foreach ($statistics as $day) {
|
||||
foreach ($day->getDetails() as $entry) {
|
||||
/** @var Activity $activity */
|
||||
$activity = $entry['activity'];
|
||||
/** @var Project $project */
|
||||
$project = $entry['project'];
|
||||
|
||||
$id = $project->getId() . '_' . $activity->getId();
|
||||
|
||||
$activities[$id] = [
|
||||
'activity' => $activity,
|
||||
'project' => $project,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'activities' => $activities,
|
||||
'data' => $statistics,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,37 +9,40 @@
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Security\CurrentUser;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use DateTime;
|
||||
|
||||
final class PaginatedWorkingTimeChart extends SimpleWidget
|
||||
final class PaginatedWorkingTimeChart extends SimpleWidget implements UserWidget
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
private $dateTimeFactory;
|
||||
|
||||
public function __construct(TimesheetRepository $repository, CurrentUser $user, UserDateTimeFactory $dateTime)
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->dateTimeFactory = $dateTime;
|
||||
$this->setId('PaginatedWorkingTimeChart');
|
||||
$this->setTitle('stats.yourWorkingHours');
|
||||
|
||||
$this->setOptions([
|
||||
'year' => (new DateTime('now', $this->dateTimeFactory->getTimezone()))->format('Y'),
|
||||
'week' => (new DateTime('now', $this->dateTimeFactory->getTimezone()))->format('W'),
|
||||
'user' => $user->getUser(),
|
||||
'year' => (new DateTime('now'))->format('Y'),
|
||||
'week' => (new DateTime('now'))->format('W'),
|
||||
'type' => 'bar',
|
||||
]);
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->setOption('user', $user);
|
||||
$now = new DateTime('now', new \DateTimeZone($user->getTimezone()));
|
||||
$this->setOptions([
|
||||
'year' => $now->format('Y'),
|
||||
'week' => $now->format('W'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
{
|
||||
$options = parent::getOptions($options);
|
||||
@@ -51,9 +54,9 @@ final class PaginatedWorkingTimeChart extends SimpleWidget
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getDate($year, $week, $day, $hour, $minute, $second)
|
||||
private function getDate(\DateTimeZone $timezone, $year, $week, $day, $hour, $minute, $second)
|
||||
{
|
||||
$now = new DateTime('now', $this->dateTimeFactory->getTimezone());
|
||||
$now = new DateTime('now', $timezone);
|
||||
$now->setISODate($year, $week, $day);
|
||||
$now->setTime($hour, $minute, $second);
|
||||
|
||||
@@ -63,10 +66,16 @@ final class PaginatedWorkingTimeChart extends SimpleWidget
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$options = $this->getOptions($options);
|
||||
$user = $options['user'];
|
||||
|
||||
$weekBegin = $this->getDate($options['year'], $options['week'], 1, 0, 0, 0);
|
||||
$weekEnd = $this->getDate($options['year'], $options['week'], 7, 23, 59, 59);
|
||||
$user = $options['user'];
|
||||
if (null === $user || !($user instanceof User)) {
|
||||
throw new \InvalidArgumentException('Widget option "user" must be an instance of ' . User::class);
|
||||
}
|
||||
|
||||
$timezone = new \DateTimeZone($user->getTimezone());
|
||||
|
||||
$weekBegin = $this->getDate($timezone, $options['year'], $options['week'], 1, 0, 0, 0);
|
||||
$weekEnd = $this->getDate($timezone, $options['year'], $options['week'], 7, 23, 59, 59);
|
||||
|
||||
return [
|
||||
'begin' => clone $weekBegin,
|
||||
@@ -74,8 +83,8 @@ final class PaginatedWorkingTimeChart extends SimpleWidget
|
||||
'stats' => $this->repository->getDailyStats($user, $weekBegin, $weekEnd),
|
||||
'day' => $this->repository->getStatistic(
|
||||
'duration',
|
||||
new DateTime('00:00:00', $this->dateTimeFactory->getTimezone()),
|
||||
new DateTime('23:59:59', $this->dateTimeFactory->getTimezone()),
|
||||
new DateTime('00:00:00', $timezone),
|
||||
new DateTime('23:59:59', $timezone),
|
||||
$user
|
||||
),
|
||||
'week' => $this->repository->getStatistic(
|
||||
@@ -92,8 +101,8 @@ final class PaginatedWorkingTimeChart extends SimpleWidget
|
||||
),
|
||||
'year' => $this->repository->getStatistic(
|
||||
'duration',
|
||||
new DateTime(sprintf('01 january %s 00:00:00', $options['year']), $this->dateTimeFactory->getTimezone()),
|
||||
new DateTime(sprintf('31 december %s 23:59:59', $options['year']), $this->dateTimeFactory->getTimezone()),
|
||||
new DateTime(sprintf('01 january %s 00:00:00', $options['year']), $timezone),
|
||||
new DateTime(sprintf('31 december %s 23:59:59', $options['year']), $timezone),
|
||||
$user
|
||||
),
|
||||
];
|
||||
|
||||
101
src/Widget/Type/SimpleStatisticChart.php
Normal file
101
src/Widget/Type/SimpleStatisticChart.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\WidgetException;
|
||||
|
||||
class SimpleStatisticChart extends SimpleWidget
|
||||
{
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $query;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $begin;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $end;
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(TimesheetRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function setQuery(string $query): SimpleStatisticChart
|
||||
{
|
||||
$this->query = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setBegin(?string $begin): SimpleStatisticChart
|
||||
{
|
||||
$this->begin = $begin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEnd(?string $end): SimpleStatisticChart
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUser(User $user): SimpleStatisticChart
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setData($data): AbstractWidgetType
|
||||
{
|
||||
throw new \InvalidArgumentException('Cannot set data on instances of SimpleStatisticChart');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return mixed|null
|
||||
* @throws WidgetException
|
||||
*/
|
||||
public function getData(array $options = [])
|
||||
{
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $this->user) {
|
||||
$timezone = $this->user->getTimezone();
|
||||
}
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
|
||||
$begin = !empty($this->begin) ? new \DateTime($this->begin, $timezone) : null;
|
||||
$end = !empty($this->end) ? new \DateTime($this->end, $timezone) : null;
|
||||
|
||||
try {
|
||||
return $this->repository->getStatistic($this->query, $begin, $end, $this->user);
|
||||
} catch (\Exception $ex) {
|
||||
throw new WidgetException(
|
||||
'Failed loading widget data: ' . $ex->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,23 +13,19 @@ use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Security\CurrentUser;
|
||||
|
||||
class UserTeamProjects extends SimpleWidget implements AuthorizedWidget
|
||||
class UserTeamProjects extends SimpleWidget implements AuthorizedWidget, UserWidget
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
public function __construct(CurrentUser $user, ProjectRepository $repository)
|
||||
public function __construct(ProjectRepository $repository)
|
||||
{
|
||||
$this->setId('UserTeamProjects');
|
||||
$this->setTitle('label.my_team_projects');
|
||||
$this->setOptions([
|
||||
'user' => $user->getUser(),
|
||||
'id' => '',
|
||||
]);
|
||||
$this->setOption('id', '');
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
@@ -80,4 +76,9 @@ class UserTeamProjects extends SimpleWidget implements AuthorizedWidget
|
||||
{
|
||||
return ['budget_team_project', 'budget_teamlead_project', 'budget_project'];
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->setOption('user', $user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,14 @@
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\CurrentUser;
|
||||
|
||||
class UserTeams extends SimpleWidget implements AuthorizedWidget
|
||||
class UserTeams extends SimpleWidget implements AuthorizedWidget, UserWidget
|
||||
{
|
||||
public function __construct(CurrentUser $user)
|
||||
public function __construct()
|
||||
{
|
||||
$this->setId('UserTeams');
|
||||
$this->setTitle('label.my_teams');
|
||||
$this->setOptions([
|
||||
'user' => $user->getUser(),
|
||||
'id' => '',
|
||||
]);
|
||||
$this->setOption('id', '');
|
||||
}
|
||||
|
||||
public function getOptions(array $options = []): array
|
||||
@@ -51,4 +47,9 @@ class UserTeams extends SimpleWidget implements AuthorizedWidget
|
||||
{
|
||||
return ['view_team_member', 'view_team'];
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->setOption('user', $user);
|
||||
}
|
||||
}
|
||||
|
||||
22
src/Widget/Type/UserWidget.php
Normal file
22
src/Widget/Type/UserWidget.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
interface UserWidget
|
||||
{
|
||||
/**
|
||||
* Sets the current user.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function setUser(User $user): void;
|
||||
}
|
||||
@@ -9,6 +9,6 @@
|
||||
|
||||
namespace App\Widget\Type;
|
||||
|
||||
class YearChart extends SimpleWidget
|
||||
final class YearChart extends SimpleStatisticChart
|
||||
{
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ class WidgetService
|
||||
/**
|
||||
* @var WidgetRendererInterface[]
|
||||
*/
|
||||
protected $renderer = [];
|
||||
private $renderer = [];
|
||||
/**
|
||||
* @var WidgetRepository
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @param WidgetRepository $repository
|
||||
@@ -34,10 +34,6 @@ class WidgetService
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $widget
|
||||
* @return bool
|
||||
*/
|
||||
public function hasWidget(string $widget): bool
|
||||
{
|
||||
return $this->repository->has($widget);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
{% set children = children|merge({(report.id): {'title': report.label|trans({}, 'reporting'), 'url': path(report.route), 'class': 'toolbar-action report-' ~ report.id}}) %}
|
||||
{% endfor %}
|
||||
{% set actions = actions|merge({'reporting': {'children': children}}) %}
|
||||
{% set actions = actions|merge({'help': {'url': 'reporting.html'|docu_link, 'target': '_blank'}}) %}
|
||||
{% set event = trigger('actions.reporting', {'actions': actions}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
{% block main %}
|
||||
{{ form_start(form) }}
|
||||
{% for section, counter in sections %}
|
||||
{% for section, entries in sections %}
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% block box_body %}
|
||||
{% for pref in form.children.preferences %}
|
||||
{% if pref.vars.data.section == section %}
|
||||
{% if pref.vars.data.name in entries %}
|
||||
{{ form_row(pref) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{{ render_widget('PaginatedWorkingTimeChart', {'year': year, 'week': week}) }}
|
||||
{{ render_widget('PaginatedWorkingTimeChart', {'user': user, 'year': year, 'week': week}) }}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
{% 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 %}
|
||||
{% set activities = data.activities %}
|
||||
{% set data = data.data %}
|
||||
|
||||
<div class="chart">
|
||||
<canvas id="{{ chart_id }}" style="height: {{ kimai_context.chart.height }}px;"></canvas>
|
||||
@@ -31,22 +24,50 @@
|
||||
{%- endfor %}
|
||||
],
|
||||
datasets: [
|
||||
{% for activityId, activity in activities -%}
|
||||
{% set activityColor = activity.activity|color|default(activity.project|color|default(backgroundColor)) %}
|
||||
{% set activityName = activity.activity.name %}
|
||||
{
|
||||
backgroundColor: '{{ backgroundColor }}',
|
||||
borderColor: '{{ borderColor }}',
|
||||
label: '{{ activityName }}',
|
||||
backgroundColor: '{{ activityColor }}',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1,
|
||||
data: [
|
||||
{% for day in data -%}
|
||||
{{ (day.totalDuration / 3600)|number_format(2, '.', '') }}
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor %}
|
||||
{%- for day in data -%}
|
||||
{% set realDayData = null %}
|
||||
{%- for entry in day.details -%}
|
||||
{% set loopId = (entry.project.id ~ '_' ~ entry.activity.id) %}
|
||||
{%- if loopId == activityId -%}
|
||||
{% set realDayData = (entry.duration / 3600)|number_format(2, '.', '') %}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if realDayData is not null -%}
|
||||
'{{ realDayData }}'
|
||||
{%- else -%}
|
||||
0
|
||||
{%- endif -%}
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor -%}
|
||||
],
|
||||
realData: [
|
||||
{% for day in data -%}
|
||||
'{{ day.totalDuration|duration }}'
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor %}
|
||||
{%- for day in data -%}
|
||||
{% set realDayData = null %}
|
||||
{%- for entry in day.details -%}
|
||||
{% set loopId = (entry.project.id ~ '_' ~ entry.activity.id) %}
|
||||
{%- if loopId == activityId -%}
|
||||
{% set realDayData = {duration: entry.duration|duration, project: entry.project.name, customer: entry.project.customer.name, activity: entry.activity.name, total: day.totalDuration|duration} %}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if realDayData is not null -%}
|
||||
{{ realDayData|json_encode|raw }}
|
||||
{%- else -%}
|
||||
0
|
||||
{%- endif -%}
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor -%}
|
||||
]
|
||||
}
|
||||
},
|
||||
{%- endfor %}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
@@ -57,11 +78,13 @@
|
||||
categoryPercentage: 0.9,
|
||||
scales: {
|
||||
xAxes: [{
|
||||
stacked: true,
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
}],
|
||||
yAxes: [{
|
||||
stacked: true,
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
},
|
||||
@@ -70,14 +93,36 @@
|
||||
color: '{{ gridColor }}',
|
||||
lineWidth: 1
|
||||
}
|
||||
}]
|
||||
}],
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function(tooltipItem, data) {
|
||||
return data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
|
||||
}
|
||||
}
|
||||
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
|
||||
return ' ' + tooltipData.duration + ': ' + tooltipData.activity;
|
||||
},
|
||||
beforeTitle: function(tooltipItems, data) {
|
||||
var tooltipItem = tooltipItems[0];
|
||||
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
|
||||
return tooltipData.customer;
|
||||
},
|
||||
title: function(tooltipItems, data) {
|
||||
var tooltipItem = tooltipItems[0];
|
||||
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
|
||||
return tooltipData.project;
|
||||
},
|
||||
afterTitle: function(tooltipItems, data) {
|
||||
return ' ';
|
||||
},
|
||||
footer: function(tooltipItems, data) {
|
||||
var tooltipItem = tooltipItems[0];
|
||||
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
|
||||
return '{{ 'stats.durationTotal'|trans }}: ' + tooltipData.total;
|
||||
},
|
||||
beforeFooter: function(tooltipItems, data) {
|
||||
return ' ';
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,10 +410,14 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
return [
|
||||
// assert that the user doesn't have the "hourly-rate_own_profile" permission
|
||||
[User::ROLE_USER, UserFixtures::USERNAME_USER, 82, 82, 'ar', null],
|
||||
// admins are allowed to update their own hourly rate
|
||||
// teamleads are allowed to update their own hourly rate, but not other peoples hourly rate
|
||||
[User::ROLE_TEAMLEAD, UserFixtures::USERNAME_TEAMLEAD, 35, 37.5, 'ar', 19.54],
|
||||
// admins are allowed to update their own hourly rate, but not other peoples hourly rate
|
||||
[User::ROLE_ADMIN, UserFixtures::USERNAME_ADMIN, 81, 37.5, 'ar', 19.54],
|
||||
// admins are allowed to update other peoples hourly rate
|
||||
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_USER, 82, 37.5, 'en', 19.54],
|
||||
// super-admins are allowed to update other peoples hourly rate
|
||||
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_ADMIN, 81, 37.5, 'en', 19.54],
|
||||
// super-admins are allowed to update their own hourly rate
|
||||
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_SUPER_ADMIN, 46, 37.5, 'ar', 19.54],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -431,21 +435,16 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals($hourlyRateOriginal, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
|
||||
$this->assertNull($user->getPreferenceValue(UserPreference::INTERNAL_RATE));
|
||||
$this->assertNull($user->getPreferenceValue(UserPreference::SKIN));
|
||||
$this->assertEquals(false, $user->getPreferenceValue('theme.collapsed_sidebar'));
|
||||
$this->assertEquals('month', $user->getPreferenceValue('calendar.initial_view'));
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=user_preferences_form]')->form();
|
||||
$client->submit($form, [
|
||||
'user_preferences_form' => [
|
||||
'preferences' => [
|
||||
['name' => UserPreference::HOURLY_RATE, 'value' => 37.5],
|
||||
['name' => UserPreference::INTERNAL_RATE, 'value' => 19.54],
|
||||
['name' => 'timezone', 'value' => 'America/Creston'],
|
||||
['name' => 'language', 'value' => 'ar'],
|
||||
['name' => UserPreference::SKIN, 'value' => 'blue'],
|
||||
['name' => 'theme.layout', 'value' => 'fixed'],
|
||||
['name' => 'theme.collapsed_sidebar', 'value' => true],
|
||||
['name' => 'calendar.initial_view', 'value' => 'agendaDay'],
|
||||
0 => ['name' => UserPreference::HOURLY_RATE, 'value' => 37.5],
|
||||
1 => ['name' => UserPreference::INTERNAL_RATE, 'value' => 19.54],
|
||||
2 => ['name' => UserPreference::TIMEZONE, 'value' => 'America/Creston'],
|
||||
3 => ['name' => UserPreference::LOCALE, 'value' => 'ar'],
|
||||
4 => ['name' => UserPreference::SKIN, 'value' => 'blue'],
|
||||
]
|
||||
]
|
||||
]);
|
||||
@@ -462,10 +461,11 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
|
||||
$this->assertEquals($hourlyRate, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
|
||||
$this->assertEquals($expectedInternalRate, $user->getPreferenceValue(UserPreference::INTERNAL_RATE));
|
||||
$this->assertEquals('', $user->getPreferenceValue('America/Creston'));
|
||||
$this->assertEquals('ar', $user->getPreferenceValue('language'));
|
||||
$this->assertEquals('America/Creston', $user->getPreferenceValue(UserPreference::TIMEZONE));
|
||||
$this->assertEquals('America/Creston', $user->getTimezone());
|
||||
$this->assertEquals('ar', $user->getPreferenceValue(UserPreference::LOCALE));
|
||||
$this->assertEquals('ar', $user->getLanguage());
|
||||
$this->assertEquals('ar', $user->getLocale());
|
||||
$this->assertEquals('blue', $user->getPreferenceValue(UserPreference::SKIN));
|
||||
$this->assertEquals(true, $user->getPreferenceValue('theme.collapsed_sidebar'));
|
||||
$this->assertEquals('agendaDay', $user->getPreferenceValue('calendar.initial_view'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,10 +353,8 @@ class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||
[
|
||||
'#system_configuration_form_calendar_configuration_2_value',
|
||||
'#system_configuration_form_calendar_configuration_3_value',
|
||||
'#system_configuration_form_calendar_configuration_3_value',
|
||||
'#system_configuration_form_calendar_configuration_4_value',
|
||||
'#system_configuration_form_calendar_configuration_5_value',
|
||||
'#system_configuration_form_calendar_configuration_5_value',
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
34
tests/Controller/WidgetControllerTest.php
Normal file
34
tests/Controller/WidgetControllerTest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
*/
|
||||
class WidgetControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/widgets/working-time/2020/1');
|
||||
}
|
||||
|
||||
public function testWorkingtimechartAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, '/widgets/working-time/2020/1');
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertStringContainsString('id="PaginatedWorkingTimeChart"', $content);
|
||||
self::assertStringContainsString('myChart = new Chart', $content);
|
||||
self::assertStringContainsString("KimaiPaginatedBoxWidget.create('#PaginatedWorkingTimeChart');", $content);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ use App\Event\PrepareUserEvent;
|
||||
use App\EventSubscriber\UserPreferenceSubscriber;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
@@ -88,9 +87,8 @@ class UserPreferenceSubscriberTest extends TestCase
|
||||
$authMock->expects($this->once())->method('isGranted')->willReturn($seeHourlyRate);
|
||||
|
||||
$eventMock = $this->createMock(EventDispatcherInterface::class);
|
||||
$tokenMock = $this->createMock(TokenStorageInterface::class);
|
||||
$formConfigMock = $this->createMock(FormConfiguration::class);
|
||||
|
||||
return new UserPreferenceSubscriber($eventMock, $tokenMock, $authMock, $formConfigMock);
|
||||
return new UserPreferenceSubscriber($eventMock, $authMock, $formConfigMock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,8 @@
|
||||
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\WidgetRepository;
|
||||
use App\Tests\Mocks\Security\CurrentUserFactory;
|
||||
use App\Widget\Type\CompoundChart;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\WidgetException;
|
||||
@@ -26,9 +24,8 @@ class WidgetRepositoryTest extends TestCase
|
||||
public function testHasWidget()
|
||||
{
|
||||
$repoMock = $this->createMock(TimesheetRepository::class);
|
||||
$userMock = (new CurrentUserFactory($this))->create(new User());
|
||||
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => []]);
|
||||
$sut = new WidgetRepository($repoMock, ['test' => []]);
|
||||
|
||||
$this->assertFalse($sut->has('foo'));
|
||||
$this->assertTrue($sut->has('test'));
|
||||
@@ -40,9 +37,8 @@ class WidgetRepositoryTest extends TestCase
|
||||
$this->expectExceptionMessage('Cannot find widget "foo".');
|
||||
|
||||
$repoMock = $this->createMock(TimesheetRepository::class);
|
||||
$userMock = (new CurrentUserFactory($this))->create(new User());
|
||||
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => []]);
|
||||
$sut = new WidgetRepository($repoMock, ['test' => []]);
|
||||
$sut->get('foo');
|
||||
}
|
||||
|
||||
@@ -52,21 +48,19 @@ class WidgetRepositoryTest extends TestCase
|
||||
$this->expectExceptionMessage('Unknown widget type "FooBar"');
|
||||
|
||||
$repoMock = $this->createMock(TimesheetRepository::class);
|
||||
$userMock = (new CurrentUserFactory($this))->create(new User());
|
||||
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => ['type' => 'FooBar', 'user' => false]]);
|
||||
$sut = new WidgetRepository($repoMock, ['test' => ['type' => 'FooBar', 'user' => false]]);
|
||||
$sut->get('test');
|
||||
}
|
||||
|
||||
public function testGetWidgetTriggersExceptionOnWrongClass()
|
||||
{
|
||||
$this->expectException(WidgetException::class);
|
||||
$this->expectExceptionMessage('Widget type "App\Widget\Type\CompoundChart" is not an instance of "App\Widget\Type\AbstractWidgetType"');
|
||||
$this->expectExceptionMessage('Widget type "App\Widget\Type\CompoundChart" is not an instance of "App\Widget\Type\SimpleStatisticChart"');
|
||||
|
||||
$repoMock = $this->createMock(TimesheetRepository::class);
|
||||
$userMock = (new CurrentUserFactory($this))->create(new User());
|
||||
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => ['type' => CompoundChart::class, 'user' => false]]);
|
||||
$sut = new WidgetRepository($repoMock, ['test' => ['type' => CompoundChart::class, 'user' => false]]);
|
||||
$sut->get('test');
|
||||
}
|
||||
|
||||
@@ -78,8 +72,6 @@ class WidgetRepositoryTest extends TestCase
|
||||
$repoMock = $this->createMock(TimesheetRepository::class);
|
||||
$repoMock->method('getStatistic')->willReturn($data);
|
||||
|
||||
$userMock = (new CurrentUserFactory($this))->create(new User());
|
||||
|
||||
$widget = [
|
||||
'color' => 'sunny',
|
||||
'icon' => 'far fa-test',
|
||||
@@ -91,7 +83,7 @@ class WidgetRepositoryTest extends TestCase
|
||||
'type' => Counter::class,
|
||||
];
|
||||
|
||||
$sut = new WidgetRepository($repoMock, $userMock, ['test' => $widget]);
|
||||
$sut = new WidgetRepository($repoMock, ['test' => $widget]);
|
||||
$widget = $sut->get('test');
|
||||
|
||||
$options = $widget->getOptions();
|
||||
|
||||
39
tests/Twig/EventExtensionsTest.php
Normal file
39
tests/Twig/EventExtensionsTest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Twig;
|
||||
|
||||
use App\Twig\EventExtensions;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* @covers \App\Twig\EventExtensions
|
||||
*/
|
||||
class EventExtensionsTest extends TestCase
|
||||
{
|
||||
protected function getSut(): EventExtensions
|
||||
{
|
||||
return new EventExtensions();
|
||||
}
|
||||
|
||||
public function testGetFunctions()
|
||||
{
|
||||
$functions = ['trigger'];
|
||||
$sut = $this->getSut();
|
||||
$twigFunctions = $sut->getFunctions();
|
||||
self::assertCount(\count($functions), $twigFunctions);
|
||||
$i = 0;
|
||||
/** @var TwigFunction $filter */
|
||||
foreach ($twigFunctions as $filter) {
|
||||
self::assertInstanceOf(TwigFunction::class, $filter);
|
||||
self::assertEquals($functions[$i++], $filter->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
48
tests/Twig/Runtime/ThemeEventExtensionTest.php
Normal file
48
tests/Twig/Runtime/ThemeEventExtensionTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Runtime;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\ThemeEvent;
|
||||
use App\Tests\Mocks\Security\CurrentUserFactory;
|
||||
use App\Twig\Runtime\ThemeEventExtension;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Twig\Runtime\ThemeEventExtension
|
||||
*/
|
||||
class ThemeEventExtensionTest extends TestCase
|
||||
{
|
||||
protected function getSut(bool $hasListener = true): ThemeEventExtension
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects($this->once())->method('hasListeners')->willReturn($hasListener);
|
||||
$dispatcher->expects($hasListener ? $this->once() : $this->never())->method('dispatch');
|
||||
|
||||
$user = (new CurrentUserFactory($this))->create(new User());
|
||||
|
||||
return new ThemeEventExtension($dispatcher, $user);
|
||||
}
|
||||
|
||||
public function testTrigger()
|
||||
{
|
||||
$sut = $this->getSut();
|
||||
$event = $sut->trigger('foo', []);
|
||||
self::assertInstanceOf(ThemeEvent::class, $event);
|
||||
}
|
||||
|
||||
public function testTriggerWithoutListener()
|
||||
{
|
||||
$sut = $this->getSut(false);
|
||||
$event = $sut->trigger('foo', []);
|
||||
self::assertInstanceOf(ThemeEvent::class, $event);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Tests\Twig;
|
||||
|
||||
use App\Twig\WidgetExtension;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\Type\More;
|
||||
use App\Widget\WidgetInterface;
|
||||
use App\Widget\WidgetRendererInterface;
|
||||
use App\Widget\WidgetService;
|
||||
@@ -72,7 +72,7 @@ class WidgetExtensionTest extends TestCase
|
||||
|
||||
public function testRenderWidgetByString()
|
||||
{
|
||||
$widget = new Counter();
|
||||
$widget = new More();
|
||||
$sut = $this->getSut(true, $widget, new TestRenderer());
|
||||
$options = ['foo' => 'bar', 'dataType' => 'blub'];
|
||||
$result = $sut->renderWidget('test', $options);
|
||||
@@ -82,7 +82,7 @@ class WidgetExtensionTest extends TestCase
|
||||
|
||||
public function testRenderWidgetObject()
|
||||
{
|
||||
$widget = new Counter();
|
||||
$widget = new More();
|
||||
$sut = $this->getSut(null, null, new TestRenderer());
|
||||
$options = ['foo' => 'bar', 'dataType' => 'blub'];
|
||||
$result = $sut->renderWidget($widget, $options);
|
||||
|
||||
@@ -44,7 +44,7 @@ class TimeFormatValidatorTest extends ConstraintValidatorTestCase
|
||||
/**
|
||||
* @dataProvider getValidTimes
|
||||
*/
|
||||
public function testValidationSucceeds(string $value)
|
||||
public function testValidationSucceeds(?string $value)
|
||||
{
|
||||
$this->validator->validate($value, new TimeFormat());
|
||||
$this->assertNoViolation();
|
||||
@@ -53,6 +53,8 @@ class TimeFormatValidatorTest extends ConstraintValidatorTestCase
|
||||
public function getValidTimes()
|
||||
{
|
||||
return [
|
||||
[''],
|
||||
[null],
|
||||
['00:00'],
|
||||
['00:01'],
|
||||
['23:00'],
|
||||
@@ -65,7 +67,7 @@ class TimeFormatValidatorTest extends ConstraintValidatorTestCase
|
||||
/**
|
||||
* @dataProvider getInvalidTimes
|
||||
*/
|
||||
public function testValidationProblem(string $value)
|
||||
public function testValidationProblem(?string $value)
|
||||
{
|
||||
$this->validator->validate($value, new TimeFormat());
|
||||
|
||||
@@ -78,6 +80,7 @@ class TimeFormatValidatorTest extends ConstraintValidatorTestCase
|
||||
public function getInvalidTimes()
|
||||
{
|
||||
return [
|
||||
['a'],
|
||||
['1:00'],
|
||||
['01:1'],
|
||||
['00:60'],
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 App\Widget\Type\More;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
@@ -40,7 +40,7 @@ class CompoundChartRendererTest extends TestCase
|
||||
$sut = new CompoundChartRenderer($twig);
|
||||
$row = new CompoundChart();
|
||||
$row->setTitle('foo-bar');
|
||||
$row->addWidget(new Counter());
|
||||
$row->addWidget(new More());
|
||||
|
||||
$result = $sut->render($row);
|
||||
$result = json_decode($result, true);
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 App\Widget\Type\More;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
@@ -40,7 +40,7 @@ class CompoundRowRendererTest extends TestCase
|
||||
$sut = new CompoundRowRenderer($twig);
|
||||
$row = new CompoundRow();
|
||||
$row->setTitle('foo-bar');
|
||||
$row->addWidget(new Counter());
|
||||
$row->addWidget(new More());
|
||||
|
||||
$result = $sut->render($row);
|
||||
$result = json_decode($result, true);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
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;
|
||||
@@ -60,7 +59,6 @@ class SimpleWidgetRendererTest extends TestCase
|
||||
{
|
||||
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'],
|
||||
];
|
||||
}
|
||||
|
||||
26
tests/Widget/Type/AbstractSimpleStatisticsWidgetTypeTest.php
Normal file
26
tests/Widget/Type/AbstractSimpleStatisticsWidgetTypeTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\SimpleStatisticChart
|
||||
*/
|
||||
abstract class AbstractSimpleStatisticsWidgetTypeTest extends AbstractWidgetTypeTest
|
||||
{
|
||||
public function testData()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot set data on instances of SimpleStatisticChart');
|
||||
|
||||
$sut->setData(10);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ abstract class AbstractWidgetTypeTest extends TestCase
|
||||
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()
|
||||
@@ -57,8 +56,14 @@ abstract class AbstractWidgetTypeTest extends TestCase
|
||||
// id
|
||||
$sut->setId('cvbnmyx');
|
||||
self::assertEquals('cvbnmyx', $sut->getId());
|
||||
}
|
||||
|
||||
public function testData()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
self::assertInstanceOf(AbstractWidgetType::class, $sut->setData(''));
|
||||
|
||||
// data
|
||||
$sut->setData('slkudfhalksjdhfkljsahdf');
|
||||
self::assertEquals('slkudfhalksjdhfkljsahdf', $sut->getData());
|
||||
|
||||
|
||||
@@ -9,19 +9,24 @@
|
||||
|
||||
namespace App\Tests\Widget\Type;
|
||||
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\Counter;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\Counter
|
||||
* @covers \App\Widget\Type\SimpleStatisticChart
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
*/
|
||||
class CounterTest extends AbstractWidgetTypeTest
|
||||
class CounterTest extends AbstractSimpleStatisticsWidgetTypeTest
|
||||
{
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return new Counter();
|
||||
$sut = new Counter($this->createMock(TimesheetRepository::class));
|
||||
$sut->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE);
|
||||
|
||||
return $sut;
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
@@ -37,7 +42,8 @@ class CounterTest extends AbstractWidgetTypeTest
|
||||
|
||||
public function testTemplateName()
|
||||
{
|
||||
$sut = new Counter();
|
||||
/** @var Counter $sut */
|
||||
$sut = $this->createSut();
|
||||
self::assertEquals('widget/widget-counter.html.twig', $sut->getTemplateName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
namespace App\Tests\Widget\Type;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Day;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Tests\Mocks\Security\CurrentUserFactory;
|
||||
use App\Tests\Mocks\Security\UserDateTimeFactoryFactory;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\DailyWorkingTimeChart;
|
||||
use App\Widget\Type\SimpleWidget;
|
||||
@@ -30,11 +30,11 @@ class DailyWorkingTimeChartTest extends TestCase
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
$repository = $this->createMock(TimesheetRepository::class);
|
||||
$mockFactory = new UserDateTimeFactoryFactory($this);
|
||||
$userFactory = new CurrentUserFactory($this);
|
||||
$user = $userFactory->create(new User(), 'Europe/Berlin');
|
||||
|
||||
return new DailyWorkingTimeChart($repository, $user, $mockFactory->create('Europe/Berlin'));
|
||||
$sut = new DailyWorkingTimeChart($repository);
|
||||
$sut->setUser(new User());
|
||||
|
||||
return $sut;
|
||||
}
|
||||
|
||||
public function testExtendsSimpleWidget()
|
||||
@@ -100,22 +100,38 @@ class DailyWorkingTimeChartTest extends TestCase
|
||||
|
||||
public function testGetData()
|
||||
{
|
||||
$activity = $this->createMock(Activity::class);
|
||||
$activity->method('getId')->willReturn(42);
|
||||
|
||||
$project = $this->createMock(Project::class);
|
||||
$project->method('getId')->willReturn(4711);
|
||||
|
||||
$repository = $this->getMockBuilder(TimesheetRepository::class)->disableOriginalConstructor()->onlyMethods(['getDailyData'])->getMock();
|
||||
$repository->expects($this->once())->method('getDailyData')->willReturnCallback(function ($begin, $end, $user) {
|
||||
$repository->expects($this->once())->method('getDailyData')->willReturnCallback(function ($begin, $end, $user) use ($activity, $project) {
|
||||
return [
|
||||
['year' => $begin->format('Y'), 'month' => $begin->format('n'), 'day' => $begin->format('j'), 'rate' => 13.75, 'duration' => 1234, 'details' => []]
|
||||
['year' => $begin->format('Y'), 'month' => $begin->format('n'), 'day' => $begin->format('j'), 'rate' => 13.75, 'duration' => 1234, 'details' => [
|
||||
['activity' => $activity, 'project' => $project]
|
||||
]]
|
||||
];
|
||||
});
|
||||
|
||||
$userFactory = new CurrentUserFactory($this);
|
||||
$user = $userFactory->create(new User(), 'Europe/Berlin');
|
||||
|
||||
$mockFactory = new UserDateTimeFactoryFactory($this);
|
||||
|
||||
$sut = new DailyWorkingTimeChart($repository, $user, $mockFactory->create('Europe/Berlin'));
|
||||
$sut = new DailyWorkingTimeChart($repository);
|
||||
$sut->setUser(new User());
|
||||
$data = $sut->getData([]);
|
||||
self::assertCount(7, $data);
|
||||
foreach ($data as $statObj) {
|
||||
self::assertCount(2, $data);
|
||||
self::assertArrayHasKey('activities', $data);
|
||||
self::assertArrayHasKey('data', $data);
|
||||
|
||||
self::assertCount(1, $data['activities']);
|
||||
self::assertArrayHasKey('4711_42', $data['activities']);
|
||||
self::assertCount(2, $data['activities']['4711_42']);
|
||||
self::assertArrayHasKey('activity', $data['activities']['4711_42']);
|
||||
self::assertArrayHasKey('project', $data['activities']['4711_42']);
|
||||
self::assertSame($activity, $data['activities']['4711_42']['activity']);
|
||||
self::assertSame($project, $data['activities']['4711_42']['project']);
|
||||
|
||||
self::assertCount(7, $data['data']);
|
||||
foreach ($data['data'] as $statObj) {
|
||||
self::assertInstanceOf(Day::class, $statObj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,23 @@
|
||||
|
||||
namespace App\Tests\Widget\Type;
|
||||
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Widget\Type\AbstractWidgetType;
|
||||
use App\Widget\Type\YearChart;
|
||||
|
||||
/**
|
||||
* @covers \App\Widget\Type\YearChart
|
||||
* @covers \App\Widget\Type\SimpleStatisticChart
|
||||
* @covers \App\Widget\Type\SimpleWidget
|
||||
*/
|
||||
class YearChartTest extends AbstractWidgetTypeTest
|
||||
class YearChartTest extends AbstractSimpleStatisticsWidgetTypeTest
|
||||
{
|
||||
public function createSut(): AbstractWidgetType
|
||||
{
|
||||
return new YearChart();
|
||||
$sut = new YearChart($this->createMock(TimesheetRepository::class));
|
||||
$sut->setQuery(TimesheetRepository::STATS_QUERY_ACTIVE);
|
||||
|
||||
return $sut;
|
||||
}
|
||||
|
||||
public function getDefaultOptions(): array
|
||||
|
||||
Reference in New Issue
Block a user