added reporting screen (#1805)

This commit is contained in:
Kevin Papst
2020-07-12 14:05:14 +02:00
committed by GitHub
parent b97d9f692d
commit 164af7ae02
44 changed files with 1294 additions and 79 deletions

View File

@@ -100,6 +100,7 @@ final class PermissionController extends AbstractController
new PermissionSection('Timesheet', '_timesheet'),
new PermissionSection('Timesheet (other)', '_other_timesheet'),
new PermissionSection('Timesheet (own)', '_own_timesheet'),
new PermissionSection('Reporting', '_reporting'),
];
$event = new PermissionSectionsEvent();

View File

@@ -0,0 +1,218 @@
<?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\Controller;
use App\Model\Statistic\Day;
use App\Reporting\MonthByUser;
use App\Reporting\MonthByUserForm;
use App\Reporting\MonthlyUserList;
use App\Reporting\MonthlyUserListForm;
use App\Repository\Query\UserQuery;
use App\Repository\TimesheetRepository;
use App\Repository\UserRepository;
use App\Timesheet\UserDateTimeFactory;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
/**
* Controller used to render reports.
*
* @Route(path="/reporting")
* @Security("is_granted('view_reporting')")
*/
final class ReportingController extends AbstractController
{
/**
* @var TimesheetRepository
*/
private $timesheetRepository;
/**
* @var UserRepository
*/
private $userRepository;
/**
* @var UserDateTimeFactory
*/
private $dateTimeFactory;
public function __construct(TimesheetRepository $timesheetRepository, UserRepository $userRepository, UserDateTimeFactory $dateTimeFactory)
{
$this->timesheetRepository = $timesheetRepository;
$this->userRepository = $userRepository;
$this->dateTimeFactory = $dateTimeFactory;
}
/**
* @Route(path="/", name="reporting", methods={"GET"})
* @Route(path="/month_by_user", name="report_user_month", methods={"GET","POST"})
*/
public function monthByUser(Request $request)
{
$user = $this->getUser();
$values = new MonthByUser();
$values->setUser($user);
$values->setDate($this->dateTimeFactory->getStartOfMonth());
$form = $this->createForm(MonthByUserForm::class, $values, [
'method' => 'POST',
'include_user' => $this->isGranted('view_other_timesheet') && $user->hasTeamAssignment(),
]);
$form->handleRequest($request);
if ($form->isSubmitted() && !$form->isValid()) {
$values->setUser($user);
$values->setDate($this->dateTimeFactory->getStartOfMonth());
}
if ($user !== $values->getUser() && !$this->isGranted('view_other_timesheet')) {
throw new AccessDeniedException('User is not allowed to see other users timesheet');
}
$start = $values->getDate();
$start->modify('first day of 00:00:00');
$end = clone $start;
$end->modify('last day of 23:59:59');
$selectedUser = $values->getUser();
$previousMonth = clone $start;
$previousMonth->modify('-1 month');
$nextMonth = clone $start;
$nextMonth->modify('+1 month');
$data = $this->timesheetRepository->getDailyStats($selectedUser, $start, $end);
$rows = $this->prepareMonthlyData($data);
return $this->render('reporting/month_by_user.html.twig', [
'form' => $form->createView(),
'days' => $data,
'rows' => $rows,
'user' => $selectedUser,
'current' => $start,
'next' => $nextMonth,
'previous' => $previousMonth,
]);
}
/**
* @Route(path="/monthly_users_list", name="report_monthly_users", methods={"GET","POST"})
* @Security("is_granted('view_other_timesheet')")
*/
public function montlyhUsersList(Request $request)
{
$currentUser = $this->getUser();
$query = new UserQuery();
$query->setCurrentUser($currentUser);
$allUsers = $this->userRepository->getUsersForQuery($query);
$rows = [];
$values = new MonthlyUserList();
$values->setDate($this->dateTimeFactory->getStartOfMonth());
$form = $this->createForm(MonthlyUserListForm::class, $values, [
'method' => 'POST',
]);
$form->handleRequest($request);
if ($form->isSubmitted() && !$form->isValid()) {
$values->setDate($this->dateTimeFactory->getStartOfMonth());
}
$start = $values->getDate();
$start->modify('first day of 00:00:00');
$end = clone $start;
$end->modify('last day of 23:59:59');
$previousMonth = clone $start;
$previousMonth->modify('-1 month');
$nextMonth = clone $start;
$nextMonth->modify('+1 month');
foreach ($allUsers as $user) {
$rows[] = [
'days' => $this->timesheetRepository->getDailyStats($user, $start, $end),
'user' => $user
];
}
$days = [];
if (isset($rows[0])) {
/** @var Day $day */
foreach ($rows[0]['days'] as $day) {
$days[$day->getDay()->format('Ymd')] = $day->getDay();
}
}
return $this->render('reporting/monthly_user_list.html.twig', [
'form' => $form->createView(),
'rows' => $rows,
'days' => $days,
'current' => $start,
'next' => $nextMonth,
'previous' => $previousMonth,
]);
}
private function prepareMonthlyData(array $data): array
{
$days = [];
foreach ($data as $day) {
$days[$day->getDay()->format('Ymd')] = ['date' => $day->getDay(), 'duration' => 0];
}
$rows = [];
/** @var Day $day */
foreach ($data as $day) {
$dayId = $day->getDay()->format('Ymd');
foreach ($day->getDetails() as $id => $detail) {
$projectId = $detail['project']->getId();
if (!\array_key_exists($projectId, $rows)) {
$rows[$projectId] = [
'project' => $detail['project'],
'duration' => 0,
'days' => $days,
'activities' => [],
];
}
$rows[$projectId]['duration'] += $detail['duration'];
$rows[$projectId]['days'][$dayId]['duration'] += $detail['duration'];
$activityId = $detail['activity']->getId();
if (!\array_key_exists($activityId, $rows[$projectId]['activities'])) {
$rows[$projectId]['activities'][$activityId] = [
'activity' => $detail['activity'],
'duration' => 0,
'days' => $days,
];
}
$rows[$projectId]['activities'][$activityId]['duration'] += $detail['duration'];
$rows[$projectId]['activities'][$activityId]['days'][$dayId]['duration'] += $detail['duration'];
}
}
return $rows;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Event;
use App\Entity\User;
use App\Reporting\ReportInterface;
use Symfony\Contracts\EventDispatcher\Event;
final class ReportingEvent extends Event
{
/**
* @var User
*/
private $user;
/**
* @var ReportInterface[]
*/
private $reports = [];
public function __construct(User $user)
{
$this->user = $user;
}
public function getUser(): User
{
return $this->user;
}
public function addReport(ReportInterface $report): ReportingEvent
{
$this->reports[$report->getId()] = $report;
return $this;
}
/**
* @return ReportInterface[]
*/
public function getReports(): array
{
return array_values($this->reports);
}
}

View File

@@ -93,6 +93,12 @@ final class MenuSubscriber implements EventSubscriberInterface
$menu->addChild($timesheets);
}
if ($auth->isGranted('view_reporting')) {
$reporting = new MenuItemModel('reporting', 'menu.reporting', 'reporting', [], $this->getIcon('reporting'));
$reporting->setChildRoutes(['report_user_month', 'report_monthly_users']);
$menu->addChild($reporting);
}
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
$customers = new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], $this->getIcon('customer'));
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);

View File

@@ -52,8 +52,13 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
$view->vars['attr'] = [];
}
$extendedOptions = ['class' => 'selectpicker', 'data-width' => '100%'];
if (!$options['search']) {
$extendedOptions = ['class' => 'selectpicker'];
if (false !== $options['width']) {
$extendedOptions['data-width'] = $options['width'];
}
if (false === $options['search']) {
$extendedOptions['data-minimum-results-for-search'] = 'Infinity';
}
@@ -69,6 +74,10 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
$resolver->setAllowedTypes('selectpicker', 'boolean');
$resolver->setDefault('selectpicker', true);
$resolver->setDefined(['width']);
$resolver->setAllowedTypes('width', ['string', 'boolean']);
$resolver->setDefault('width', '100%');
$resolver->setDefined(['search']);
$resolver->setAllowedTypes('search', 'boolean');
$resolver->setDefault('search', true);

View File

@@ -0,0 +1,90 @@
<?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\Form\Type;
use App\Timesheet\UserDateTimeFactory;
use App\Utils\LocaleSettings;
use App\Utils\MomentFormatConverter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a month via picker and select previous and next month.
*/
final class MonthPickerType extends AbstractType
{
/**
* @var LocaleSettings
*/
private $localeSettings;
/**
* @var UserDateTimeFactory
*/
private $dateTime;
public function __construct(LocaleSettings $localeSettings, UserDateTimeFactory $dateTime)
{
$this->localeSettings = $localeSettings;
$this->dateTime = $dateTime;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$pickerFormat = $this->localeSettings->getDatePickerFormat();
$dateFormat = $this->localeSettings->getDateTypeFormat();
$timezone = $this->dateTime->getTimezone()->getName();
$resolver->setDefaults([
'widget' => 'single_text',
'html5' => false,
'format' => $dateFormat,
'format_picker' => $pickerFormat,
'model_timezone' => $timezone,
'view_timezone' => $timezone,
]);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
/** @var \DateTime|null $date */
$date = $form->getData();
if (null === $date) {
$date = $this->dateTime->getStartOfMonth();
}
$view->vars['previousMonth'] = (clone $date)->modify('-1 month');
$view->vars['nextMonth'] = (clone $date)->modify('+1 month');
$view->vars['momentFormat'] = (new MomentFormatConverter())->convert($this->localeSettings->getDateTypeFormat());
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return DateType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'monthpicker';
}
}

View File

@@ -25,6 +25,10 @@ class Day
* @var DateTime
*/
protected $day;
/**
* @var array
*/
protected $details = [];
public function __construct(DateTime $day, int $duration, float $rate)
{
@@ -61,4 +65,16 @@ class Day
return $this;
}
public function setDetails(array $details): Day
{
$this->details = $details;
return $this;
}
public function getDetails(): array
{
return $this->details;
}
}

View 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\Reporting;
use App\Entity\User;
final class MonthByUser
{
/**
* @var User
*/
private $user;
/**
* @var \DateTime
*/
private $date;
public function getUser(): ?User
{
return $this->user;
}
public function setUser(User $user): MonthByUser
{
$this->user = $user;
return $this;
}
public function getDate(): ?\DateTime
{
return $this->date;
}
public function setDate(\DateTime $date): MonthByUser
{
$this->date = $date;
return $this;
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Reporting;
use App\Form\Type\MonthPickerType;
use App\Form\Type\UserType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MonthByUserForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('date', MonthPickerType::class);
if ($options['include_user']) {
$builder->add('user', UserType::class, ['width' => false]);
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => MonthByUser::class,
'include_user' => false,
]);
}
}

View File

@@ -0,0 +1,30 @@
<?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\Reporting;
final class MonthlyUserList
{
/**
* @var \DateTime
*/
private $date;
public function getDate(): ?\DateTime
{
return $this->date;
}
public function setDate(\DateTime $date): MonthlyUserList
{
$this->date = $date;
return $this;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Reporting;
use App\Form\Type\MonthPickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MonthlyUserListForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('date', MonthPickerType::class);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => MonthlyUserList::class,
]);
}
}

48
src/Reporting/Report.php Normal file
View 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\Reporting;
final class Report implements ReportInterface
{
/**
* @var string
*/
private $id;
/**
* @var string
*/
private $label;
/**
* @var string
*/
private $route;
public function __construct(string $id, string $route, string $label)
{
$this->id = $id;
$this->route = $route;
$this->label = $label;
}
public function getRoute(): string
{
return $this->route;
}
public function getId(): string
{
return $this->id;
}
public function getLabel(): string
{
return $this->label;
}
}

View File

@@ -0,0 +1,19 @@
<?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\Reporting;
interface ReportInterface
{
public function getId(): string;
public function getLabel(): string;
public function getRoute(): string;
}

View File

@@ -356,6 +356,9 @@ class TimesheetRepository extends EntityRepository
}
/**
* In case this method is called with one timezone and the results are from another timezone,
* it might return rows outside the time-range.
*
* @param DateTime $begin
* @param DateTime $end
* @param User|null $user
@@ -363,73 +366,101 @@ class TimesheetRepository extends EntityRepository
*/
protected function getDailyData(DateTime $begin, DateTime $end, ?User $user = null)
{
$query = new TimesheetQuery();
$query
->setBegin($begin)
->setEnd($end)
->setUser($user)
->setState(TimesheetQuery::STATE_STOPPED)
$qb = $this->getEntityManager()->createQueryBuilder();
$or = $qb->expr()->orX();
$or->add($qb->expr()->between(':begin', 't.begin', 't.end'));
$or->add($qb->expr()->between(':end', 't.begin', 't.end'));
$or->add($qb->expr()->between('t.begin', ':begin', ':end'));
$or->add($qb->expr()->between('t.end', ':begin', ':end'));
$qb->select('t, p, a, c')
->from(Timesheet::class, 't')
->andWhere($qb->expr()->isNotNull('t.end'))
->andWhere($or)
->orderBy('t.begin', 'DESC')
->setParameter('begin', $begin)
->setParameter('end', $end)
->leftJoin('t.activity', 'a')
->leftJoin('t.project', 'p')
->leftJoin('p.customer', 'c')
;
$timesheets = $this->getTimesheetsForQuery($query);
if (null !== $user) {
$qb
->andWhere($qb->expr()->eq('t.user', ':user'))
->setParameter('user', $user)
;
}
$timesheets = $qb->getQuery()->getResult();
$results = [];
/** @var Timesheet $result */
foreach ($timesheets as $result) {
$timezone = new \DateTimeZone($result->getTimezone());
/** @var \DateTime $beginTmp */
$beginTmp = $result->getBegin();
$beginTmp->setTimezone($timezone);
/** @var DateTime $endTmp */
$endTmp = $result->getEnd();
$endTmp->setTimezone($timezone);
$dateKeyEnd = $endTmp->format('Ymd');
do {
$dateKey = $beginTmp->format('Ymd');
if (!isset($results[$dateKey])) {
$results[$dateKey] = [
'rate' => 0,
'duration' => 0,
'month' => $beginTmp->format('n'),
'year' => $beginTmp->format('Y'),
'day' => $beginTmp->format('j'),
'details' => []
];
}
if ($dateKey !== $dateKeyEnd) {
$newDateBegin = clone $beginTmp;
$newDateBegin->add(new \DateInterval('P1D'));
// overlapping records should always start at midnight
$newDateBegin->setTime(0, 0, 0);
} else {
$newDateBegin = clone $endTmp;
}
$duration = $newDateBegin->getTimestamp() - $beginTmp->getTimestamp();
$durationPercent = 0;
if ($result->getDuration() !== null && $result->getDuration() > 0) {
$durationPercent = $duration / $result->getDuration();
}
$rate = $result->getRate() * $durationPercent;
// make sure to exclude entries that are outside the requested timerange:
// 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
// --------------------------------------------------------------------------------------
// Be aware that this will NOT filter every record, in case there is a timezone mismatch between the
// begin/end dates and the ones from the database (eg. recorded in UTC) - which might actually be
// before $begin (which happens thanks to the timezone conversion when querying the database)
if ($newDateBegin > $begin && $beginTmp < $end) {
if (!isset($results[$dateKey])) {
$results[$dateKey] = [
'rate' => 0,
'duration' => 0,
'month' => $beginTmp->format('n'),
'year' => $beginTmp->format('Y'),
'day' => $beginTmp->format('j'),
'details' => []
];
}
$duration = $newDateBegin->getTimestamp() - $beginTmp->getTimestamp();
$durationPercent = 0;
if ($result->getDuration() !== null && $result->getDuration() > 0) {
$durationPercent = $duration / $result->getDuration();
}
$rate = $result->getRate() * $durationPercent;
$results[$dateKey]['rate'] += $rate;
$results[$dateKey]['duration'] += $duration;
$detailsId = $result->getProject()->getCustomer()->getId() . '_' . $result->getProject()->getId();
if (!isset($results[$dateKey]['details'][$detailsId])) {
$results[$dateKey]['details'][$detailsId] = [
'project' => $result->getProject(),
'activity' => $result->getActivity(),
'duration' => 0,
'rate' => 0,
];
$results[$dateKey]['rate'] += $rate;
$results[$dateKey]['duration'] += $duration;
$detailsId = $result->getProject()->getCustomer()->getId() . '_' . $result->getProject()->getId();
if (!isset($results[$dateKey]['details'][$detailsId])) {
$results[$dateKey]['details'][$detailsId] = [
'project' => $result->getProject(),
'activity' => $result->getActivity(),
'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;
// yes, we only want to compare the day, not the time
if ((int) $end->format('Ymd') < (int) $newDateBegin->format('Ymd')) {
break 1;
}
@@ -447,13 +478,13 @@ class TimesheetRepository extends EntityRepository
}
/**
* @param User $user
* @param User|null $user
* @param DateTime $begin
* @param DateTime $end
* @return Day[]
* @throws \Exception
*/
public function getDailyStats(User $user, DateTime $begin, DateTime $end): array
public function getDailyStats(?User $user, DateTime $begin, DateTime $end): array
{
/** @var Day[] $days */
$days = [];
@@ -474,7 +505,13 @@ class TimesheetRepository extends EntityRepository
$dateTime->setDate($statRow['year'], $statRow['month'], $statRow['day']);
$dateTime->setTime(0, 0, 0);
$day = new Day($dateTime, (int) $statRow['duration'], (float) $statRow['rate']);
$days[$dateTime->format('Ymd')] = $day;
$day->setDetails($statRow['details']);
$dateKey = $dateTime->format('Ymd');
// make sure entries from other timezones are filtered
if (!\array_key_exists($dateKey, $days)) {
continue;
}
$days[$dateKey] = $day;
}
ksort($days);
@@ -677,7 +714,7 @@ class TimesheetRepository extends EntityRepository
if (empty($user) && null !== $query->getCurrentUser()) {
$currentUser = $query->getCurrentUser();
if (!$currentUser->isSuperAdmin() && !$currentUser->isAdmin()) {
if (!$currentUser->canSeeAllData()) {
// make sure that the user himself is in the list of users, if he is part of a team
// if teams are used and the user is not a teamlead, the list of users would be empty and then leading to NOT limit the select by user IDs
$user[] = $currentUser;
@@ -955,7 +992,7 @@ class TimesheetRepository extends EntityRepository
$qb->setParameter('end', $timesheet->getEnd());
}
$qb->select('t')
$qb->select($qb->expr()->count('t.id'))
->from(Timesheet::class, 't')
->andWhere($qb->expr()->eq('t.user', ':user'))
->andWhere($qb->expr()->isNotNull('t.end'))
@@ -964,8 +1001,12 @@ class TimesheetRepository extends EntityRepository
->setParameter('user', $timesheet->getUser())
;
$result = $qb->getQuery()->getResult();
try {
$result = (int) $qb->getQuery()->getSingleScalarResult();
} catch (\Exception $ex) {
return true;
}
return !empty($result);
return $result > 0;
}
}

View File

@@ -17,22 +17,30 @@ class UserDateTimeFactory
/**
* @var \DateTimeZone
*/
protected $timezone;
private $timezone;
/**
* @var CurrentUser
*/
private $user;
public function __construct(CurrentUser $user)
{
$timezone = date_default_timezone_get();
$user = $user->getUser();
if ($user instanceof User) {
$timezone = $user->getTimezone();
}
$this->timezone = new \DateTimeZone($timezone);
$this->user = $user;
}
public function getTimezone(): \DateTimeZone
{
if (null === $this->timezone) {
$timezone = date_default_timezone_get();
$user = $this->user->getUser();
if ($user instanceof User) {
$timezone = $user->getTimezone();
}
$this->timezone = new \DateTimeZone($timezone);
}
return $this->timezone;
}
@@ -54,7 +62,7 @@ class UserDateTimeFactory
public function createDateTime(string $datetime = 'now'): \DateTime
{
$date = new \DateTime($datetime, $this->timezone);
$date = new \DateTime($datetime, $this->getTimezone());
return $date;
}
@@ -66,7 +74,7 @@ class UserDateTimeFactory
*/
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now')
{
$date = \DateTime::createFromFormat($format, $datetime, $this->timezone);
$date = \DateTime::createFromFormat($format, $datetime, $this->getTimezone());
return $date;
}

View File

@@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
/**
* Date specific twig extensions
@@ -86,6 +87,20 @@ class DateExtensions extends AbstractExtension
];
}
public function getTests()
{
return [
new TwigTest('weekend', function ($dateTime) {
if (!$dateTime instanceof \DateTime) {
return false;
}
$day = (int) $dateTime->format('w');
return ($day === 0 || $day === 6);
}),
];
}
/**
* {@inheritdoc}
*/
@@ -217,6 +232,7 @@ class DateExtensions extends AbstractExtension
public function monthName(\DateTime $dateTime): string
{
// @see http://userguide.icu-project.org/formatparse/datetime
$formatter = new \IntlDateFormatter(
$this->locale,
\IntlDateFormatter::FULL,
@@ -229,15 +245,16 @@ class DateExtensions extends AbstractExtension
return $formatter->format($dateTime);
}
public function dayName(\DateTime $dateTime): string
public function dayName(\DateTime $dateTime, bool $short = false): string
{
// @see http://userguide.icu-project.org/formatparse/datetime
$formatter = new \IntlDateFormatter(
$this->locale,
\IntlDateFormatter::FULL,
\IntlDateFormatter::FULL,
$dateTime->getTimezone()->getName(),
\IntlDateFormatter::GREGORIAN,
'EEEE'
$short ? 'EE' : 'EEEE'
);
return $formatter->format($dateTime);

View File

@@ -71,6 +71,7 @@ final class IconExtension extends AbstractExtension
'profile-stats' => 'far fa-chart-bar',
'project' => 'fas fa-briefcase',
'repeat' => 'fas fa-redo-alt',
'reporting' => 'far fa-chart-bar',
'right' => 'fas fa-chevron-right',
'roles' => 'fas fa-user-shield',
'search' => 'fas fa-search',

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig;
use App\Entity\User;
use App\Event\ReportingEvent;
use App\Reporting\Report;
use App\Reporting\ReportInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
final class ReportingExtension extends AbstractExtension
{
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* @var AuthorizationCheckerInterface
*/
private $security;
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $security)
{
$this->dispatcher = $dispatcher;
$this->security = $security;
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return [
new TwigFunction('available_reports', [$this, 'getAvailableReports'], []),
];
}
/**
* @param User $user
* @return ReportInterface[]
*/
public function getAvailableReports(User $user): array
{
$event = new ReportingEvent($user);
if ($this->security->isGranted('view_reporting')) {
$event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month'));
if ($this->security->isGranted('view_other_timesheet')) {
$event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users'));
}
}
$this->dispatcher->dispatch($event);
return $event->getReports();
}
}