added daterange-picker and localized date-inputs (#540)

This commit is contained in:
Kevin Papst
2019-02-03 21:24:30 +01:00
committed by GitHub
parent aae9183de6
commit 062735c9e3
37 changed files with 1058 additions and 264 deletions

View File

@@ -168,6 +168,13 @@ class InvoiceController extends AbstractController
}
$query->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER);
if (null === $query->getBegin()) {
$query->setBegin(new \DateTime('first day of this month'));
}
if (null === $query->getEnd()) {
$query->setEnd(new \DateTime('last day of this month'));
}
$query->getBegin()->setTime(0, 0, 0);
$query->getEnd()->setTime(23, 59, 59);

View File

@@ -172,9 +172,12 @@ class Configuration implements ConfigurationInterface
$node
->arrayPrototype()
->children()
->scalarNode('date_short')->end()
->scalarNode('duration')->end()
->scalarNode('duration_short')->end()
->scalarNode('date_time_type')->defaultValue('yyyy-MM-dd HH:mm')->end() // for DateTimeType
->scalarNode('date_time_picker')->defaultValue('YYYY-MM-DD HH:mm')->end() // for DateTimeType JS component
->scalarNode('date_type')->defaultValue('yyyy-MM-dd')->end() // for DateType
->scalarNode('date_picker')->defaultValue('YYYY-MM-DD')->end() // for DateType JS component
->scalarNode('date')->defaultValue('Y-m-d')->end() // for display via twig
->scalarNode('duration')->defaultValue('%%h:%%m h')->end() // for display via twig
->end()
->end()
;

View File

@@ -0,0 +1,62 @@
<?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\Model;
use DateTime;
class DateRange
{
/**
* @var DateTime
*/
protected $begin;
/**
* @var DateTime
*/
protected $end;
/**
* @return DateTime
*/
public function getBegin(): ?DateTime
{
return $this->begin;
}
/**
* @param DateTime $begin
* @return DateRange
*/
public function setBegin(DateTime $begin)
{
$this->begin = $begin;
return $this;
}
/**
* @return DateTime
*/
public function getEnd(): ?DateTime
{
return $this->end;
}
/**
* @param DateTime $end
* @return DateRange
*/
public function setEnd(DateTime $end)
{
$this->end = $end;
return $this;
}
}

View File

@@ -12,6 +12,7 @@ namespace App\Form;
use App\Entity\Timesheet;
use App\Form\Type\ActivityType;
use App\Form\Type\CustomerType;
use App\Form\Type\DateTimePickerType;
use App\Form\Type\DurationType;
use App\Form\Type\ProjectType;
use App\Form\Type\UserType;
@@ -19,7 +20,6 @@ use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -90,13 +90,8 @@ class TimesheetEditForm extends AbstractType
}
if (null === $end || !$options['duration_only']) {
$builder->add('begin', DateTimeType::class, [
$builder->add('begin', DateTimePickerType::class, [
'label' => 'label.begin',
'widget' => 'single_text',
'html5' => false,
'format' => 'yyyy-MM-dd HH:mm',
'with_seconds' => false,
'attr' => ['autocomplete' => 'off', 'data-datetimepicker' => 'on', 'placeholder' => 'yyyy-MM-dd HH:mm'],
]);
}
@@ -105,14 +100,8 @@ class TimesheetEditForm extends AbstractType
'required' => false,
]);
} else {
$builder->add('end', DateTimeType::class, [
$builder->add('end', DateTimePickerType::class, [
'label' => 'label.end',
'widget' => 'single_text',
'required' => false,
'html5' => false,
'format' => 'yyyy-MM-dd HH:mm',
'with_seconds' => false,
'attr' => ['autocomplete' => 'off', 'data-datetimepicker' => 'on', 'placeholder' => 'yyyy-MM-dd HH:mm'],
]);
}

View File

@@ -10,9 +10,8 @@
namespace App\Form\Toolbar;
use App\Form\Type\ActivityType;
use App\Form\Type\BeginDateType;
use App\Form\Type\CustomerType;
use App\Form\Type\EndDateType;
use App\Form\Type\DateRangeType;
use App\Form\Type\PageSizeType;
use App\Form\Type\ProjectType;
use App\Form\Type\UserRoleType;
@@ -115,20 +114,11 @@ abstract class AbstractToolbarForm extends AbstractType
/**
* @param FormBuilderInterface $builder
*/
protected function addStartDateChoice(FormBuilderInterface $builder)
protected function addDateRangeChoice(FormBuilderInterface $builder, $allowEmpty = true)
{
$builder->add('begin', BeginDateType::class, [
'required' => false,
]);
}
/**
* @param FormBuilderInterface $builder
*/
protected function addEndDateChoice(FormBuilderInterface $builder)
{
$builder->add('end', EndDateType::class, [
$builder->add('daterange', DateRangeType::class, [
'required' => false,
'allow_empty' => $allowEmpty,
]);
}

View File

@@ -26,8 +26,7 @@ class InvoiceToolbarForm extends AbstractToolbarForm
{
$this->addTemplateChoice($builder);
$this->addUserChoice($builder);
$this->addStartDateChoice($builder);
$this->addEndDateChoice($builder);
$this->addDateRangeChoice($builder);
$this->addCustomerChoice($builder);
$this->addProjectChoice($builder);
$this->addActivityChoice($builder);

View File

@@ -24,8 +24,7 @@ class TimesheetAdminToolbarForm extends TimesheetToolbarForm
$this->addTimesheetStateChoice($builder);
$this->addPageSizeChoice($builder);
$this->addUserChoice($builder);
$this->addStartDateChoice($builder);
$this->addEndDateChoice($builder);
$this->addDateRangeChoice($builder);
$this->addCustomerChoice($builder);
$this->addProjectChoice($builder);
$this->addActivityChoice($builder);

View File

@@ -26,8 +26,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
{
$this->addTimesheetStateChoice($builder);
$this->addPageSizeChoice($builder);
$this->addStartDateChoice($builder);
$this->addEndDateChoice($builder);
$this->addDateRangeChoice($builder);
$this->addCustomerChoice($builder);
$this->addProjectChoice($builder);
$this->addActivityChoice($builder);

View File

@@ -9,33 +9,50 @@
namespace App\Form\Type;
use App\Utils\LocaleSettings;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to display the begin-date input field.
* Custom form field type to display the date input fields.
*/
class BeginDateType extends AbstractType
class DatePickerType extends AbstractType
{
/**
* @var LocaleSettings
*/
protected $localeSettings;
/**
* @param LocaleSettings $localeSettings
*/
public function __construct(LocaleSettings $localeSettings)
{
$this->localeSettings = $localeSettings;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$pickerFormat = $this->localeSettings->getDatePickerFormat();
$dateFormat = $this->localeSettings->getDateTypeFormat();
$resolver->setDefaults([
'label' => 'label.begin',
'widget' => 'single_text',
'html5' => false,
'format' => DateType::HTML5_FORMAT,
'format' => $dateFormat,
'format_picker' => $pickerFormat,
]);
$resolver->setDefault('attr', function (Options $options) {
return [
'autocomplete' => 'off',
'data-datepicker' => 'on',
'placeholder' => $options['format'],
'data-format' => $options['format_picker'],
];
});
}

View File

@@ -0,0 +1,154 @@
<?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\Form\Model\DateRange;
use App\Utils\LocaleSettings;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a date range
*/
class DateRangeType extends AbstractType
{
public const DATE_SPACER = ' - ';
/**
* @var LocaleSettings
*/
protected $localeSettings;
/**
* @param LocaleSettings $localeSettings
*/
public function __construct(LocaleSettings $localeSettings)
{
$this->localeSettings = $localeSettings;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$pickerFormat = $this->localeSettings->getDatePickerFormat();
$dateFormat = $this->localeSettings->getDateFormat();
$resolver->setDefaults([
'model_timezone' => null,
'view_timezone' => null,
'label' => 'label.daterange',
'format' => $dateFormat,
'separator' => self::DATE_SPACER,
'format_picker' => $pickerFormat,
'allow_empty' => true,
]);
$resolver->setDefault('attr', function (Options $options) {
return [
'autocomplete' => 'off',
'placeholder' => $options['format_picker'] . $options['separator'] . $options['format_picker'],
'data-format' => $options['format_picker'],
'data-separator' => $options['separator'],
];
});
}
protected function formatToPattern(string $format, string $separator)
{
$pattern = str_replace('d', '[0-9]{2}', $format);
$pattern = str_replace('m', '[0-9]{2}', $pattern);
$pattern = str_replace('Y', '[0-9]{4}', $pattern);
return '/^' . $pattern . $separator . $pattern . '$/';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$formatDate = $options['format'];
$separator = $options['separator'];
$allowEmpty = $options['allow_empty'];
$pattern = $this->formatToPattern($formatDate, $separator);
$builder->addModelTransformer(new CallbackTransformer(
function (DateRange $range) use ($formatDate, $separator) {
if (null === $range->getBegin()) {
return '';
}
$display = $range->getBegin()->format($formatDate);
if (null !== $range->getEnd()) {
$display .= $separator . $range->getEnd()->format($formatDate);
}
return $display;
},
function ($dates) use ($formatDate, $pattern, $separator, $allowEmpty) {
$range = new DateRange();
if (empty($dates) && $allowEmpty) {
return $range;
}
if (preg_match($pattern, $dates) !== 1) {
throw new TransformationFailedException('Invalid date range given');
}
$values = explode($separator, $dates);
if (count($values) !== 2) {
throw new TransformationFailedException('Invalid date range given');
}
$begin = \DateTime::createFromFormat($formatDate, $values[0]);
if ($begin === false) {
throw new TransformationFailedException('Invalid begin date given');
}
$range->setBegin($begin);
$end = \DateTime::createFromFormat($formatDate, $values[1]);
if ($end === false) {
throw new TransformationFailedException('Invalid end date given');
}
$range->setEnd($end);
if ($begin->getTimestamp() > $end->getTimestamp()) {
throw new TransformationFailedException('Begin date must be before end date');
}
return $range;
}
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'daterange';
}
}

View File

@@ -0,0 +1,70 @@
<?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\LocaleSettings;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to display the date-time input fields.
*/
class DateTimePickerType extends AbstractType
{
/**
* @var LocaleSettings
*/
protected $localeSettings;
/**
* @param LocaleSettings $localeSettings
*/
public function __construct(LocaleSettings $localeSettings)
{
$this->localeSettings = $localeSettings;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$dateTimePicker = $this->localeSettings->getDateTimePickerFormat();
$dateTimeFormat = $this->localeSettings->getDateTimeTypeFormat();
$resolver->setDefaults([
'label' => 'label.begin',
'widget' => 'single_text',
'html5' => false,
'format' => $dateTimeFormat,
'format_picker' => $dateTimePicker,
'with_seconds' => false,
]);
$resolver->setDefault('attr', function (Options $options) {
return [
'data-datetimepicker' => 'on',
'autocomplete' => 'off',
'placeholder' => $options['format'],
'data-format' => $options['format_picker'],
];
});
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return DateTimeType::class;
}
}

View File

@@ -1,50 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to display the end-date input field.
*/
class EndDateType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'label' => 'label.end',
'widget' => 'single_text',
'html5' => false,
'format' => DateType::HTML5_FORMAT,
]);
$resolver->setDefault('attr', function (Options $options) {
return [
'autocomplete' => 'off',
'data-datepicker' => 'on',
'placeholder' => $options['format'],
];
});
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return DateType::class;
}
}

View File

@@ -11,6 +11,7 @@ namespace App\Repository\Query;
use App\Entity\Activity;
use App\Entity\User;
use App\Form\Model\DateRange;
/**
* Can be used for advanced timesheet repository queries.
@@ -44,13 +45,14 @@ class TimesheetQuery extends ActivityQuery
*/
protected $state = self::STATE_ALL;
/**
* @var \DateTime
* @var DateRange
*/
protected $begin;
/**
* @var \DateTime
*/
protected $end;
protected $dateRange;
public function __construct()
{
$this->dateRange = new DateRange();
}
/**
* @return User
@@ -123,7 +125,7 @@ class TimesheetQuery extends ActivityQuery
*/
public function getBegin()
{
return $this->begin;
return $this->dateRange->getBegin();
}
/**
@@ -132,7 +134,7 @@ class TimesheetQuery extends ActivityQuery
*/
public function setBegin($begin)
{
$this->begin = $begin;
$this->dateRange->setBegin($begin);
return $this;
}
@@ -142,7 +144,7 @@ class TimesheetQuery extends ActivityQuery
*/
public function getEnd()
{
return $this->end;
return $this->dateRange->getEnd();
}
/**
@@ -151,7 +153,26 @@ class TimesheetQuery extends ActivityQuery
*/
public function setEnd($end)
{
$this->end = $end;
$this->dateRange->setEnd($end);
return $this;
}
/**
* @return DateRange
*/
public function getDateRange(): DateRange
{
return $this->dateRange;
}
/**
* @param DateRange $dateRange
* @return TimesheetQuery
*/
public function setDateRange(DateRange $dateRange)
{
$this->dateRange = $dateRange;
return $this;
}

View File

@@ -9,8 +9,8 @@
namespace App\Twig;
use App\Utils\LocaleSettings;
use DateTime;
use Symfony\Component\HttpFoundation\RequestStack;
use Twig\TwigFilter;
/**
@@ -18,27 +18,17 @@ use Twig\TwigFilter;
*/
class DateExtensions extends \Twig_Extension
{
private const FALLBACK_SHORT = 'Y-m-d';
/**
* @var LocaleSettings
*/
protected $localeSettings;
/**
* @var array
* @param LocaleSettings $localeSettings
*/
protected $dateSettings;
/**
* @var RequestStack
*/
protected $requestStack;
/**
* DateExtensions constructor.
* @param RequestStack $requestStack
* @param array $dateSettings
*/
public function __construct(RequestStack $requestStack, array $dateSettings)
public function __construct(LocaleSettings $localeSettings)
{
$this->requestStack = $requestStack;
$this->dateSettings = $dateSettings;
$this->localeSettings = $localeSettings;
}
/**
@@ -53,26 +43,12 @@ class DateExtensions extends \Twig_Extension
}
/**
* @return string
*/
protected function getLocale()
{
return $this->requestStack->getCurrentRequest()->getLocale();
}
/**
* @param array $context
* @param DateTime $date
* @return string
*/
public function dateShort(DateTime $date)
{
$locale = $this->getLocale();
$format = self::FALLBACK_SHORT;
if (isset($this->dateSettings[$locale]['date_short'])) {
$format = $this->dateSettings[$locale]['date_short'];
}
$format = $this->localeSettings->getDateFormat();
return date_format($date, $format);
}

View File

@@ -11,6 +11,7 @@ namespace App\Twig;
use App\Entity\Timesheet;
use App\Utils\Duration;
use App\Utils\LocaleSettings;
use NumberFormatter;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Intl\Intl;
@@ -22,9 +23,9 @@ use Twig\TwigFilter;
class Extensions extends \Twig_Extension
{
/**
* @var string[]
* @var LocaleSettings
*/
protected $locales;
protected $localeSettings;
/**
* @var string
@@ -89,12 +90,12 @@ class Extensions extends \Twig_Extension
/**
* @param RequestStack $requestStack
* @param array $languages
* @param LocaleSettings $localeSettings
*/
public function __construct(RequestStack $requestStack, array $languages)
public function __construct(RequestStack $requestStack, LocaleSettings $localeSettings)
{
$this->requestStack = $requestStack;
$this->locales = $languages;
$this->localeSettings = $localeSettings;
$this->durationFormatter = new Duration();
}
@@ -200,19 +201,8 @@ class Extensions extends \Twig_Extension
return '?';
}
$locale = $this->getLocale();
switch ($format) {
case 'full':
$format = isset($this->locales[$locale]) ? $this->locales[$locale]['duration'] : null;
break;
case null:
case 'short':
$format = isset($this->locales[$locale]) ? $this->locales[$locale]['duration_short'] : null;
break;
}
if (null === $format) {
$format = '%h:%m h';
$format = $this->localeSettings->getDurationFormat();
}
return $this->durationFormatter->format($seconds, $format);
@@ -253,7 +243,7 @@ class Extensions extends \Twig_Extension
*/
public function money($amount, $currency = null)
{
$locale = $this->getLocale();
$locale = $this->localeSettings->getLocale();
if ($this->locale !== $locale) {
$this->locale = $locale;
@@ -271,14 +261,6 @@ class Extensions extends \Twig_Extension
return $result;
}
/**
* @return string
*/
protected function getLocale()
{
return $this->requestStack->getCurrentRequest()->getLocale();
}
/**
* Takes the list of codes of the locales (languages) enabled in the
* application and returns an array with the name of each locale written
@@ -289,7 +271,7 @@ class Extensions extends \Twig_Extension
public function getLocales()
{
$locales = [];
foreach (array_keys($this->locales) as $locale) {
foreach ($this->localeSettings->getAvailableLanguages() as $locale) {
$locales[] = ['code' => $locale, 'name' => Intl::getLocaleBundle()->getLocaleName($locale, $locale)];
}

View File

@@ -0,0 +1,147 @@
<?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\Utils;
use Symfony\Component\HttpFoundation\RequestStack;
class LocaleSettings
{
/**
* @var array
*/
protected $settings;
/**
* @var string
*/
protected $locale = 'en';
/**
* @param RequestStack $requestStack
* @param array $languageSettings
*/
public function __construct(RequestStack $requestStack, array $languageSettings)
{
// It can be null in a console command
if (null !== $requestStack->getMasterRequest()) {
$this->locale = $requestStack->getMasterRequest()->getLocale();
}
$this->settings = $languageSettings;
}
/**
* Returns an array with all available locale/language codes.
*
* @return string[]
*/
public function getAvailableLanguages(): array
{
return array_keys($this->settings);
}
/**
* Returns the current locale used by the user in this request.
*
* @return string
*/
public function getLocale(): string
{
return $this->locale;
}
/**
* Returns the format which is used by the form component to handle date values.
*
* @param null|string $locale
* @return string
*/
public function getDateTypeFormat(?string $locale = null): string
{
return $this->getConfigByLocaleAndKey('date_type', $locale);
}
/**
* Returns the format which is used by the Javascript component to handle date values.
*
* @param null|string $locale
* @return string
*/
public function getDatePickerFormat(?string $locale = null): string
{
return $this->getConfigByLocaleAndKey('date_picker', $locale);
}
/**
* Returns the format which is used by the form component to handle datetime values.
*
* @param null|string $locale
* @return string
*/
public function getDateTimeTypeFormat(?string $locale = null): string
{
return $this->getConfigByLocaleAndKey('date_time_type', $locale);
}
/**
* Returns the format which is used by the Javascript component to handle datetime values.
*
* @param null|string $locale
* @return string
*/
public function getDateTimePickerFormat(?string $locale = null): string
{
return $this->getConfigByLocaleAndKey('date_time_picker', $locale);
}
/**
* Returns the locale specific date format, which should be used in combination with the twig filter "|date".
*
* @param null|string $locale
* @return string
*/
public function getDateFormat(?string $locale = null): string
{
return $this->getConfigByLocaleAndKey('date', $locale);
}
/**
* Returns the format used in the "|duration" twig filter to display a Timesheet duration.
*
* @param null|string $locale
* @return string
*/
public function getDurationFormat(?string $locale = null): string
{
return $this->getConfigByLocaleAndKey('duration', $locale);
}
/**
* @param string $key
* @param null|string $locale
* @return string
*/
protected function getConfigByLocaleAndKey(string $key, ?string $locale = null): string
{
if (null === $locale) {
$locale = $this->getLocale();
}
if (!isset($this->settings[$locale])) {
throw new \InvalidArgumentException(sprintf('Unknown locale given: %s', $locale));
}
if (!isset($this->settings[$locale][$key])) {
throw new \InvalidArgumentException(sprintf('Unknown setting for locale %s: %s', $locale, $key));
}
return $this->settings[$locale][$key];
}
}