Merge remote-tracking branch 'origin/master' into issue120_filter_social_actions

This commit is contained in:
2023-07-13 21:14:31 +02:00
85 changed files with 3318 additions and 974 deletions

View File

@@ -97,7 +97,7 @@ interface ExportInterface extends ExportElementInterface
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
* @param mixed $data The data from the export's form (as defined in `buildForm`)
*
* @return callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
* @return (callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
*/
public function getLabels($key, array $values, $data);

View File

@@ -40,7 +40,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
'label' => false,
'required' => false,
'attr' => [
'placeholder' => 'activity_filter.Search',
'placeholder' => 'filter_order.Search',
]
]);
}
@@ -48,16 +48,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
$checkboxesBuilder = $builder->create('checkboxes', null, ['compound' => true]);
foreach ($helper->getCheckboxes() as $name => $c) {
$choices = array_combine(
array_map(static function ($c, $t) {
if (null !== $t) {
return $t;
}
return $c;
}, $c['choices'], $c['trans']),
$c['choices']
);
$choices = self::buildCheckboxChoices($c['choices'], $c['trans']);
$checkboxesBuilder->add($name, ChoiceType::class, [
'choices' => $choices,
@@ -148,6 +139,20 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
}
public static function buildCheckboxChoices(array $choices, array $trans = []): array
{
return array_combine(
array_map(static function ($c, $t) {
if (null !== $t) {
return $t;
}
return $c;
}, $choices, $trans),
$choices
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
/** @var FilterOrderHelper $helper */

View File

@@ -44,13 +44,5 @@ form {
}
.chill_filter_order {
background: $gray-100; /*
border: 3px dashed $white;
background: repeating-linear-gradient(
-45deg,
$gray-100,
$gray-100 2px,
$white 2px,
$white 6px
); */
background: $gray-100;
}

View File

@@ -1,110 +1,144 @@
{{ form_start(form) }}
{% set btnSubmit = 0 %}
<div class="chill_filter_order container-xxl p-5 py-2 my-3">
<div class="row my-2">
{% if form.vars.has_search_box %}
<div class="col-sm-12">
<div class="input-group">
{{ form_widget(form.q) }}
<button type="submit" class="btn btn-misc"><i class="fa fa-search"></i></button>
</div>
</div>
{% endif %}
</div>
{% if form.dateRanges is defined %}
{% set btnSubmit = 1 %}
{% if form.dateRanges|length > 0 %}
{% for dateRangeName, _o in form.dateRanges %}
<div class="row my-2">
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %}
{{ form_label(form.dateRanges[dateRangeName])}}
{% else %}
<div class="col-sm-4 col-form-label">{{ 'activity_filter.By date'|trans }}</div>
{% endif %}
<div class="col-sm-8 pt-1">
<div class="input-group">
<span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span>
{{ form_widget(form.dateRanges[dateRangeName]['from']) }}
<span class="input-group-text">{{ 'chill_calendar.To'|trans }}</span>
{{ form_widget(form.dateRanges[dateRangeName]['to']) }}
<div class="accordion my-3" id="filterOrderAccordion">
<h2 class="accordion-header" id="filterOrderHeading">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#filterOrderCollapse" aria-expanded="true" aria-controls="filterOrderCollapse">
<strong><i class="fa fa-fw fa-filter"></i>Filtrer la liste</strong>
</button>
</h2>
<div class="accordion-collapse collapse" id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion">
{% set btnSubmit = 0 %}
<div class="accordion-body chill_filter_order container-xxl p-5 py-2">
<div class="row my-2">
{% if form.vars.has_search_box %}
<div class="col-sm-12">
<div class="input-group">
{{ form_widget(form.q) }}
<button type="submit" class="btn btn-misc"><i class="fa fa-search"></i></button>
</div>
</div>
{% endif %}
</div>
{% if form.dateRanges is defined %}
{% set btnSubmit = 1 %}
{% if form.dateRanges|length > 0 %}
{% for dateRangeName, _o in form.dateRanges %}
<div class="row my-2">
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %}
{{ form_label(form.dateRanges[dateRangeName])}}
{% else %}
<div class="col-sm-4 col-form-label">{{ 'filter_order.By date'|trans }}</div>
{% endif %}
<div class="col-sm-8 pt-1">
<div class="input-group">
<span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span>
{{ form_widget(form.dateRanges[dateRangeName]['from']) }}
<span class="input-group-text">{{ 'chill_calendar.To'|trans }}</span>
{{ form_widget(form.dateRanges[dateRangeName]['to']) }}
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}
{% if form.checkboxes is defined %}
{% set btnSubmit = 1 %}
{% if form.checkboxes|length > 0 %}
{% for checkbox_name, options in form.checkboxes %}
<div class="row my-2">
<div class="col-sm-4 col-form-label">{{ 'activity_filter.By'|trans }}</div>
<div class="col-sm-8 pt-2">
{% for c in form['checkboxes'][checkbox_name].children %}
{{ form_widget(c) }}
{{ form_label(c) }}
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if form.entity_choices is defined %}
{% set btnSubmit = 1 %}
{% if form.entity_choices |length > 0 %}
{% for checkbox_name, options in form.entity_choices %}
<div class="row my-2">
{% if form.entity_choices[checkbox_name].vars.label is not same as(false) %}
{{ form_label(form.entity_choices[checkbox_name])}}
{% endif %}
<div class="col-sm-8 pt-2">
{% for c in form['entity_choices'][checkbox_name].children %}
{{ form_widget(c) }}
{{ form_label(c) }}
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if form.user_pickers is defined %}
{% set btnSubmit = 1 %}
{% if form.user_pickers.children|length > 0 %}
{% for name, options in form.user_pickers %}
<div class="row my-2">
{% if form.user_pickers[name].vars.label is not same as(false) %}
{{ form_label(form.user_pickers[name]) }}
{% else %}
{{ form_label(form.user_pickers[name].vars.label) }}
{% endif %}
<div class="col-sm-8 pt-2">
{{ form_widget(form.user_pickers[name]) }}
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if form.single_checkboxes is defined %}
{% set btnSubmit = 1 %}
{% for name, _o in form.single_checkboxes %}
{% if form.checkboxes is defined %}
{% set btnSubmit = 1 %}
{% if form.checkboxes|length > 0 %}
{% for checkbox_name, options in form.checkboxes %}
<div class="row my-2">
<div class="col-sm-4 col-form-label">{{ 'filter_order.By'|trans }}</div>
<div class="col-sm-8 pt-2">
{% for c in form['checkboxes'][checkbox_name].children %}
{{ form_widget(c) }}
{{ form_label(c) }}
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if form.entity_choices is defined %}
{% set btnSubmit = 1 %}
{% if form.entity_choices |length > 0 %}
{% for checkbox_name, options in form.entity_choices %}
<div class="row my-2">
{% if form.entity_choices[checkbox_name].vars.label is not same as(false) %}
{{ form_label(form.entity_choices[checkbox_name])}}
{% endif %}
<div class="col-sm-8 pt-2">
{% for c in form['entity_choices'][checkbox_name].children %}
{{ form_widget(c) }}
{{ form_label(c) }}
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if form.user_pickers is defined %}
{% set btnSubmit = 1 %}
{% if form.user_pickers.children|length > 0 %}
{% for name, options in form.user_pickers %}
<div class="row my-2">
{% if form.user_pickers[name].vars.label is not same as(false) %}
{{ form_label(form.user_pickers[name]) }}
{% else %}
{{ form_label(form.user_pickers[name].vars.label) }}
{% endif %}
<div class="col-sm-8 pt-2">
{{ form_widget(form.user_pickers[name]) }}
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if form.single_checkboxes is defined %}
{% set btnSubmit = 1 %}
{% for name, _o in form.single_checkboxes %}
<div class="row my-2">
<div class="col-sm-4 col-form-label">{{ 'filter_order.By'|trans }}</div>
<div class="col-sm-8 pt-2">
{{ form_widget(form.single_checkboxes[name]) }}
</div>
</div>
{% endfor %}
{% endif %}
{% if btnSubmit == 1 %}
<div class="row my-2">
<div class="col-sm-4 col-form-label">{{ 'activity_filter.By'|trans }}</div>
<div class="col-sm-8 pt-2">
{{ form_widget(form.single_checkboxes[name]) }}
</div>
<button type="submit" class="btn btn-sm btn-misc"><i class="fa fa-fw fa-filter"></i>{{ 'Filter'|trans }}</button>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if btnSubmit == 1 %}
<div class="row my-2">
<button type="submit" class="btn btn-sm btn-misc"><i class="fa fa-fw fa-filter"></i>{{ 'Filter'|trans }}</button>
</div>
{% endif %}
</div>
</div>
{% for k,v in otherParameters %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
{% if active|length > 0 %}
<div class="activeFilters mt-3">
{% for f in active %}
<span class="badge rounded-pill bg-secondary ms-1 {{ f.position }} {{ f.name }}">
{%- if f.label != '' %}
<span class="text-dark">{{ f.label|trans }}&nbsp;: </span>
{% endif -%}
{%- if f.position == 'search_box' and f.value is not null %}
<span class="text-dark">{{ 'filter_order.search_box'|trans ~ ' :' }}</span>
{% endif -%}
{{ f.value}}{#
#}</span>
{% endfor %}
</div>
{% endif %}
<div>
</div>
</div>
{% for k,v in otherParameters %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
{{ form_end(form) }}

View File

@@ -195,10 +195,6 @@ class AuthorizationHelper implements AuthorizationHelperInterface
/**
* Return all reachable scope for a given user, center and role.
*
* @param Center|Center[] $center
*
* @return array|Scope[]
*/
public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array
{

View File

@@ -25,7 +25,8 @@ interface AuthorizationHelperForCurrentUserInterface
public function getReachableCenters(string $role, ?Scope $scope = null): array;
/**
* @param array|Center|Center[] $center
* @param list<Center>|Center $center
* @return list<Scope>
*/
public function getReachableScopes(string $role, $center): array;
public function getReachableScopes(string $role, array|Center $center): array;
}

View File

@@ -26,7 +26,8 @@ interface AuthorizationHelperInterface
public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array;
/**
* @param Center|list<Center> $center
* @param Center|array<Center> $center
* @return list<Scope>
*/
public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array;
}

View File

@@ -18,8 +18,12 @@ use UnexpectedValueException;
class RollingDateConverter implements RollingDateConverterInterface
{
public function convert(RollingDate $rollingDate): DateTimeImmutable
public function convert(?RollingDate $rollingDate): ?DateTimeImmutable
{
if (null === $rollingDate) {
return null;
}
switch ($rollingDate->getRoll()) {
case RollingDate::T_MONTH_CURRENT_START:
return $this->toBeginOfMonth($rollingDate->getPivotDate());

View File

@@ -15,5 +15,9 @@ use DateTimeImmutable;
interface RollingDateConverterInterface
{
public function convert(RollingDate $rollingDate): DateTimeImmutable;
/**
* @param RollingDate|null $rollingDate
* @return ($rollingDate is null ? null : DateTimeImmutable)
*/
public function convert(?RollingDate $rollingDate): ?DateTimeImmutable;
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Templating\Listing;
use Chill\MainBundle\Templating\Entity\UserRender;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class FilterOrderGetActiveFilterHelper
{
public function __construct(
private TranslatorInterface $translator,
private PropertyAccessorInterface $propertyAccessor,
private UserRender $userRender,
) {
}
/**
* Return all the data required to display the active filters
*
* @param FilterOrderHelper $filterOrderHelper
* @return array<array{label: string, value: string, position: string, name: string}>
*/
public function getActiveFilters(FilterOrderHelper $filterOrderHelper): array
{
$result = [];
if ($filterOrderHelper->hasSearchBox() && '' !== $filterOrderHelper->getQueryString()) {
$result[] = ['label' => '', 'value' => $filterOrderHelper->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q'];
}
foreach ($filterOrderHelper->getDateRanges() as $name => ['label' => $label]) {
$base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string)$label];
if (null !== ($from = $filterOrderHelper->getDateRangeData($name)['from'] ?? null)) {
$result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base];
}
if (null !== ($to = $filterOrderHelper->getDateRangeData($name)['to'] ?? null)) {
$result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base];
}
}
foreach ($filterOrderHelper->getCheckboxes() as $name => ['choices' => $choices, 'trans' => $trans]) {
$translatedChoice = array_combine($choices, [...$trans]);
foreach ($filterOrderHelper->getCheckboxData($name) as $keyChoice) {
$result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name];
}
}
foreach ($filterOrderHelper->getEntityChoices() as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) {
foreach ($filterOrderHelper->getEntityChoiceData($name) as $selected) {
if (is_callable($options['choice_label'])) {
$value = call_user_func($options['choice_label'], $selected);
} elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) {
$value = $this->propertyAccessor->getValue($selected, $options['choice_label']);
} else {
if (!$selected instanceof \Stringable) {
throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected)));
}
$value = (string)$selected;
}
$result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name];
}
}
foreach ($filterOrderHelper->getUserPickers() as $name => ['label' => $label, 'options' => $options]) {
foreach ($filterOrderHelper->getUserPickerData($name) as $user) {
$result[] = ['value' => $this->userRender->renderString($user, []), 'label' => (string) $label, 'position' => FilterOrderPositionEnum::UserPicker->value, 'name' => $name];
}
}
foreach ($filterOrderHelper->getSingleCheckbox() as $name => ['label' => $label]) {
if (true === $filterOrderHelper->getSingleCheckboxData($name)) {
$result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name];
}
}
return $result;
}
}

View File

@@ -20,6 +20,11 @@ use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_merge;
use function count;
@@ -34,16 +39,12 @@ class FilterOrderHelper
private array $dateRanges = [];
private FormFactoryInterface $formFactory;
public const FORM_NAME = 'f';
private array $formOptions = [];
private string $formType = FilterOrderType::class;
private RequestStack $requestStack;
private ?array $searchBoxFields = null;
private ?array $submitted = null;
@@ -59,11 +60,9 @@ class FilterOrderHelper
private array $userPickers = [];
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
private readonly FormFactoryInterface $formFactory,
private readonly RequestStack $requestStack,
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
}
public function addSingleCheckbox(string $name, string $label): self
@@ -98,14 +97,14 @@ class FilterOrderHelper
public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self
{
$missing = count($choices) - count($trans) - 1;
if ([] === $trans) {
$trans = $choices;
}
$this->checkboxes[$name] = [
'choices' => $choices, 'default' => $default,
'trans' => array_merge(
$trans,
0 < $missing ?
array_fill(0, $missing, null) : []
),
'choices' => $choices,
'default' => $default,
'trans' => $trans,
...$options,
];
@@ -135,21 +134,39 @@ class FilterOrderHelper
return $this->userPickers;
}
public function getUserPickerData(string $name)
/**
* @return list<User>
*/
public function getUserPickerData(string $name): array
{
return $this->getFormData()['user_pickers'][$name];
}
public function hasCheckboxData(string $name): bool
{
return array_key_exists($name, $this->checkboxes);
}
public function getCheckboxData(string $name): array
{
return $this->getFormData()['checkboxes'][$name];
}
public function hasSingleCheckboxData(string $name): bool
{
return array_key_exists($name, $this->singleCheckbox);
}
public function getSingleCheckboxData(string $name): ?bool
{
return $this->getFormData()['single_checkboxes'][$name];
}
public function hasEntityChoice(string $name): bool
{
return array_key_exists($name, $this->entityChoices);
}
public function getEntityChoiceData($name): mixed
{
return $this->getFormData()['entity_choices'][$name];
@@ -173,6 +190,11 @@ class FilterOrderHelper
return $this->singleCheckbox;
}
public function hasDateRangeData(string $name): bool
{
return array_key_exists($name, $this->dateRanges);
}
/**
* @return array{to: ?DateTimeImmutable, from: ?DateTimeImmutable}
*/
@@ -239,7 +261,6 @@ class FilterOrderHelper
}
return $r;
}
private function getFormData(): array

View File

@@ -14,6 +14,8 @@ namespace Chill\MainBundle\Templating\Listing;
use DateTimeImmutable;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class FilterOrderHelperBuilder
{
@@ -44,7 +46,7 @@ class FilterOrderHelperBuilder
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
RequestStack $requestStack,
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
@@ -99,7 +101,7 @@ class FilterOrderHelperBuilder
{
$helper = new FilterOrderHelper(
$this->formFactory,
$this->requestStack
$this->requestStack,
);
$helper->setSearchBox($this->searchBoxFields);

View File

@@ -13,6 +13,8 @@ namespace Chill\MainBundle\Templating\Listing;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface
{
@@ -22,7 +24,7 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
RequestStack $requestStack,
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Templating\Listing;
enum FilterOrderPositionEnum: string
{
case SearchBox = 'search_box';
case Checkboxes = 'checkboxes';
case DateRange = 'date_range';
case EntityChoice = 'entity_choice';
case SingleCheckbox = 'single_checkbox';
case UserPicker = 'user_picker';
}

View File

@@ -24,6 +24,7 @@ class Templating extends AbstractExtension
{
public function __construct(
private readonly RequestStack $requestStack,
private readonly FilterOrderGetActiveFilterHelper $filterOrderGetActiveFilterHelper,
) {
}
@@ -68,6 +69,7 @@ class Templating extends AbstractExtension
return $environment->render($template, [
'helper' => $helper,
'active' => $this->filterOrderGetActiveFilterHelper->getActiveFilters($helper),
'form' => $helper->buildForm()->createView(),
'options' => $options,
'otherParameters' => $otherParameters,

View File

@@ -54,3 +54,12 @@ duration:
few {# minutes}
other {# minutes}
}
filter_order:
by_date:
From: Depuis le {from_date, date, long}
To: Jusqu'au {to_date, date, long}
By: Filtrer par
Search: Chercher dans la liste
By date: Filtrer par date
search_box: Filtrer par contenu