diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index e090dd37..42a728dd 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -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); } diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index e6f35431..e603652f 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -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(); } } diff --git a/src/Entity/User.php b/src/Entity/User.php index bbb5211e..7de5fc9e 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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; } } diff --git a/src/Event/UserPreferenceEvent.php b/src/Event/UserPreferenceEvent.php index 0512faf0..45eabbe3 100644 --- a/src/Event/UserPreferenceEvent.php +++ b/src/Event/UserPreferenceEvent.php @@ -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; } diff --git a/src/EventSubscriber/UserPreferenceSubscriber.php b/src/EventSubscriber/UserPreferenceSubscriber.php index 89c20ab1..093b16e1 100644 --- a/src/EventSubscriber/UserPreferenceSubscriber.php +++ b/src/EventSubscriber/UserPreferenceSubscriber.php @@ -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; } diff --git a/src/EventSubscriber/UserProfileSubscriber.php b/src/EventSubscriber/UserProfileSubscriber.php index b675d318..30eff558 100644 --- a/src/EventSubscriber/UserProfileSubscriber.php +++ b/src/EventSubscriber/UserProfileSubscriber.php @@ -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()) { diff --git a/src/Form/Type/UserPreferenceType.php b/src/Form/Type/UserPreferenceType.php index e23a730e..8a568374 100644 --- a/src/Form/Type/UserPreferenceType.php +++ b/src/Form/Type/UserPreferenceType.php @@ -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; } diff --git a/src/Repository/TimesheetRepository.php b/src/Repository/TimesheetRepository.php index 00f762f5..2518053f 100644 --- a/src/Repository/TimesheetRepository.php +++ b/src/Repository/TimesheetRepository.php @@ -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; diff --git a/src/Repository/WidgetRepository.php b/src/Repository/WidgetRepository.php index 614450cb..5bb88b10 100644 --- a/src/Repository/WidgetRepository.php +++ b/src/Repository/WidgetRepository.php @@ -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'); diff --git a/src/Twig/EventExtensions.php b/src/Twig/EventExtensions.php index 1146d1f8..9c04df8d 100644 --- a/src/Twig/EventExtensions.php +++ b/src/Twig/EventExtensions.php @@ -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; - } } diff --git a/src/Twig/MarkdownExtension.php b/src/Twig/MarkdownExtension.php index 05e3790f..e2076cc7 100644 --- a/src/Twig/MarkdownExtension.php +++ b/src/Twig/MarkdownExtension.php @@ -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 */ diff --git a/src/Twig/Runtime/ThemeEventExtension.php b/src/Twig/Runtime/ThemeEventExtension.php new file mode 100644 index 00000000..e824726d --- /dev/null +++ b/src/Twig/Runtime/ThemeEventExtension.php @@ -0,0 +1,49 @@ +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; + } +} diff --git a/src/Validator/Constraints/TimeFormatValidator.php b/src/Validator/Constraints/TimeFormatValidator.php index b6c2cbf7..b8ed544b 100644 --- a/src/Validator/Constraints/TimeFormatValidator.php +++ b/src/Validator/Constraints/TimeFormatValidator.php @@ -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'); } diff --git a/src/Widget/Type/AbstractWidgetType.php b/src/Widget/Type/AbstractWidgetType.php index 07970b3f..fd16c4a6 100644 --- a/src/Widget/Type/AbstractWidgetType.php +++ b/src/Widget/Type/AbstractWidgetType.php @@ -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; diff --git a/src/Widget/Type/Counter.php b/src/Widget/Type/Counter.php index 81ace956..4d8ca532 100644 --- a/src/Widget/Type/Counter.php +++ b/src/Widget/Type/Counter.php @@ -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'); } } diff --git a/src/Widget/Type/DailyWorkingTimeChart.php b/src/Widget/Type/DailyWorkingTimeChart.php index 8efb8414..b33c6c60 100644 --- a/src/Widget/Type/DailyWorkingTimeChart.php +++ b/src/Widget/Type/DailyWorkingTimeChart.php @@ -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, + ]; } } diff --git a/src/Widget/Type/PaginatedWorkingTimeChart.php b/src/Widget/Type/PaginatedWorkingTimeChart.php index 3ee5c658..f5f931f9 100644 --- a/src/Widget/Type/PaginatedWorkingTimeChart.php +++ b/src/Widget/Type/PaginatedWorkingTimeChart.php @@ -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 ), ]; diff --git a/src/Widget/Type/SimpleStatisticChart.php b/src/Widget/Type/SimpleStatisticChart.php new file mode 100644 index 00000000..0354fdea --- /dev/null +++ b/src/Widget/Type/SimpleStatisticChart.php @@ -0,0 +1,101 @@ +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() + ); + } + } +} diff --git a/src/Widget/Type/UserTeamProjects.php b/src/Widget/Type/UserTeamProjects.php index 8e24f665..7c82b080 100644 --- a/src/Widget/Type/UserTeamProjects.php +++ b/src/Widget/Type/UserTeamProjects.php @@ -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); + } } diff --git a/src/Widget/Type/UserTeams.php b/src/Widget/Type/UserTeams.php index 557ac217..2cfd4d19 100644 --- a/src/Widget/Type/UserTeams.php +++ b/src/Widget/Type/UserTeams.php @@ -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); + } } diff --git a/src/Widget/Type/UserWidget.php b/src/Widget/Type/UserWidget.php new file mode 100644 index 00000000..742fc3f3 --- /dev/null +++ b/src/Widget/Type/UserWidget.php @@ -0,0 +1,22 @@ +repository = $repository; } - /** - * @param string $widget - * @return bool - */ public function hasWidget(string $widget): bool { return $this->repository->has($widget); diff --git a/templates/reporting/actions.html.twig b/templates/reporting/actions.html.twig index 5ba21434..2906dd7a 100644 --- a/templates/reporting/actions.html.twig +++ b/templates/reporting/actions.html.twig @@ -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 %} diff --git a/templates/user/form.html.twig b/templates/user/form.html.twig index 3a75585c..9eb24948 100644 --- a/templates/user/form.html.twig +++ b/templates/user/form.html.twig @@ -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 %} diff --git a/templates/widget/paginatedworkingtimechart.html.twig b/templates/widget/paginatedworkingtimechart.html.twig index 83656ec3..801e06f3 100644 --- a/templates/widget/paginatedworkingtimechart.html.twig +++ b/templates/widget/paginatedworkingtimechart.html.twig @@ -1 +1 @@ -{{ render_widget('PaginatedWorkingTimeChart', {'year': year, 'week': week}) }} +{{ render_widget('PaginatedWorkingTimeChart', {'user': user, 'year': year, 'week': week}) }} diff --git a/templates/widget/widget-dailyworkingtimechart.html.twig b/templates/widget/widget-dailyworkingtimechart.html.twig index a2efaa16..252122e1 100644 --- a/templates/widget/widget-dailyworkingtimechart.html.twig +++ b/templates/widget/widget-dailyworkingtimechart.html.twig @@ -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 %}