Weekly reporting view (#1892)
This commit is contained in:
@@ -42,4 +42,8 @@ class Constants
|
||||
* Application wide default locale.
|
||||
*/
|
||||
public const DEFAULT_LOCALE = 'en';
|
||||
/**
|
||||
* Default color for Customer, Project and Activity entities
|
||||
*/
|
||||
public const DEFAULT_COLOR = '#d2d6de';
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\LanguageFormattings;
|
||||
use App\Entity\User;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use App\Utils\LocaleFormats;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
|
||||
use Symfony\Component\Translation\DataCollectorTranslator;
|
||||
@@ -108,7 +111,22 @@ abstract class AbstractController extends BaseAbstractController implements Serv
|
||||
{
|
||||
return array_merge(parent::getSubscribedServices(), [
|
||||
'translator' => TranslatorInterface::class,
|
||||
'logger' => LoggerInterface::class
|
||||
'logger' => LoggerInterface::class,
|
||||
LanguageFormattings::class => LanguageFormattings::class,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getDateTimeFactory(?User $user = null): DateTimeFactory
|
||||
{
|
||||
if (null === $user) {
|
||||
$user = $this->getUser();
|
||||
}
|
||||
|
||||
return new DateTimeFactory(new \DateTimeZone($user->getTimezone()));
|
||||
}
|
||||
|
||||
protected function getLocaleFormats(string $locale): LocaleFormats
|
||||
{
|
||||
return new LocaleFormats($this->container->get(LanguageFormattings::class), $locale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,15 @@ use App\Reporting\MonthByUser;
|
||||
use App\Reporting\MonthByUserForm;
|
||||
use App\Reporting\MonthlyUserList;
|
||||
use App\Reporting\MonthlyUserListForm;
|
||||
use App\Reporting\WeekByUser;
|
||||
use App\Reporting\WeekByUserForm;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Exception;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
@@ -39,46 +42,79 @@ final class ReportingController extends AbstractController
|
||||
* @var UserRepository
|
||||
*/
|
||||
private $userRepository;
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
private $dateTimeFactory;
|
||||
|
||||
public function __construct(TimesheetRepository $timesheetRepository, UserRepository $userRepository, UserDateTimeFactory $dateTimeFactory)
|
||||
public function __construct(TimesheetRepository $timesheetRepository, UserRepository $userRepository)
|
||||
{
|
||||
$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"})
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function monthByUser(Request $request)
|
||||
public function defaultReport(): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
return $this->redirectToRoute('report_user_week');
|
||||
}
|
||||
|
||||
private function canSelectUser(): bool
|
||||
{
|
||||
if (!$this->isGranted('view_other_timesheet')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentUser = $this->getUser();
|
||||
|
||||
if ($currentUser->canSeeAllData()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($currentUser->hasTeamAssignment()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/month_by_user", name="report_user_month", methods={"GET","POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
public function monthByUser(Request $request): Response
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$localeFormats = $this->getLocaleFormats($request->getLocale());
|
||||
$canChangeUser = $this->canSelectUser();
|
||||
|
||||
$values = new MonthByUser();
|
||||
$values->setUser($user);
|
||||
$values->setDate($this->dateTimeFactory->getStartOfMonth());
|
||||
$values->setUser($currentUser);
|
||||
$values->setDate($dateTimeFactory->getStartOfMonth());
|
||||
|
||||
$form = $this->createForm(MonthByUserForm::class, $values, [
|
||||
'include_user' => $this->isGranted('view_other_timesheet') && $user->hasTeamAssignment(),
|
||||
'include_user' => $canChangeUser,
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
'format' => $localeFormats->getDateTypeFormat(),
|
||||
]);
|
||||
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($values->getUser() === null) {
|
||||
$values->setUser($user);
|
||||
$values->setUser($currentUser);
|
||||
}
|
||||
|
||||
if ($user !== $values->getUser() && !$this->isGranted('view_other_timesheet')) {
|
||||
if ($currentUser !== $values->getUser() && !$canChangeUser) {
|
||||
throw new AccessDeniedException('User is not allowed to see other users timesheet');
|
||||
}
|
||||
|
||||
if ($values->getDate() === null) {
|
||||
$values->setDate($this->dateTimeFactory->getStartOfMonth());
|
||||
$values->setDate($dateTimeFactory->getStartOfMonth());
|
||||
}
|
||||
|
||||
$start = $values->getDate();
|
||||
@@ -110,12 +146,82 @@ final class ReportingController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/monthly_users_list", name="report_monthly_users", methods={"GET","POST"})
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
* @Route(path="/week_by_user", name="report_user_week", methods={"GET","POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
public function montlyhUsersList(Request $request)
|
||||
public function weekByUser(Request $request): Response
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$localeFormats = $this->getLocaleFormats($request->getLocale());
|
||||
$canChangeUser = $this->canSelectUser();
|
||||
|
||||
$values = new WeekByUser();
|
||||
$values->setUser($currentUser);
|
||||
$values->setDate($dateTimeFactory->getStartOfWeek());
|
||||
|
||||
$form = $this->createForm(WeekByUserForm::class, $values, [
|
||||
'include_user' => $canChangeUser,
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
'format' => $localeFormats->getDateTypeFormat(),
|
||||
]);
|
||||
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($values->getUser() === null) {
|
||||
$values->setUser($currentUser);
|
||||
}
|
||||
|
||||
if ($currentUser !== $values->getUser() && !$canChangeUser) {
|
||||
throw new AccessDeniedException('User is not allowed to see other users timesheet');
|
||||
}
|
||||
|
||||
if ($values->getDate() === null) {
|
||||
$values->setDate($dateTimeFactory->getStartOfWeek());
|
||||
}
|
||||
|
||||
$start = $dateTimeFactory->getStartOfWeek($values->getDate());
|
||||
$end = $dateTimeFactory->getEndOfWeek($values->getDate());
|
||||
|
||||
$selectedUser = $values->getUser();
|
||||
|
||||
$previous = clone $start;
|
||||
$previous->modify('-1 week');
|
||||
|
||||
$next = clone $start;
|
||||
$next->modify('+1 week');
|
||||
|
||||
$data = $this->timesheetRepository->getDailyStats($selectedUser, $start, $end);
|
||||
$rows = $this->prepareMonthlyData($data);
|
||||
|
||||
return $this->render('reporting/week_by_user.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'days' => $data,
|
||||
'rows' => $rows,
|
||||
'user' => $selectedUser,
|
||||
'current' => $start,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/monthly_users_list", name="report_monthly_users", methods={"GET","POST"})
|
||||
* @Security("is_granted('view_other_timesheet')")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws Exception
|
||||
*/
|
||||
public function monthlyUsersList(Request $request): Response
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory();
|
||||
$localeFormats = $this->getLocaleFormats($request->getLocale());
|
||||
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($currentUser);
|
||||
@@ -124,18 +230,22 @@ final class ReportingController extends AbstractController
|
||||
$rows = [];
|
||||
|
||||
$values = new MonthlyUserList();
|
||||
$values->setDate($this->dateTimeFactory->getStartOfMonth());
|
||||
$values->setDate($dateTimeFactory->getStartOfMonth());
|
||||
|
||||
$form = $this->createForm(MonthlyUserListForm::class, $values, []);
|
||||
$form = $this->createForm(MonthlyUserListForm::class, $values, [
|
||||
'timezone' => $dateTimeFactory->getTimezone()->getName(),
|
||||
'start_date' => $values->getDate(),
|
||||
'format' => $localeFormats->getDateTypeFormat(),
|
||||
]);
|
||||
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if ($form->isSubmitted() && !$form->isValid()) {
|
||||
$values->setDate($this->dateTimeFactory->getStartOfMonth());
|
||||
$values->setDate($dateTimeFactory->getStartOfMonth());
|
||||
}
|
||||
|
||||
if ($values->getDate() === null) {
|
||||
$values->setDate($this->dateTimeFactory->getStartOfMonth());
|
||||
$values->setDate($dateTimeFactory->getStartOfMonth());
|
||||
}
|
||||
|
||||
$start = $values->getDate();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Constants;
|
||||
use App\Export\Annotation as Exporter;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
@@ -36,6 +37,10 @@ trait ColorTrait
|
||||
*/
|
||||
public function getColor(): ?string
|
||||
{
|
||||
if ($this->color === Constants::DEFAULT_COLOR) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Constants;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ColorType;
|
||||
@@ -17,7 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ColorPickerType extends AbstractType implements DataTransformerInterface
|
||||
{
|
||||
public const DEFAULT_COLOR = '#d2d6de';
|
||||
public const DEFAULT_COLOR = Constants::DEFAULT_COLOR;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
|
||||
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;
|
||||
@@ -25,38 +23,16 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
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,
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'start_date' => new \DateTime(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -66,13 +42,13 @@ final class MonthPickerType extends AbstractType
|
||||
$date = $form->getData();
|
||||
|
||||
if (null === $date) {
|
||||
$date = $this->dateTime->getStartOfMonth();
|
||||
$date = $options['start_date'];
|
||||
}
|
||||
|
||||
$view->vars['month'] = $date;
|
||||
$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());
|
||||
$view->vars['momentFormat'] = (new MomentFormatConverter())->convert($options['format']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
69
src/Form/Type/WeekPickerType.php
Normal file
69
src/Form/Type/WeekPickerType.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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\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 week via picker and select previous and next week.
|
||||
*
|
||||
* Always falls back to the current week if none or an invalid date is given.
|
||||
*/
|
||||
final class WeekPickerType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'widget' => 'single_text',
|
||||
'html5' => false,
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'start_date' => new \DateTime(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
/** @var \DateTime|null $date */
|
||||
$date = $form->getData();
|
||||
|
||||
if (null === $date) {
|
||||
$date = $options['start_date'];
|
||||
}
|
||||
|
||||
$view->vars['week'] = $date;
|
||||
$view->vars['previousWeek'] = (clone $date)->modify('-1 week');
|
||||
$view->vars['nextWeek'] = (clone $date)->modify('+1 week');
|
||||
$view->vars['momentFormat'] = (new MomentFormatConverter())->convert($options['format']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return DateType::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'weekpicker';
|
||||
}
|
||||
}
|
||||
48
src/Reporting/DateByUser.php
Normal file
48
src/Reporting/DateByUser.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\Reporting;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
abstract class DateByUser
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
private $date;
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDate(): ?\DateTime
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function setDate(\DateTime $date): self
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -9,40 +9,6 @@
|
||||
|
||||
namespace App\Reporting;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
final class MonthByUser
|
||||
final class MonthByUser extends DateByUser
|
||||
{
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Reporting;
|
||||
use App\Form\Type\MonthPickerType;
|
||||
use App\Form\Type\UserType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -32,7 +33,12 @@ class MonthByUserForm extends AbstractType
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('date', MonthPickerType::class);
|
||||
$builder->add('date', MonthPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
'format' => $options['format'],
|
||||
]);
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class, ['width' => false]);
|
||||
@@ -46,6 +52,9 @@ class MonthByUserForm extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => MonthByUser::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'include_user' => false,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Reporting;
|
||||
|
||||
use App\Form\Type\MonthPickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -31,7 +32,12 @@ class MonthlyUserListForm extends AbstractType
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('date', MonthPickerType::class);
|
||||
$builder->add('date', MonthPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
'format' => $options['format'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,6 +47,9 @@ class MonthlyUserListForm extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => MonthlyUserList::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
|
||||
14
src/Reporting/WeekByUser.php
Normal file
14
src/Reporting/WeekByUser.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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 WeekByUser extends DateByUser
|
||||
{
|
||||
}
|
||||
63
src/Reporting/WeekByUserForm.php
Normal file
63
src/Reporting/WeekByUserForm.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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\UserType;
|
||||
use App\Form\Type\WeekPickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class WeekByUserForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('date', WeekPickerType::class, [
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
'start_date' => $options['start_date'],
|
||||
'format' => $options['format'],
|
||||
]);
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class, ['width' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => WeekByUser::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'start_date' => new \DateTime(),
|
||||
'format' => DateType::HTML5_FORMAT,
|
||||
'include_user' => false,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -444,7 +444,11 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
$results[$dateKey]['rate'] += $rate;
|
||||
$results[$dateKey]['duration'] += $duration;
|
||||
$detailsId = $result->getProject()->getCustomer()->getId() . '_' . $result->getProject()->getId();
|
||||
$detailsId =
|
||||
$result->getProject()->getCustomer()->getId()
|
||||
. '_' . $result->getProject()->getId()
|
||||
. '_' . $result->getActivity()->getId()
|
||||
;
|
||||
if (!isset($results[$dateKey]['details'][$detailsId])) {
|
||||
$results[$dateKey]['details'][$detailsId] = [
|
||||
'project' => $result->getProject(),
|
||||
|
||||
101
src/Timesheet/DateTimeFactory.php
Normal file
101
src/Timesheet/DateTimeFactory.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\Timesheet;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
|
||||
class DateTimeFactory
|
||||
{
|
||||
/**
|
||||
* @var DateTimeZone
|
||||
*/
|
||||
private $timezone;
|
||||
|
||||
public function __construct(?DateTimeZone $timezone = null)
|
||||
{
|
||||
if (null === $timezone) {
|
||||
$timezone = new \DateTimeZone(date_default_timezone_get());
|
||||
}
|
||||
$this->setTimezone($timezone);
|
||||
}
|
||||
|
||||
public function setTimezone(DateTimeZone $timezone)
|
||||
{
|
||||
$this->timezone = $timezone;
|
||||
}
|
||||
|
||||
public function getTimezone(): DateTimeZone
|
||||
{
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
public function getStartOfMonth(): DateTime
|
||||
{
|
||||
$date = $this->createDateTime('first day of this month');
|
||||
$date->setTime(0, 0, 0);
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function getStartOfWeek(?DateTime $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime('now');
|
||||
}
|
||||
|
||||
return $this->createWeekDateTime($date->format('Y'), $date->format('W'), 1, 0, 0, 0);
|
||||
}
|
||||
|
||||
public function getEndOfWeek(?DateTime $date = null): DateTime
|
||||
{
|
||||
if (null === $date) {
|
||||
$date = $this->createDateTime('now');
|
||||
}
|
||||
|
||||
return $this->createWeekDateTime($date->format('Y'), $date->format('W'), 7, 23, 59, 59);
|
||||
}
|
||||
|
||||
public function getEndOfMonth(): DateTime
|
||||
{
|
||||
$date = $this->createDateTime('last day of this month');
|
||||
$date->setTime(23, 59, 59);
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
private function createWeekDateTime($year, $week, $day, $hour, $minute, $second)
|
||||
{
|
||||
$date = new DateTime('now', $this->getTimezone());
|
||||
$date->setISODate($year, $week, $day);
|
||||
$date->setTime($hour, $minute, $second);
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function createDateTime(string $datetime = 'now'): DateTime
|
||||
{
|
||||
$date = new DateTime($datetime, $this->getTimezone());
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $format
|
||||
* @param null|string $datetime
|
||||
* @return bool|DateTime
|
||||
*/
|
||||
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now')
|
||||
{
|
||||
$date = DateTime::createFromFormat($format, $datetime, $this->getTimezone());
|
||||
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
@@ -11,26 +11,31 @@ namespace App\Timesheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Security\CurrentUser;
|
||||
use DateTimeZone;
|
||||
|
||||
class UserDateTimeFactory
|
||||
/**
|
||||
* @internal use DateTimeFactory instead: this one relies on the global context and will be deprecated in the future
|
||||
*/
|
||||
class UserDateTimeFactory extends DateTimeFactory
|
||||
{
|
||||
/**
|
||||
* @var \DateTimeZone
|
||||
*/
|
||||
private $timezone;
|
||||
/**
|
||||
* @var CurrentUser
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $initializedFromUser = false;
|
||||
|
||||
public function __construct(CurrentUser $user)
|
||||
{
|
||||
parent::__construct(null);
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getTimezone(): \DateTimeZone
|
||||
public function getTimezone(): DateTimeZone
|
||||
{
|
||||
if (null === $this->timezone) {
|
||||
if ($this->initializedFromUser === false) {
|
||||
$timezone = date_default_timezone_get();
|
||||
|
||||
$user = $this->user->getUser();
|
||||
@@ -38,44 +43,12 @@ class UserDateTimeFactory
|
||||
$timezone = $user->getTimezone();
|
||||
}
|
||||
|
||||
$this->timezone = new \DateTimeZone($timezone);
|
||||
$timezone = new DateTimeZone($timezone);
|
||||
|
||||
parent::setTimezone($timezone);
|
||||
$this->initializedFromUser = true;
|
||||
}
|
||||
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
public function getStartOfMonth(): \DateTime
|
||||
{
|
||||
$date = $this->createDateTime('first day of this month');
|
||||
$date->setTime(0, 0, 0);
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function getEndOfMonth(): \DateTime
|
||||
{
|
||||
$date = $this->createDateTime('last day of this month');
|
||||
$date->setTime(23, 59, 59);
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function createDateTime(string $datetime = 'now'): \DateTime
|
||||
{
|
||||
$date = new \DateTime($datetime, $this->getTimezone());
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $format
|
||||
* @param null|string $datetime
|
||||
* @return bool|\DateTime
|
||||
*/
|
||||
public function createDateTimeFromFormat(string $format, ?string $datetime = 'now')
|
||||
{
|
||||
$date = \DateTime::createFromFormat($format, $datetime, $this->getTimezone());
|
||||
|
||||
return $date;
|
||||
return parent::getTimezone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,34 +230,36 @@ class DateExtensions extends AbstractExtension
|
||||
return $date->format($this->timeFormat);
|
||||
}
|
||||
|
||||
public function monthName(\DateTime $dateTime): string
|
||||
/**
|
||||
* @see https://framework.zend.com/manual/1.12/en/zend.date.constants.html#zend.date.constants.selfdefinedformats
|
||||
* @see http://userguide.icu-project.org/formatparse/datetime
|
||||
*
|
||||
* @param DateTime $dateTime
|
||||
* @param string $format
|
||||
* @return string
|
||||
*/
|
||||
private function formatIntl(\DateTime $dateTime, string $format): string
|
||||
{
|
||||
// @see http://userguide.icu-project.org/formatparse/datetime
|
||||
$formatter = new \IntlDateFormatter(
|
||||
$this->locale,
|
||||
\IntlDateFormatter::FULL,
|
||||
\IntlDateFormatter::FULL,
|
||||
$dateTime->getTimezone()->getName(),
|
||||
\IntlDateFormatter::GREGORIAN,
|
||||
'LLLL'
|
||||
$format
|
||||
);
|
||||
|
||||
return $formatter->format($dateTime);
|
||||
}
|
||||
|
||||
public function monthName(\DateTime $dateTime, bool $withYear = false): string
|
||||
{
|
||||
return $this->formatIntl($dateTime, ($withYear ? 'LLLL yyyy' : 'LLLL'));
|
||||
}
|
||||
|
||||
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,
|
||||
$short ? 'EE' : 'EEEE'
|
||||
);
|
||||
|
||||
return $formatter->format($dateTime);
|
||||
return $this->formatIntl($dateTime, ($short ? 'EE' : 'EEEE'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\EntityWithMetaFields;
|
||||
use App\Entity\Project;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
@@ -27,6 +31,7 @@ class Extensions extends AbstractExtension
|
||||
return [
|
||||
new TwigFilter('docu_link', [$this, 'documentationLink']),
|
||||
new TwigFilter('multiline_indent', [$this, 'multilineIndent']),
|
||||
new TwigFilter('color', [$this, 'color']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -40,6 +45,34 @@ class Extensions extends AbstractExtension
|
||||
];
|
||||
}
|
||||
|
||||
public function color(EntityWithMetaFields $entity): ?string
|
||||
{
|
||||
if ($entity instanceof Activity) {
|
||||
if (!empty($entity->getColor())) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
|
||||
if (null !== $entity->getProject()) {
|
||||
$entity = $entity->getProject();
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity instanceof Project) {
|
||||
if (!empty($entity->getColor())) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
$entity = $entity->getCustomer();
|
||||
}
|
||||
|
||||
if ($entity instanceof Customer) {
|
||||
if (!empty($entity->getColor())) {
|
||||
return $entity->getColor();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $object
|
||||
* @return null|string
|
||||
|
||||
@@ -54,6 +54,7 @@ final class ReportingExtension extends AbstractExtension
|
||||
$event = new ReportingEvent($user);
|
||||
|
||||
if ($this->security->isGranted('view_reporting')) {
|
||||
$event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week'));
|
||||
$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'));
|
||||
|
||||
@@ -48,26 +48,30 @@
|
||||
|
||||
{% block monthpicker_widget -%}
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{previousMonth|date_short}}').change()">
|
||||
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousMonth|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ previousMonth|month_name(true) }}">
|
||||
<i class="{{ 'left'|icon }}"></i>
|
||||
</a>
|
||||
<a class="btn btn-default" href="#" onclick="return false;">
|
||||
<span id="{{ form.vars.id }}_month_name">{{ month|month_name|trans ~ ' ' ~ month|date_format('Y') }}</span>
|
||||
<span id="{{ form.vars.id }}_month_name">{{ month|month_name(true) }}</span>
|
||||
</a>
|
||||
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{nextMonth|date_short}}').change()">
|
||||
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextMonth|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ nextMonth|month_name(true) }}">
|
||||
<i class="{{ 'right'|icon }}"></i>
|
||||
</a>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
$('#{{ form.vars.id }}').on('change', function(ev) {
|
||||
var newDate = moment($(this).val(), '{{ momentFormat }}').format('MMMM YYYY');
|
||||
$('#{{ form.vars.id }}_month_name').html(
|
||||
newDate[0].toUpperCase() + newDate.slice(1)
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% set type = 'hidden' %}
|
||||
{{ block('form_widget_simple') }}
|
||||
{{ block('hidden_widget') }}
|
||||
{%- endblock monthpicker_widget %}
|
||||
|
||||
{% block weekpicker_widget -%}
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-default btn-left" href="#" onclick="$('#{{ form.vars.id }}').val('{{ previousWeek|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': previousWeek|date_format('W')}) }}">
|
||||
<i class="{{ 'left'|icon }}"></i>
|
||||
</a>
|
||||
<a class="btn btn-default" href="#" onclick="return false;">
|
||||
<span id="{{ form.vars.id }}_week_number">{{ 'stats.workingTimeWeek'|trans({'%week%': week|date_format('W')}) }}</span>
|
||||
</a>
|
||||
<a class="btn btn-default btn-right" href="#" onclick="$('#{{ form.vars.id }}').val('{{ nextWeek|date_short }}').change()" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': nextWeek|date_format('W')}) }}">
|
||||
<i class="{{ 'right'|icon }}"></i>
|
||||
</a>
|
||||
</div>
|
||||
{{ block('hidden_widget') }}
|
||||
{%- endblock weekpicker_widget %}
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
|
||||
{% block report %}
|
||||
|
||||
{% set hasData = false %}
|
||||
{% for day in days %}
|
||||
{% if day.details is not empty %}
|
||||
{% set hasData = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_before %}
|
||||
@@ -20,10 +27,14 @@
|
||||
{% endif %}
|
||||
{{ form_widget(form.date) }}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}user-month-reporting-box no-padding table-responsive{% endblock %}
|
||||
{% block box_body_class %}user-month-reporting-box table-responsive{% if hasData %} no-padding{% endif %}{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if not hasData %}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
<table class="table table-bordered table-hover dataTable">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
{% for day in days %}
|
||||
<th class="text-center text-nowrap{% if day.day is weekend %} weekend{% endif %}">
|
||||
@@ -31,52 +42,54 @@
|
||||
{{ day.day|date_format('d.m') }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for project in rows %}
|
||||
<tr>
|
||||
<td class="text-nowrap">
|
||||
<strong>{{ widgets.label_project(project.project) }}</strong>
|
||||
</td>
|
||||
<th class="text-nowrap text-center">{{ project.duration|duration }}</th>
|
||||
{% for day in project.days %}
|
||||
<td class="text-nowrap{% if day.date is weekend %} weekend{% endif %}">
|
||||
<td class="text-nowrap text-center{% if day.date is weekend %} weekend{% endif %}">
|
||||
{% if day.duration > 0 %}
|
||||
{{ day.duration|duration }}
|
||||
<strong>{{ day.duration|duration }}</strong>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<th class="text-nowrap">{{ project.duration|duration }}</th>
|
||||
</tr>
|
||||
{% for activity in project.activities %}
|
||||
<tr>
|
||||
<td class="text-nowrap">
|
||||
{{ widgets.label_activity(activity.activity) }}
|
||||
</td>
|
||||
<th class="text-nowrap text-center">{{ activity.duration|duration }}</th>
|
||||
{% for day in activity.days %}
|
||||
<td class="text-nowrap{% if day.date is weekend %} weekend{% endif %}">
|
||||
<td class="text-nowrap text-center{% if day.date is weekend %} weekend{% endif %}">
|
||||
{% if day.duration > 0 %}
|
||||
{{ day.duration|duration }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<th class="text-nowrap">{{ activity.duration|duration }}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% set total = 0 %}
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for day in days %}
|
||||
<th class="text-nowrap{% if day.day is weekend %} weekend{% endif %}">
|
||||
{% set total = total + day.totalDuration %}
|
||||
{% endfor %}
|
||||
<th></th>
|
||||
<th class="text-nowrap text-center">{{ total|duration }}</th>
|
||||
{% for day in days %}
|
||||
<th class="text-nowrap text-center{% if day.day is weekend %} weekend{% endif %}">
|
||||
{% if day.totalDuration > 0 %}
|
||||
{{ day.totalDuration|duration }}
|
||||
{% set total = total + day.totalDuration %}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th class="text-nowrap">{{ total|duration }}</th>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
{% block box_title %}
|
||||
{{ form_widget(form.date) }}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}monthly-user-list-reporting-box no-padding table-responsive{% endblock %}
|
||||
{% block box_body_class %}monthly-user-list-reporting-box table-responsive no-padding{% endblock %}
|
||||
{% block box_body %}
|
||||
<table class="table table-bordered table-hover dataTable">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
{% for day in days %}
|
||||
<th class="text-center text-nowrap{% if day is weekend %} weekend{% endif %}">
|
||||
@@ -26,7 +27,6 @@
|
||||
{{ day|date_format('d.m') }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for userDay in rows %}
|
||||
{% set usersMonthDuration = 0 %}
|
||||
@@ -34,15 +34,25 @@
|
||||
<td class="text-nowrap">
|
||||
<strong>{{ widgets.username(userDay.user) }}</strong>
|
||||
</td>
|
||||
{% for day in userDay.days %}
|
||||
{% if day.totalDuration > 0 %}
|
||||
{% set usersMonthDuration = usersMonthDuration + day.totalDuration %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<th class="text-nowrap text-center">
|
||||
{% if usersMonthDuration == 0 %}
|
||||
-
|
||||
{% else %}
|
||||
{{ usersMonthDuration|duration }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% for day in userDay.days %}
|
||||
<td class="text-nowrap{% if day.day is weekend %} weekend{% endif %}">
|
||||
{% if day.totalDuration > 0 %}
|
||||
{{ day.totalDuration|duration }}
|
||||
{% set usersMonthDuration = usersMonthDuration + day.totalDuration %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<th class="text-nowrap">{{ usersMonthDuration|duration }}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
112
templates/reporting/week_by_user.html.twig
Normal file
112
templates/reporting/week_by_user.html.twig
Normal file
@@ -0,0 +1,112 @@
|
||||
{% extends 'reporting/layout.html.twig' %}
|
||||
|
||||
{% block report_title %}{{ 'report_user_week'|trans({}, 'reporting') }}{% endblock %}
|
||||
|
||||
{% block report %}
|
||||
|
||||
{% set hasData = false %}
|
||||
{% for day in days %}
|
||||
{% if day.details is not empty %}
|
||||
{% set hasData = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_before %}
|
||||
{{ form_start(form, {'action': path('report_user_week'), 'attr': {'class': 'form-inline'}}) }}
|
||||
{% endblock %}
|
||||
{% block box_after %}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
{% block box_title %}
|
||||
{% if form.user is defined %}
|
||||
{{ form_widget(form.user) }}
|
||||
{% else %}
|
||||
{{ widgets.username(user) }}
|
||||
{% endif %}
|
||||
{{ form_widget(form.date) }}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}user-week-reporting-box table-responsive{% if hasData %} no-padding{% endif %}{% endblock %}
|
||||
{% block box_body %}
|
||||
{% if not hasData %}
|
||||
{{ widgets.nothing_found() }}
|
||||
{% else %}
|
||||
<table class="table table-bordered table-hover dataTable">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
{% for day in days %}
|
||||
<th class="text-center text-nowrap{% if day.day is weekend %} weekend{% endif %}">
|
||||
{{ day.day|day_name(true) }}<br>
|
||||
{{ day.day|date_format('d.m') }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for project in rows %}
|
||||
<tr>
|
||||
<td class="text-nowrap">
|
||||
<strong>{{ widgets.label_project(project.project) }}</strong>
|
||||
</td>
|
||||
<th class="text-nowrap text-center">{{ project.duration|duration }}</th>
|
||||
{% for day in project.days %}
|
||||
<td class="text-nowrap text-center{% if day.date is weekend %} weekend{% endif %}">
|
||||
{% if day.duration > 0 %}
|
||||
<strong>{{ day.duration|duration }}</strong>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for activity in project.activities %}
|
||||
<tr>
|
||||
<td class="text-nowrap">
|
||||
{{ widgets.label_activity(activity.activity) }}
|
||||
</td>
|
||||
<th class="text-nowrap text-center">{{ activity.duration|duration }}</th>
|
||||
{% for day in activity.days %}
|
||||
<td class="text-nowrap text-center{% if day.date is weekend %} weekend{% endif %}">
|
||||
{% if day.duration > 0 %}
|
||||
{{ day.duration|duration }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% set total = 0 %}
|
||||
<tr>
|
||||
{% for day in days %}
|
||||
{% set total = total + day.totalDuration %}
|
||||
{% endfor %}
|
||||
<th></th>
|
||||
<th class="text-nowrap text-center">{{ total|duration }}</th>
|
||||
{% for day in days %}
|
||||
<th class="text-nowrap text-center{% if day.day is weekend %} weekend{% endif %}">
|
||||
{% if day.totalDuration > 0 %}
|
||||
{{ day.totalDuration|duration }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('kimai.initialized', function() {
|
||||
{% if form.user is defined %}
|
||||
$('#{{ form.user.vars.id }}').on('change', function(ev) {
|
||||
$(this).closest('form').submit();
|
||||
});
|
||||
{% endif %}
|
||||
$('#{{ form.date.vars.id }}').on('change', function(ev) {
|
||||
$(this).closest('form').submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
@@ -21,6 +22,16 @@ class ReportingControllerTest extends ControllerBaseTest
|
||||
$this->assertUrlIsSecured('/reporting');
|
||||
}
|
||||
|
||||
public function testWeekByUserIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/reporting/week_by_user');
|
||||
}
|
||||
|
||||
public function testMonthByUserIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/reporting/month_by_user');
|
||||
}
|
||||
|
||||
public function testMonthlyListIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/reporting/monthly_users_list');
|
||||
@@ -31,17 +42,53 @@ class ReportingControllerTest extends ControllerBaseTest
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/reporting/monthly_users_list');
|
||||
}
|
||||
|
||||
public function testDefaultUsersMonthReport()
|
||||
protected function importReportingFixture(string $role)
|
||||
{
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(50);
|
||||
$fixture->setAmountRunning(10);
|
||||
$fixture->setUser($this->getUserByRole($role));
|
||||
$fixture->setStartDate(new \DateTime());
|
||||
$this->importFixture($fixture);
|
||||
}
|
||||
|
||||
public function testRedirectForDefaultReportUrl()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, '/reporting/');
|
||||
$this->importReportingFixture(User::ROLE_USER);
|
||||
$this->request($client, '/reporting/');
|
||||
$this->assertIsRedirect($client, $this->createUrl('/reporting/week_by_user'));
|
||||
$client->followRedirect();
|
||||
self::assertStringContainsString('<div class="box-body user-week-reporting-box', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testUserWeekReport()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->importReportingFixture(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/reporting/week_by_user?user=4&date=12999119191');
|
||||
self::assertStringContainsString('<div class="box-body user-week-reporting-box', $client->getResponse()->getContent());
|
||||
$option = $client->getCrawler()->filterXPath("//select[@id='user']/option[@selected]");
|
||||
self::assertEquals(4, $option->attr('value'));
|
||||
}
|
||||
|
||||
public function testUserMonthReport()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->importReportingFixture(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, '/reporting/month_by_user?user=4&date=12999119191');
|
||||
self::assertStringContainsString('<div class="box-body user-month-reporting-box', $client->getResponse()->getContent());
|
||||
$select = $client->getCrawler()->filterXPath("//select[@id='user']");
|
||||
self::assertEquals(0, $select->count());
|
||||
}
|
||||
|
||||
public function testMonthlyUsersReport()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->importReportingFixture(User::ROLE_TEAMLEAD);
|
||||
$this->assertAccessIsGranted($client, '/reporting/monthly_users_list');
|
||||
self::assertStringContainsString('<div class="box-body monthly-user-list-reporting-box', $client->getResponse()->getContent());
|
||||
$select = $client->getCrawler()->filterXPath("//select[@id='user']");
|
||||
self::assertEquals(0, $select->count());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityMeta;
|
||||
use App\Entity\Project;
|
||||
@@ -58,6 +59,9 @@ class ActivityTest extends TestCase
|
||||
$this->assertInstanceOf(Activity::class, $sut->setColor('#fffccc'));
|
||||
$this->assertEquals('#fffccc', $sut->getColor());
|
||||
|
||||
$this->assertInstanceOf(Activity::class, $sut->setColor(Constants::DEFAULT_COLOR));
|
||||
$this->assertNull($sut->getColor());
|
||||
|
||||
$this->assertInstanceOf(Activity::class, $sut->setBudget(12345.67));
|
||||
$this->assertEquals(12345.67, $sut->getBudget());
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerMeta;
|
||||
use App\Entity\Team;
|
||||
@@ -72,6 +73,9 @@ class CustomerTest extends TestCase
|
||||
self::assertInstanceOf(Customer::class, $sut->setColor('#fffccc'));
|
||||
self::assertEquals('#fffccc', $sut->getColor());
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setColor(Constants::DEFAULT_COLOR));
|
||||
self::assertNull($sut->getColor());
|
||||
|
||||
self::assertInstanceOf(Customer::class, $sut->setCompany('test company'));
|
||||
self::assertEquals('test company', $sut->getCompany());
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectMeta;
|
||||
@@ -82,6 +83,9 @@ class ProjectTest extends TestCase
|
||||
self::assertInstanceOf(Project::class, $sut->setColor('#fffccc'));
|
||||
self::assertEquals('#fffccc', $sut->getColor());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setColor(Constants::DEFAULT_COLOR));
|
||||
self::assertNull($sut->getColor());
|
||||
|
||||
self::assertInstanceOf(Project::class, $sut->setVisible(false));
|
||||
self::assertFalse($sut->isVisible());
|
||||
|
||||
|
||||
43
tests/Reporting/AbstractDateByUserTest.php
Normal file
43
tests/Reporting/AbstractDateByUserTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Reporting;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Reporting\DateByUser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Reporting\DateByUser
|
||||
*/
|
||||
abstract class AbstractDateByUserTest extends TestCase
|
||||
{
|
||||
abstract protected function createSut(): DateByUser;
|
||||
|
||||
public function testEmptyObject()
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
self::assertNull($sut->getDate());
|
||||
self::assertNull($sut->getUser());
|
||||
}
|
||||
|
||||
public function testSetter()
|
||||
{
|
||||
$date = new \DateTime('2019-05-27');
|
||||
$user = new User();
|
||||
$user->setAlias('sdfsdfdsdf');
|
||||
|
||||
$sut = $this->createSut();
|
||||
self::assertInstanceOf(DateByUser::class, $sut->setDate($date));
|
||||
self::assertInstanceOf(DateByUser::class, $sut->setUser($user));
|
||||
|
||||
self::assertSame($date, $sut->getDate());
|
||||
self::assertSame($user, $sut->getUser());
|
||||
}
|
||||
}
|
||||
25
tests/Reporting/MonthByUserTest.php
Normal file
25
tests/Reporting/MonthByUserTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Reporting;
|
||||
|
||||
use App\Reporting\DateByUser;
|
||||
use App\Reporting\MonthByUser;
|
||||
|
||||
/**
|
||||
* @covers \App\Reporting\MonthByUser
|
||||
* @covers \App\Reporting\DateByUser
|
||||
*/
|
||||
class MonthByUserTest extends AbstractDateByUserTest
|
||||
{
|
||||
protected function createSut(): DateByUser
|
||||
{
|
||||
return new MonthByUser();
|
||||
}
|
||||
}
|
||||
25
tests/Reporting/WeekByUserTest.php
Normal file
25
tests/Reporting/WeekByUserTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Reporting;
|
||||
|
||||
use App\Reporting\DateByUser;
|
||||
use App\Reporting\WeekByUser;
|
||||
|
||||
/**
|
||||
* @covers \App\Reporting\WeekByUser
|
||||
* @covers \App\Reporting\DateByUser
|
||||
*/
|
||||
class WeekByUserTest extends AbstractDateByUserTest
|
||||
{
|
||||
protected function createSut(): DateByUser
|
||||
{
|
||||
return new WeekByUser();
|
||||
}
|
||||
}
|
||||
158
tests/Timesheet/DateTimeFactoryTest.php
Normal file
158
tests/Timesheet/DateTimeFactoryTest.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?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\Timesheet;
|
||||
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Timesheet\DateTimeFactory
|
||||
*/
|
||||
class DateTimeFactoryTest extends TestCase
|
||||
{
|
||||
public const TEST_TIMEZONE = 'Europe/London';
|
||||
|
||||
protected function createDateTimeFactory(?string $timezone = null): DateTimeFactory
|
||||
{
|
||||
if (null === $timezone) {
|
||||
return new DateTimeFactory();
|
||||
}
|
||||
|
||||
return new DateTimeFactory(new DateTimeZone($timezone));
|
||||
}
|
||||
|
||||
public function testGetTimezone()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $sut->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testGetTimezoneWithFallbackTimezone()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory();
|
||||
$this->assertEquals(date_default_timezone_get(), $sut->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testGetStartOfMonth()
|
||||
{
|
||||
$expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE));
|
||||
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->getStartOfMonth();
|
||||
$this->assertEquals(0, $dateTime->format('H'));
|
||||
$this->assertEquals(0, $dateTime->format('i'));
|
||||
$this->assertEquals(0, $dateTime->format('s'));
|
||||
$this->assertEquals(1, $dateTime->format('d'));
|
||||
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
|
||||
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testGetEndOfMonth()
|
||||
{
|
||||
$expected = new DateTime('last day of this month', new DateTimeZone(self::TEST_TIMEZONE));
|
||||
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->getEndOfMonth();
|
||||
$this->assertEquals(23, $dateTime->format('H'));
|
||||
$this->assertEquals(59, $dateTime->format('i'));
|
||||
$this->assertEquals(59, $dateTime->format('s'));
|
||||
$this->assertEquals($expected->format('d'), $dateTime->format('d'));
|
||||
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
|
||||
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testGetStartOfWeek()
|
||||
{
|
||||
$expected = new DateTime('2018-07-26 16:47:31', new DateTimeZone(self::TEST_TIMEZONE));
|
||||
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->getStartOfWeek($expected);
|
||||
|
||||
$this->assertEquals(0, $dateTime->format('H'));
|
||||
$this->assertEquals(0, $dateTime->format('i'));
|
||||
$this->assertEquals(0, $dateTime->format('s'));
|
||||
$this->assertEquals(23, $dateTime->format('d'));
|
||||
$this->assertEquals(1, $dateTime->format('N'));
|
||||
$this->assertEquals('Monday', $dateTime->format('l'));
|
||||
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
|
||||
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
|
||||
$expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE));
|
||||
$dateTime = $sut->getStartOfWeek();
|
||||
|
||||
$this->assertEquals(0, $dateTime->format('H'));
|
||||
$this->assertEquals(0, $dateTime->format('i'));
|
||||
$this->assertEquals(0, $dateTime->format('s'));
|
||||
$this->assertEquals(1, $dateTime->format('N'));
|
||||
$this->assertEquals('Monday', $dateTime->format('l'));
|
||||
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
|
||||
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testGetEndOfWeek()
|
||||
{
|
||||
$expected = new DateTime('2018-07-26 16:47:31', new DateTimeZone(self::TEST_TIMEZONE));
|
||||
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->getEndOfWeek($expected);
|
||||
|
||||
$this->assertEquals(23, $dateTime->format('H'));
|
||||
$this->assertEquals(59, $dateTime->format('i'));
|
||||
$this->assertEquals(59, $dateTime->format('s'));
|
||||
$this->assertEquals(29, $dateTime->format('d'));
|
||||
$this->assertEquals(7, $dateTime->format('N'));
|
||||
$this->assertEquals('Sunday', $dateTime->format('l'));
|
||||
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
|
||||
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
|
||||
$expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE));
|
||||
$dateTime = $sut->getEndOfWeek();
|
||||
|
||||
$this->assertEquals(23, $dateTime->format('H'));
|
||||
$this->assertEquals(59, $dateTime->format('i'));
|
||||
$this->assertEquals(59, $dateTime->format('s'));
|
||||
$this->assertEquals(7, $dateTime->format('N'));
|
||||
$this->assertEquals('Sunday', $dateTime->format('l'));
|
||||
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
|
||||
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testCreateDateTime()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->createDateTime('2015-07-24 13:45:21');
|
||||
$this->assertEquals(13, $dateTime->format('H'));
|
||||
$this->assertEquals(45, $dateTime->format('i'));
|
||||
$this->assertEquals(21, $dateTime->format('s'));
|
||||
$this->assertEquals('24', $dateTime->format('d'));
|
||||
$this->assertEquals('07', $dateTime->format('m'));
|
||||
$this->assertEquals('2015', $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testCreateDateTimeWithDefaultValue()
|
||||
{
|
||||
$expected = new DateTime('now', new DateTimeZone(self::TEST_TIMEZONE));
|
||||
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->createDateTime();
|
||||
$difference = $expected->getTimestamp() - $dateTime->getTimestamp();
|
||||
// poor test, but there shouldn't be more than 2 seconds between the creation of two DateTime objects
|
||||
$this->assertTrue(2 >= $difference);
|
||||
}
|
||||
}
|
||||
@@ -14,80 +14,27 @@ use App\Timesheet\UserDateTimeFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Timesheet\DateTimeFactory
|
||||
* @covers \App\Timesheet\UserDateTimeFactory
|
||||
*/
|
||||
class UserDateTimeFactoryTest extends TestCase
|
||||
{
|
||||
public const TEST_TIMEZONE = 'Europe/London';
|
||||
public const TEST_TIMEZONE = 'Africa/Asmara';
|
||||
|
||||
protected function createDateTimeFactory(?string $timezone = null): UserDateTimeFactory
|
||||
protected function createUserDateTimeFactory(?string $timezone = null): UserDateTimeFactory
|
||||
{
|
||||
return (new UserDateTimeFactoryFactory($this))->create($timezone);
|
||||
}
|
||||
|
||||
public function testGetTimezone()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$sut = $this->createUserDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $sut->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testGetTimezoneWithFallbackTimezone()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory();
|
||||
$sut = $this->createUserDateTimeFactory();
|
||||
$this->assertEquals(date_default_timezone_get(), $sut->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testGetStartOfMonth()
|
||||
{
|
||||
$expected = new \DateTime('now', new \DateTimeZone(self::TEST_TIMEZONE));
|
||||
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->getStartOfMonth();
|
||||
$this->assertEquals(0, $dateTime->format('H'));
|
||||
$this->assertEquals(0, $dateTime->format('i'));
|
||||
$this->assertEquals(0, $dateTime->format('s'));
|
||||
$this->assertEquals(1, $dateTime->format('d'));
|
||||
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
|
||||
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testGetEndOfMonth()
|
||||
{
|
||||
$expected = new \DateTime('last day of this month', new \DateTimeZone(self::TEST_TIMEZONE));
|
||||
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->getEndOfMonth();
|
||||
$this->assertEquals(23, $dateTime->format('H'));
|
||||
$this->assertEquals(59, $dateTime->format('i'));
|
||||
$this->assertEquals(59, $dateTime->format('s'));
|
||||
$this->assertEquals($expected->format('d'), $dateTime->format('d'));
|
||||
$this->assertEquals($expected->format('m'), $dateTime->format('m'));
|
||||
$this->assertEquals($expected->format('Y'), $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testCreateDateTime()
|
||||
{
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->createDateTime('2015-07-24 13:45:21');
|
||||
$this->assertEquals(13, $dateTime->format('H'));
|
||||
$this->assertEquals(45, $dateTime->format('i'));
|
||||
$this->assertEquals(21, $dateTime->format('s'));
|
||||
$this->assertEquals('24', $dateTime->format('d'));
|
||||
$this->assertEquals('07', $dateTime->format('m'));
|
||||
$this->assertEquals('2015', $dateTime->format('Y'));
|
||||
$this->assertEquals(self::TEST_TIMEZONE, $dateTime->getTimezone()->getName());
|
||||
}
|
||||
|
||||
public function testCreateDateTimeWithDefaultValue()
|
||||
{
|
||||
$expected = new \DateTime('now', new \DateTimeZone(self::TEST_TIMEZONE));
|
||||
|
||||
$sut = $this->createDateTimeFactory(self::TEST_TIMEZONE);
|
||||
$dateTime = $sut->createDateTime();
|
||||
$difference = $expected->getTimestamp() - $dateTime->getTimestamp();
|
||||
// poor test, but there shouldn't be more than 2 seconds between the creation of two DateTime objects
|
||||
$this->assertTrue(2 >= $difference);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,20 +139,27 @@ class DateExtensionsTest extends TestCase
|
||||
/**
|
||||
* @dataProvider getMonthNameTestData
|
||||
*/
|
||||
public function testMonthName(string $locale, string $date, string $expectedName)
|
||||
public function testMonthName(string $locale, string $date, string $expectedName, bool $withYear = false)
|
||||
{
|
||||
$sut = $this->getSut($locale, []);
|
||||
self::assertEquals($expectedName, $sut->monthName(new \DateTime($date)));
|
||||
self::assertEquals($expectedName, $sut->monthName(new \DateTime($date), $withYear));
|
||||
}
|
||||
|
||||
public function getMonthNameTestData()
|
||||
{
|
||||
return [
|
||||
['de', '2020-07-09 23:59:59', 'Juli'],
|
||||
['en', '2020-07-09 23:59:59', 'July'],
|
||||
['de', 'January 2016', 'Januar'],
|
||||
['en', 'January 2016', 'January'],
|
||||
['en', '2016-12-23', 'December'],
|
||||
['de', '2020-07-09 23:59:59', 'Juli', false],
|
||||
['en', '2020-07-09 23:59:59', 'July', false],
|
||||
['de', 'January 2016', 'Januar', false],
|
||||
['en', 'January 2016', 'January', false],
|
||||
['en', '2016-12-23', 'December', false],
|
||||
['ru', '2016-12-23', 'декабрь', false],
|
||||
['de', '2020-07-09 23:59:59', 'Juli 2020', true],
|
||||
['en', '2020-07-09 23:59:59', 'July 2020', true],
|
||||
['de', 'January 2016', 'Januar 2016', true],
|
||||
['en', 'January 2016', 'January 2016', true],
|
||||
['en', '2015-12-23', 'December 2015', true],
|
||||
['ru', '2015-12-23', 'декабрь 2015', true],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Tests\Twig;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Twig\Extensions;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -27,7 +30,7 @@ class ExtensionsTest extends TestCase
|
||||
|
||||
public function testGetFilters()
|
||||
{
|
||||
$filters = ['docu_link', 'multiline_indent'];
|
||||
$filters = ['docu_link', 'multiline_indent', 'color'];
|
||||
$sut = $this->getSut();
|
||||
$twigFilters = $sut->getFilters();
|
||||
$this->assertCount(\count($filters), $twigFilters);
|
||||
@@ -116,4 +119,39 @@ sdfsdf' . PHP_EOL . "\n" .
|
||||
$sut = $this->getSut();
|
||||
self::assertEquals(implode("\n", $expected), $sut->multilineIndent($string, $indent));
|
||||
}
|
||||
|
||||
public function testColor()
|
||||
{
|
||||
$sut = $this->getSut();
|
||||
|
||||
$globalActivity = new Activity();
|
||||
self::assertNull($sut->color($globalActivity));
|
||||
|
||||
$globalActivity->setColor('#000001');
|
||||
self::assertEquals('#000001', $sut->color($globalActivity));
|
||||
|
||||
$customer = new Customer();
|
||||
self::assertNull($sut->color($customer));
|
||||
|
||||
$customer->setColor('#000004');
|
||||
self::assertEquals('#000004', $sut->color($customer));
|
||||
|
||||
$project = new Project();
|
||||
self::assertNull($sut->color($project));
|
||||
|
||||
$project->setCustomer($customer);
|
||||
self::assertEquals('#000004', $sut->color($project));
|
||||
|
||||
$project->setColor('#000003');
|
||||
self::assertEquals('#000003', $sut->color($project));
|
||||
|
||||
$activity = new Activity();
|
||||
self::assertNull($sut->color($activity));
|
||||
|
||||
$activity->setProject($project);
|
||||
self::assertEquals('#000003', $sut->color($activity));
|
||||
|
||||
$activity->setColor('#000002');
|
||||
self::assertEquals('#000002', $sut->color($activity));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,6 @@ class ReportingExtensionTest extends TestCase
|
||||
$sut = $this->getSut(true);
|
||||
$reports = $sut->getAvailableReports(new User());
|
||||
self::assertIsArray($reports);
|
||||
self::assertCount(2, $reports);
|
||||
self::assertCount(3, $reports);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,6 +648,10 @@
|
||||
<!--
|
||||
Statistics data for Dashboard & Users profile
|
||||
-->
|
||||
<trans-unit id="stats.workingTimeWeek">
|
||||
<source>stats.workingTimeWeek</source>
|
||||
<target>Kalenderweek %week%</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.durationToday">
|
||||
<source>stats.durationToday</source>
|
||||
<target>Prestaties vandaag</target>
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<source>reporting.title</source>
|
||||
<target>Reporting</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_user_week">
|
||||
<source>report_user_week</source>
|
||||
<target>Wochenansicht für einen Benutzer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_user_month">
|
||||
<source>report_user_month</source>
|
||||
<target>Monatsansicht für einen Benutzer</target>
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<source>reporting.title</source>
|
||||
<target>Reporting</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_user_week">
|
||||
<source>report_user_week</source>
|
||||
<target>Weekly view for one user</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_user_month">
|
||||
<source>report_user_month</source>
|
||||
<target>Monthly view for one user</target>
|
||||
|
||||
23
translations/reporting.nl.xlf
Normal file
23
translations/reporting.nl.xlf
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" target-language="nl" datatype="plaintext" original="reporting.en.xlf">
|
||||
<body>
|
||||
<trans-unit id="reporting.title">
|
||||
<source>reporting.title</source>
|
||||
<target>Rapporten</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_user_week">
|
||||
<source>report_user_week</source>
|
||||
<target>Weekoverzicht voor een user</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_user_month">
|
||||
<source>report_user_month</source>
|
||||
<target>Maandoverzicht voor een user</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="report_monthly_users">
|
||||
<source>report_monthly_users</source>
|
||||
<target>Maandoverzicht voor alle users</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
Reference in New Issue
Block a user