configure display of customer, project and activity in dropdown lists (#3151)

This commit is contained in:
Kevin Papst
2022-02-14 17:43:05 +01:00
committed by GitHub
parent 83a894823f
commit 7da7468965
17 changed files with 508 additions and 40 deletions

View File

@@ -14,12 +14,15 @@ use App\Event\SystemConfigurationEvent;
use App\Form\Model\Configuration;
use App\Form\Model\SystemConfiguration as SystemConfigurationModel;
use App\Form\SystemConfigurationForm;
use App\Form\Type\ActivityTypePatternType;
use App\Form\Type\ArrayToCommaStringType;
use App\Form\Type\CustomerTypePatternType;
use App\Form\Type\DatePickerType;
use App\Form\Type\DateTimeTextType;
use App\Form\Type\DayTimeType;
use App\Form\Type\LanguageType;
use App\Form\Type\MinuteIncrementType;
use App\Form\Type\ProjectTypePatternType;
use App\Form\Type\RoundingModeType;
use App\Form\Type\SkinType;
use App\Form\Type\TimezoneType;
@@ -496,6 +499,24 @@ final class SystemConfigurationController extends AbstractController
->setLabel('currency')
->setType(CurrencyType::class)
->setOptions(['help' => 'default_value_new']),
(new Configuration())
->setName('customer.choice_pattern')
->setLabel('choice_pattern')
->setType(CustomerTypePatternType::class),
]),
(new SystemConfigurationModel('project'))
->setConfiguration([
(new Configuration())
->setName('project.choice_pattern')
->setLabel('choice_pattern')
->setType(ProjectTypePatternType::class),
]),
(new SystemConfigurationModel('activity'))
->setConfiguration([
(new Configuration())
->setName('activity.choice_pattern')
->setLabel('choice_pattern')
->setType(ActivityTypePatternType::class),
]),
(new SystemConfigurationModel('user'))
->setConfiguration([

View File

@@ -34,6 +34,10 @@ class ActivitiesSubscriber extends AbstractActionsSubscriber
$event->addCreate($this->path('admin_activity_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'activity']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('activity.html'));
}
}

View File

@@ -34,6 +34,10 @@ class CustomersSubscriber extends AbstractActionsSubscriber
$event->addCreate($this->path('admin_customer_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'customer']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('customer.html'));
}
}

View File

@@ -35,6 +35,10 @@ class ProjectsSubscriber extends AbstractActionsSubscriber
$event->addCreate($this->path('admin_project_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'project']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('project.html'));
}
}

View File

@@ -9,6 +9,7 @@
namespace App\Form\Type;
use App\Configuration\SystemConfiguration;
use App\Entity\Activity;
use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityFormTypeQuery;
@@ -22,6 +23,44 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
class ActivityType extends AbstractType
{
public const PATTERN_NAME = '{name}';
public const PATTERN_COMMENT = '{comment}';
public const PATTERN_SPACER = '{spacer}';
public const SPACER = ' - ';
private $configuration;
private $pattern;
public function __construct(SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
public function getChoiceLabel(Activity $activity): string
{
if ($this->pattern === null) {
$this->pattern = $this->configuration->find('activity.choice_pattern');
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
$this->pattern = self::PATTERN_NAME;
}
}
$name = $this->pattern;
$name = str_replace(self::PATTERN_NAME, $activity->getName(), $name);
$name = str_replace(self::PATTERN_COMMENT, $activity->getComment(), $name);
$name = str_replace(self::PATTERN_SPACER, self::SPACER, $name);
$name = ltrim($name, self::SPACER);
$name = rtrim($name, self::SPACER);
if ($name === '' || $name === self::SPACER) {
$name = $activity->getName();
}
return $name;
}
/**
* {@inheritdoc}
*/
@@ -35,32 +74,18 @@ class ActivityType extends AbstractType
}
/**
* {@inheritdoc}
*/
public function choiceLabel(Activity $activity)
{
return $activity->getName();
}
/**
* @param Activity $choiceValue
* @param Activity $activity
* @param string $key
* @param mixed $value
* @return array
*/
public function choiceAttr($choiceValue, $key, $value)
public function getChoiceAttributes(Activity $activity, $key, $value)
{
$project = null;
if (!($choiceValue instanceof Activity)) {
return [];
if (null !== ($project = $activity->getProject())) {
return ['data-project' => $project->getId(), 'data-currency' => $project->getCustomer()->getCurrency()];
}
if (null !== $choiceValue->getProject()) {
$project = $choiceValue->getProject()->getId();
}
return ['data-project' => $project];
return [];
}
/**
@@ -76,9 +101,9 @@ class ActivityType extends AbstractType
],
'label' => 'label.activity',
'class' => Activity::class,
'choice_label' => [$this, 'choiceLabel'],
'choice_label' => [$this, 'getChoiceLabel'],
'choice_attr' => [$this, 'getChoiceAttributes'],
'group_by' => [$this, 'groupBy'],
'choice_attr' => [$this, 'choiceAttr'],
'query_builder_for_user' => true,
// @var Project|Project[]|int|int[]|null
'projects' => null,

View File

@@ -0,0 +1,55 @@
<?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\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Select the pattern that will be used when rendering an activity select.
*/
class ActivityTypePatternType extends AbstractType
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$name = $this->translator->trans('label.name');
$comment = $this->translator->trans('label.description');
$spacer = ActivityType::SPACER;
$resolver->setDefaults([
'label' => 'label.choice_pattern',
'choices' => [
$name => ActivityType::PATTERN_NAME,
$comment => ActivityType::PATTERN_COMMENT,
$name . $spacer . $comment => ActivityType::PATTERN_NAME . ActivityType::PATTERN_SPACER . ActivityType::PATTERN_COMMENT,
]
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
}

View File

@@ -9,6 +9,7 @@
namespace App\Form\Type;
use App\Configuration\SystemConfiguration;
use App\Entity\Customer;
use App\Repository\CustomerRepository;
use App\Repository\Query\CustomerFormTypeQuery;
@@ -23,6 +24,53 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
class CustomerType extends AbstractType
{
public const PATTERN_NAME = '{name}';
public const PATTERN_NUMBER = '{number}';
public const PATTERN_COMPANY = '{company}';
public const PATTERN_COMMENT = '{comment}';
public const PATTERN_SPACER = '{spacer}';
public const SPACER = ' - ';
private $configuration;
private $pattern;
public function __construct(SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
public function getChoiceLabel(Customer $customer): string
{
if ($this->pattern === null) {
$this->pattern = $this->configuration->find('customer.choice_pattern');
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
$this->pattern = self::PATTERN_NAME;
}
}
$name = $this->pattern;
$name = str_replace(self::PATTERN_NAME, $customer->getName(), $name);
$name = str_replace(self::PATTERN_COMMENT, $customer->getComment(), $name);
$name = str_replace(self::PATTERN_NUMBER, $customer->getNumber(), $name);
$name = str_replace(self::PATTERN_COMPANY, $customer->getCompany() ?? $customer->getName(), $name);
$name = str_replace(self::PATTERN_SPACER, self::SPACER, $name);
$name = ltrim($name, self::SPACER);
$name = rtrim($name, self::SPACER);
if ($name === '' || $name === self::SPACER) {
$name = $customer->getName();
}
return $name;
}
public function getChoiceAttributes(Customer $customer, $key, $value): array
{
return ['data-currency' => $customer->getCurrency()];
}
/**
* {@inheritdoc}
*/
@@ -36,7 +84,8 @@ class CustomerType extends AbstractType
],
'label' => 'label.customer',
'class' => Customer::class,
'choice_label' => 'name',
'choice_label' => [$this, 'getChoiceLabel'],
'choice_attr' => [$this, 'getChoiceAttributes'],
'query_builder_for_user' => true,
'project_enabled' => false,
'project_select' => 'project',

View File

@@ -0,0 +1,65 @@
<?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\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Select the pattern that will be used when rendering a custom select.
*/
class CustomerTypePatternType extends AbstractType
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$name = $this->translator->trans('label.name');
$company = $this->translator->trans('label.company');
$number = $this->translator->trans('label.number');
$comment = $this->translator->trans('label.description');
$spacer = CustomerType::SPACER;
$resolver->setDefaults([
'label' => 'label.choice_pattern',
'choices' => [
$name => CustomerType::PATTERN_NAME,
$company => CustomerType::PATTERN_COMPANY,
$number => CustomerType::PATTERN_NUMBER,
$comment => CustomerType::PATTERN_COMMENT,
$name . $spacer . $company => CustomerType::PATTERN_NAME . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMPANY,
$name . $spacer . $number => CustomerType::PATTERN_NAME . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_NUMBER,
$name . $spacer . $comment => CustomerType::PATTERN_NAME . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMMENT,
$number . $spacer . $name => CustomerType::PATTERN_NUMBER . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_NAME,
$number . $spacer . $company => CustomerType::PATTERN_NUMBER . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMPANY,
$number . $spacer . $comment => CustomerType::PATTERN_NUMBER . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMMENT,
$company . $spacer . $comment => CustomerType::PATTERN_COMPANY . CustomerType::PATTERN_SPACER . CustomerType::PATTERN_COMMENT,
]
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
}

View File

@@ -9,10 +9,12 @@
namespace App\Form\Type;
use App\Configuration\SystemConfiguration;
use App\Entity\Project;
use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityQuery;
use App\Repository\Query\ProjectFormTypeQuery;
use App\Utils\LocaleSettings;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
@@ -23,25 +25,93 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
class ProjectType extends AbstractType
{
public const PATTERN_NAME = '{name}';
public const PATTERN_COMMENT = '{comment}';
public const PATTERN_ORDERNUMBER = '{ordernumber}';
public const PATTERN_DATERANGE = '{daterange}';
public const PATTERN_START = '{start}';
public const PATTERN_END = '{end}';
public const PATTERN_SPACER = '{spacer}';
public const SPACER = ' - ';
private $configuration;
private $localeSettings;
private $dateFormat;
private $pattern;
public function __construct(SystemConfiguration $configuration, LocaleSettings $localeSettings)
{
$this->configuration = $configuration;
$this->localeSettings = $localeSettings;
}
public function getChoiceLabel(Project $project): string
{
if ($this->dateFormat === null) {
$this->dateFormat = $this->localeSettings->getDateFormat();
}
if ($this->pattern === null) {
$this->pattern = $this->configuration->find('project.choice_pattern');
if ($this->pattern === null || stripos($this->pattern, '{') === false || stripos($this->pattern, '}') === false) {
$this->pattern = self::PATTERN_NAME;
}
}
$dateRange = '';
if ($project->getStart() !== null) {
$dateRange = self::PATTERN_START;
}
if ($project->getEnd() !== null) {
if ($dateRange !== '') {
$dateRange .= '-';
}
$dateRange .= self::PATTERN_END;
}
$start = '';
if ($project->getStart() !== null) {
$start = $project->getStart()->format($this->dateFormat);
}
$end = '';
if ($project->getEnd() !== null) {
$end = $project->getEnd()->format($this->dateFormat);
}
$name = $this->pattern;
$name = str_replace(self::PATTERN_NAME, $project->getName(), $name);
$name = str_replace(self::PATTERN_COMMENT, $project->getComment(), $name);
$name = str_replace(self::PATTERN_ORDERNUMBER, $project->getOrderNumber(), $name);
$name = str_replace(self::PATTERN_DATERANGE, $dateRange, $name);
$name = str_replace(self::PATTERN_START, $start, $name);
$name = str_replace(self::PATTERN_END, $end, $name);
$name = str_replace(self::PATTERN_SPACER, self::SPACER, $name);
$name = ltrim($name, self::SPACER);
$name = rtrim($name, self::SPACER);
if ($name === '' || $name === self::SPACER) {
$name = $project->getName();
}
return $name;
}
/**
* @param Project $choiceValue
* @param Project $project
* @param string $key
* @param mixed $value
* @return array
*/
public function choiceAttr($choiceValue, $key, $value)
public function getChoiceAttributes(Project $project, $key, $value): array
{
$customer = null;
if (!($choiceValue instanceof Project)) {
return [];
if (null !== ($customer = $project->getCustomer())) {
return ['data-customer' => $customer->getId(), 'data-currency' => $customer->getCurrency()];
}
if (null !== $choiceValue->getCustomer()) {
$customer = $choiceValue->getCustomer()->getId();
}
return ['data-customer' => $customer];
return [];
}
/**
@@ -57,8 +127,8 @@ class ProjectType extends AbstractType
],
'label' => 'label.project',
'class' => Project::class,
'choice_label' => 'name',
'choice_attr' => [$this, 'choiceAttr'],
'choice_label' => [$this, 'getChoiceLabel'],
'choice_attr' => [$this, 'getChoiceAttributes'],
'group_by' => function (Project $project, $key, $index) {
return $project->getCustomer()->getName();
},

View File

@@ -0,0 +1,61 @@
<?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\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Select the pattern that will be used when rendering a project select.
*/
class ProjectTypePatternType extends AbstractType
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$name = $this->translator->trans('label.name');
$comment = $this->translator->trans('label.description');
$orderNumber = $this->translator->trans('label.orderNumber');
$projectStart = $this->translator->trans('label.project_start');
$projectEnd = $this->translator->trans('label.project_end');
$spacer = ProjectType::SPACER;
$resolver->setDefaults([
'label' => 'label.choice_pattern',
'choices' => [
$name => ProjectType::PATTERN_NAME,
$comment => ProjectType::PATTERN_COMMENT,
$name . $spacer . $orderNumber => ProjectType::PATTERN_NAME . ProjectType::PATTERN_SPACER . ProjectType::PATTERN_ORDERNUMBER,
$name . $spacer . $comment => ProjectType::PATTERN_NAME . ProjectType::PATTERN_SPACER . ProjectType::PATTERN_COMMENT,
$name . $spacer . $projectStart . '-' . $projectEnd => ProjectType::PATTERN_NAME . ProjectType::PATTERN_SPACER . ProjectType::PATTERN_DATERANGE,
]
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
}

View File

@@ -18,13 +18,27 @@ use Symfony\Component\HttpFoundation\RequestStack;
*/
final class LocaleSettings extends LocaleFormats
{
private $requestStack;
private $locale;
public function __construct(RequestStack $requestStack, LanguageFormattings $formats)
{
$locale = Constants::DEFAULT_LOCALE;
// request is null in a console command
if (null !== $requestStack->getMasterRequest()) {
$locale = $requestStack->getMasterRequest()->getLocale();
parent::__construct($formats, Constants::DEFAULT_LOCALE);
$this->requestStack = $requestStack;
}
public function getLocale(): string
{
if ($this->locale === null) {
$locale = \Locale::getDefault();
// request is null in a console command
if (null !== $this->requestStack->getMasterRequest()) {
$locale = $this->requestStack->getMasterRequest()->getLocale();
}
$this->locale = $locale;
}
parent::__construct($formats, $locale);
return $this->locale;
}
}