Compare commits

..

9 Commits

39 changed files with 638 additions and 1064 deletions

View File

@@ -0,0 +1,5 @@
kind: DX
body: '[FilterOrderHelper] add entity choice and singleCheckbox'
time: 2023-06-23T12:24:08.133491895+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,5 @@
kind: Feature
body: '[activity list] add filtering for activities list'
time: 2023-06-23T12:25:30.49643551+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,6 @@
kind: Feature
body: '[activity list] in person context, show also the activities from the accompanying
periods where the person participates'
time: 2023-06-23T12:27:02.159041095+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,5 @@
kind: Feature
body: '[activity list] add pagination to the list of activities'
time: 2023-06-23T12:44:38.879098862+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,5 @@
kind: Feature
body: Allow filtering on the basis of a user within general tasks list
time: 2023-07-05T14:03:36.664880092+02:00
custom:
Issue: ""

View File

@@ -1,5 +0,0 @@
kind: Feature
body: '[export] Add a list for people with their associated course'
time: 2023-07-07T12:36:09.596469063+02:00
custom:
Issue: "125"

View File

@@ -1,6 +0,0 @@
kind: Feature
body: '[export] Add ordering by person''s lastname or course opening date in list
which concerns accompanying course or peoples'
time: 2023-07-07T12:41:32.112725962+02:00
custom:
Issue: ""

View File

@@ -1,5 +0,0 @@
kind: Feature
body: '[Export] allow to group activities by localisation'
time: 2023-07-11T15:00:55.770070399+02:00
custom:
Issue: "128"

View File

@@ -1,5 +0,0 @@
kind: Feature
body: '[export] Add a filter "filter course having an activity between two dates"'
time: 2023-07-11T15:59:29.065329834+02:00
custom:
Issue: "129"

View File

@@ -30,8 +30,6 @@ kinds:
auto: patch
- label: DX
auto: patch
- label: UX
auto: patch
newlines:
afterChangelogHeader: 1
beforeChangelogVersion: 1

View File

@@ -259,8 +259,8 @@ final class ActivityController extends AbstractController
$filterArgs = [
'my_activities' => $filter->getSingleCheckboxData('my_activities'),
'types' => $filter->hasEntityChoice('activity_types') ? $filter->getEntityChoiceData('activity_types') : [],
'jobs' => $filter->hasEntityChoice('jobs') ? $filter->getEntityChoiceData('jobs') : [],
'types' => $filter->getEntityChoiceData('activity_types'),
'jobs' => $filter->getEntityChoiceData('jobs'),
'before' => $filter->getDateRangeData('activity_date')['to'],
'after' => $filter->getDateRangeData('activity_date')['from'],
];
@@ -327,28 +327,21 @@ final class ActivityController extends AbstractController
$filterBuilder
->addDateRange('activity_date', 'activity.date')
->addSingleCheckbox('my_activities', 'activity_filter.My activities');
->addSingleCheckbox('my_activities', 'activity_filter.My activities')
->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [
'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) {
$text = match ($activityType->hasCategory()) {
true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ',
false => '',
};
if (1 < count($types)) {
$filterBuilder
->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [
'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) {
$text = match ($activityType->hasCategory()) {
true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ',
false => '',
};
return $text . $this->translatableStringHelper->localize($activityType->getName());
}
]);
}
if (1 < count($jobs)) {
$filterBuilder
->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [
'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel())
]);
}
return $text . $this->translatableStringHelper->localize($activityType->getName());
}
])
->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [
'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel())
])
;
return $filterBuilder->build();
}

View File

@@ -1,80 +0,0 @@
<?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\ActivityBundle\Export\Aggregator;
use Chill\ActivityBundle\Export\Declarations;
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Repository\LocationRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Closure;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
final readonly class ActivityLocationAggregator implements AggregatorInterface
{
public const KEY = 'activity_location_aggregator';
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('actloc', $qb->getAllAliases(), true)) {
$qb->leftJoin('activity.location', 'actloc');
}
$qb->addSelect(sprintf('actloc.name AS %s', self::KEY));
$qb->addGroupBy(self::KEY);
}
public function applyOn(): string
{
return Declarations::ACTIVITY;
}
public function buildForm(FormBuilderInterface $builder)
{
// no form required for this aggregator
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): Closure
{
return function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.activity.by_location.Activity Location';
}
if (null === $value || '' === $value) {
return '';
}
return $value;
};
}
public function getQueryKeys($data): array
{
return [self::KEY];
}
public function getTitle()
{
return 'export.aggregator.activity.by_location.Title';
}
}

View File

@@ -1,90 +0,0 @@
<?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\ActivityBundle\Export\Filter\ACPFilters;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInterface
{
public function __construct(
private RollingDateConverterInterface $rollingDateConverter,
) {
}
public function getTitle()
{
return 'export.filter.activity.course_having_activity_between_date.Title';
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('start_date', PickRollingDateType::class, [
'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity after'
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity before'
]);
}
public function getFormDefaultData(): array
{
return [
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY)
];
}
public function describeAction($data, $format = 'string')
{
return [
'export.filter.activity.course_having_activity_between_date.Only course having an activity between from and to',
[
'from' => $this->rollingDateConverter->convert($data['start_date']),
'to' => $this->rollingDateConverter->convert($data['end_date']),
]
];
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$alias = 'act_period_having_act_betw_date_alias';
$from = 'act_period_having_act_betw_date_start';
$to = 'act_period_having_act_betw_date_end';
$qb->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . Activity::class . " {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp"
)
);
$qb
->setParameter($from, $this->rollingDateConverter->convert($data['start_date']))
->setParameter($to, $this->rollingDateConverter->convert($data['end_date']));
}
public function applyOn()
{
return \Chill\PersonBundle\Export\Declarations::ACP_TYPE;
}
}

View File

@@ -135,10 +135,6 @@ services:
tags:
- { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' }
Chill\ActivityBundle\Export\Filter\ACPFilters\PeriodHavingActivityBetweenDatesFilter:
tags:
- { name: chill.export_filter, alias: 'period_having_activity_betw_dates_filter' }
## Aggregators
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator:
tags:
@@ -148,10 +144,6 @@ services:
tags:
- { name: chill.export_aggregator, alias: activity_common_type_aggregator }
Chill\ActivityBundle\Export\Aggregator\ActivityLocationAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_common_location_aggregator }
chill.activity.export.user_aggregator:
class: Chill\ActivityBundle\Export\Aggregator\ActivityUserAggregator
tags:

View File

@@ -1,5 +0,0 @@
export:
filter:
activity:
course_having_activity_between_date:
Only course having an activity between from and to: Seulement les parcours ayant reçu au moins un échange entre le {from, date, short} et le {to, date, short}

View File

@@ -96,6 +96,9 @@ activity_filter:
My activities: Mes échanges (où j'interviens)
Types: Par type d'échange
Jobs: Par métier impliqué
By: Filtrer par
Search: Chercher dans la liste
By date: Filtrer par date
#timeline
'%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"'
@@ -373,12 +376,6 @@ export:
by_usersscope:
Filter by users scope: Filtrer les échanges par services d'au moins un utilisateur participant
'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%'
course_having_activity_between_date:
Title: Filtre les parcours ayant reçu un échange entre deux dates
Receiving an activity after: Ayant reçu un échange après le
Receiving an activity before: Ayant reçu un échange avant le
aggregator:
activity:
by_sent_received:
@@ -386,9 +383,6 @@ export:
is sent: envoyé
is received: reçu
Group activity by sentreceived: Grouper les échanges par envoyé / reçu
by_location:
Activity Location: Localisation de l'échange
Title: Grouper les échanges par localisation de l'échange
generic_doc:
filter:

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

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Listing;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -39,7 +40,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
'label' => false,
'required' => false,
'attr' => [
'placeholder' => 'filter_order.Search',
'placeholder' => 'activity_filter.Search',
]
]);
}
@@ -47,7 +48,16 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
$checkboxesBuilder = $builder->create('checkboxes', null, ['compound' => true]);
foreach ($helper->getCheckboxes() as $name => $c) {
$choices = self::buildCheckboxChoices($c['choices'], $c['trans']);
$choices = array_combine(
array_map(static function ($c, $t) {
if (null !== $t) {
return $t;
}
return $c;
}, $c['choices'], $c['trans']),
$c['choices']
);
$checkboxesBuilder->add($name, ChoiceType::class, [
'choices' => $choices,
@@ -114,20 +124,28 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
$builder->add($singleCheckBoxBuilder);
}
}
public static function buildCheckboxChoices(array $choices, array $trans = []): array
{
return array_combine(
array_map(static function ($c, $t) {
if (null !== $t) {
return $t;
}
if ([] !== $helper->getUserPickers()) {
$userPickersBuilder = $builder->create('user_pickers', null, ['compound' => true]);
foreach ($helper->getUserPickers() as $name => [
'label' => $label, 'options' => $opts
]) {
$userPickersBuilder->add(
$name,
PickUserDynamicType::class,
[
'multiple' => true,
'label' => $label,
...$opts,
]
);
}
$builder->add($userPickersBuilder);
}
return $c;
}, $choices, $trans),
$choices
);
}
public function buildView(FormView $view, FormInterface $form, array $options)

View File

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

View File

@@ -1,120 +1,110 @@
{{ form_start(form) }}
<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>
{% endfor %}
{% endif %}
{% 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 %}
{% 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.single_checkboxes is defined %}
{% set btnSubmit = 1 %}
{% for name, _o in form.single_checkboxes %}
</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">
<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]) }}
{% 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>
</div>
</div>
{% endfor %}
{% 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 %}
{% 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 %}
</div>
</div>
{% 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>
{% 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">{{ 'activity_filter.By'|trans }}</div>
<div class="col-sm-8 pt-2">
{{ form_widget(form.single_checkboxes[name]) }}
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div>
{% 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 %}
{% for k,v in otherParameters %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
{{ form_end(form) }}

View File

@@ -1,84 +0,0 @@
<?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 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,
) {
}
/**
* 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->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

@@ -13,19 +13,16 @@ namespace Chill\MainBundle\Templating\Listing;
use Chill\MainBundle\Form\Type\Listing\FilterOrderType;
use DateTimeImmutable;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
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;
final class FilterOrderHelper
class FilterOrderHelper
{
private array $checkboxes = [];
@@ -36,12 +33,16 @@ final 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;
@@ -51,11 +52,17 @@ final class FilterOrderHelper
*/
private array $entityChoices = [];
/**
* @var array<string, array{label: string, options: array}>
*/
private array $userPickers = [];
public function __construct(
private readonly FormFactoryInterface $formFactory,
private readonly RequestStack $requestStack,
FormFactoryInterface $formFactory,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
}
public function addSingleCheckbox(string $name, string $label): self
@@ -80,16 +87,24 @@ final class FilterOrderHelper
return $this->entityChoices;
}
public function addUserPicker(string $name, ?string $label = null, array $options = []): self
{
$this->userPickers[$name] = ['label' => $label, 'options' => $options];
return $this;
}
public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self
{
if ([] === $trans) {
$trans = $choices;
}
$missing = count($choices) - count($trans) - 1;
$this->checkboxes[$name] = [
'choices' => $choices,
'default' => $default,
'trans' => $trans,
'choices' => $choices, 'default' => $default,
'trans' => array_merge(
$trans,
0 < $missing ?
array_fill(0, $missing, null) : []
),
...$options,
];
@@ -114,9 +129,14 @@ final class FilterOrderHelper
->handleRequest($this->requestStack->getCurrentRequest());
}
public function hasCheckboxData(string $name): bool
public function getUserPickers(): array
{
return array_key_exists($name, $this->checkboxes);
return $this->userPickers;
}
public function getUserPickerData(string $name): array
{
return $this->getFormData()['user_pickers'][$name];
}
public function getCheckboxData(string $name): array
@@ -124,21 +144,11 @@ final class FilterOrderHelper
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];
@@ -162,11 +172,6 @@ final class FilterOrderHelper
return $this->singleCheckbox;
}
public function hasDateRangeData(string $name): bool
{
return array_key_exists($name, $this->dateRanges);
}
/**
* @return array{to: ?DateTimeImmutable, from: ?DateTimeImmutable}
*/
@@ -203,7 +208,8 @@ final class FilterOrderHelper
'checkboxes' => [],
'dateRanges' => [],
'single_checkboxes' => [],
'entity_choices' => []
'entity_choices' => [],
'user_pickers' => []
];
if ($this->hasSearchBox()) {
@@ -227,7 +233,12 @@ final class FilterOrderHelper
$r['entity_choices'][$name] = ($c['options']['multiple'] ?? true) ? [] : null;
}
foreach ($this->userPickers as $name => $u) {
$r['user_pickers'][$name] = ($u['options']['multiple'] ?? true) ? [] : null;
}
return $r;
}
private function getFormData(): array

View File

@@ -14,8 +14,6 @@ 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
{
@@ -39,9 +37,14 @@ class FilterOrderHelperBuilder
*/
private array $entityChoices = [];
/**
* @var array<string, array{label: string, options: array}>
*/
private array $userPickers = [];
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
@@ -85,11 +88,18 @@ class FilterOrderHelperBuilder
return $this;
}
public function addUserPicker(string $name, ?string $label = null, ?array $options = []): self
{
$this->userPickers[$name] = ['label' => $label, 'options' => $options];
return $this;
}
public function build(): FilterOrderHelper
{
$helper = new FilterOrderHelper(
$this->formFactory,
$this->requestStack,
$this->requestStack
);
$helper->setSearchBox($this->searchBoxFields);
@@ -126,6 +136,17 @@ class FilterOrderHelperBuilder
$helper->addDateRange($name, $label, $from, $to);
}
foreach (
$this->userPickers as $name => [
'label' => $label,
'options' => $options
]
) {
$helper->addUserPicker($name, $label, $options);
}
return $helper;
}
}

View File

@@ -13,8 +13,6 @@ 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
{
@@ -24,7 +22,7 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;

View File

@@ -1,21 +0,0 @@
<?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';
}

View File

@@ -24,7 +24,6 @@ class Templating extends AbstractExtension
{
public function __construct(
private readonly RequestStack $requestStack,
private readonly FilterOrderGetActiveFilterHelper $filterOrderGetActiveFilterHelper,
) {
}
@@ -69,7 +68,6 @@ 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,12 +54,3 @@ 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

View File

@@ -29,7 +29,6 @@ use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
@@ -46,13 +45,95 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function strlen;
final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
{
private const FIELDS = [
'id',
'step',
'stepSince',
'openingDate',
'closingDate',
'referrer',
'referrerSince',
'administrativeLocation',
'locationIsPerson',
'locationIsTemp',
'locationPersonName',
'locationPersonId',
'origin',
'closingMotive',
'confidential',
'emergency',
'intensity',
'job',
'isRequestorPerson',
'isRequestorThirdParty',
'requestorPerson',
'requestorPersonId',
'requestorThirdParty',
'requestorThirdPartyId',
'scopes',
'socialIssues',
'createdAt',
'createdBy',
'updatedAt',
'updatedBy',
];
private ExportAddressHelper $addressHelper;
private DateTimeHelper $dateTimeHelper;
private EntityManagerInterface $entityManager;
private PersonRenderInterface $personRender;
private PersonRepository $personRepository;
private RollingDateConverterInterface $rollingDateConverter;
private SocialIssueRender $socialIssueRender;
private SocialIssueRepository $socialIssueRepository;
private ThirdPartyRender $thirdPartyRender;
private ThirdPartyRepository $thirdPartyRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
private UserHelper $userHelper;
public function __construct(
private EntityManagerInterface $entityManager,
private RollingDateConverterInterface $rollingDateConverter,
private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper,
ExportAddressHelper $addressHelper,
DateTimeHelper $dateTimeHelper,
EntityManagerInterface $entityManager,
PersonRenderInterface $personRender,
PersonRepository $personRepository,
ThirdPartyRepository $thirdPartyRepository,
ThirdPartyRender $thirdPartyRender,
SocialIssueRepository $socialIssueRepository,
SocialIssueRender $socialIssueRender,
TranslatableStringHelperInterface $translatableStringHelper,
TranslatorInterface $translator,
RollingDateConverterInterface $rollingDateConverter,
UserHelper $userHelper
) {
$this->addressHelper = $addressHelper;
$this->dateTimeHelper = $dateTimeHelper;
$this->entityManager = $entityManager;
$this->personRender = $personRender;
$this->personRepository = $personRepository;
$this->socialIssueRender = $socialIssueRender;
$this->socialIssueRepository = $socialIssueRepository;
$this->thirdPartyRender = $thirdPartyRender;
$this->thirdPartyRepository = $thirdPartyRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
$this->rollingDateConverter = $rollingDateConverter;
$this->userHelper = $userHelper;
}
public function buildForm(FormBuilderInterface $builder)
@@ -88,12 +169,141 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp
public function getLabels($key, array $values, $data)
{
return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data);
if (substr($key, 0, strlen('address_fields')) === 'address_fields') {
return $this->addressHelper->getLabel($key, $values, $data, 'address_fields');
}
switch ($key) {
case 'stepSince':
case 'openingDate':
case 'closingDate':
case 'referrerSince':
case 'createdAt':
case 'updatedAt':
return $this->dateTimeHelper->getLabel('export.list.acp.' . $key);
case 'origin':
case 'closingMotive':
case 'job':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR));
};
case 'locationPersonName':
case 'requestorPerson':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value || null === $person = $this->personRepository->find($value)) {
return '';
}
return $this->personRender->renderString($person, []);
};
case 'requestorThirdParty':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) {
return '';
}
return $this->thirdPartyRender->renderString($thirdparty, []);
};
case 'scopes':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return implode(
'|',
array_map(
fn ($s) => $this->translatableStringHelper->localize($s),
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
)
);
};
case 'socialIssues':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return implode(
'|',
array_map(
fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []),
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
)
);
};
case 'step':
return fn ($value) => match ($value) {
'_header' => 'export.list.acp.step',
null => '',
AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'),
AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'),
AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'),
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'),
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'),
default => $value,
};
case 'intensity':
return fn ($value) => match ($value) {
'_header' => 'export.list.acp.intensity',
null => '',
AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'),
AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'),
default => $value,
};
default:
return static function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return $value;
};
}
}
public function getQueryKeys($data)
{
return $this->listAccompanyingPeriodHelper->getQueryKeys($data);
return array_merge(
self::FIELDS,
$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')
);
}
public function getResult($query, $data)
@@ -131,12 +341,7 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp
->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT)
->setParameter('authorized_centers', $centers);
$this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date']));
$qb
->addOrderBy('acp.openingDate')
->addOrderBy('acp.closingDate')
->addOrderBy('acp.id');
$this->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date']));
return $qb;
}
@@ -152,4 +357,91 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp
Declarations::ACP_TYPE,
];
}
private function addSelectClauses(QueryBuilder $qb, DateTimeImmutable $calcDate): void
{
// add the regular fields
foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) {
$qb->addSelect(sprintf('acp.%s AS %s', $field, $field));
}
// add the field which are simple association
foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) {
$qb
->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t")
->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity));
}
// step at date
$qb
->addSelect('stepHistory.step AS step')
->addSelect('stepHistory.startDate AS stepSince')
->leftJoin('acp.stepHistories', 'stepHistory')
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte('stepHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate'))
)
);
// referree at date
$qb
->addSelect('referrer_t.label AS referrer')
->addSelect('userHistory.startDate AS referrerSince')
->leftJoin('acp.userHistories', 'userHistory')
->leftJoin('userHistory.user', 'referrer_t')
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('userHistory'),
$qb->expr()->andX(
$qb->expr()->lte('userHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate'))
)
)
);
// location of the acp
$qb
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson')
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp')
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName')
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId')
->leftJoin('acp.locationHistories', 'locationHistory')
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('locationHistory'),
$qb->expr()->andX(
$qb->expr()->lte('locationHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate'))
)
)
)
->leftJoin(
PersonHouseholdAddress::class,
'personAddress',
Join::WITH,
'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))'
)
->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id');
$this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields');
// requestor
$qb
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson')
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty')
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId')
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId')
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson')
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty');
$qb
// scopes
->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes')
// social issues
->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues');
// add parameter
$qb->setParameter('calcDate', $calcDate);
}
}

View File

@@ -35,12 +35,7 @@ use function count;
use function in_array;
use function strlen;
/**
* List the persons, having an accompanying period.
*
* Details of the accompanying period are not included
*/
class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface
class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface
{
private ExportAddressHelper $addressHelper;
@@ -190,11 +185,6 @@ class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterf
$this->listPersonHelper->addSelect($qb, $fields, $data['address_date']);
$qb
->addOrderBy('person.lastName')
->addOrderBy('person.firstName')
->addOrderBy('person.id');
return $qb;
}

View File

@@ -1,155 +0,0 @@
<?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\PersonBundle\Export\Export;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
use Chill\PersonBundle\Export\Helper\ListPersonHelper;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use DateTimeImmutable;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use function array_key_exists;
use function count;
use function in_array;
use function strlen;
/**
* List the persons having an accompanying period, with the accompanying period details
*
*/
final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInterface, GroupedExportInterface
{
public function __construct(
private ListPersonHelper $listPersonHelper,
private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper,
private EntityManagerInterface $entityManager,
private RollingDateConverterInterface $rollingDateConverter,
) {
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('address_date', PickRollingDateType::class, [
'label' => 'Data valid at this date',
'help' => 'Data regarding center, addresses, and so on will be computed at this date',
]);
}
public function getFormDefaultData(): array
{
return ['address_date' => new RollingDate(RollingDate::T_TODAY)];
}
public function getAllowedFormattersTypes()
{
return [FormatterInterface::TYPE_LIST];
}
public function getDescription()
{
return 'export.list.person_with_acp.Create a list of people having an accompaying periods with details of period, according to various filters.';
}
public function getGroup(): string
{
return 'Exports of persons';
}
public function getLabels($key, array $values, $data)
{
if (in_array($key, $this->listPersonHelper->getAllKeys(), true)) {
return $this->listPersonHelper->getLabels($key, $values, $data);
}
return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data);
}
public function getQueryKeys($data)
{
return array_merge(
$this->listPersonHelper->getAllKeys(),
$this->listAccompanyingPeriodHelper->getQueryKeys($data),
);
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR);
}
public function getTitle()
{
return 'export.list.person_with_acp.List peoples having an accompanying period with period details';
}
public function getType()
{
return Declarations::PERSON_TYPE;
}
/**
* param array{fields: string[], address_date: DateTimeImmutable} $data.
*/
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static fn ($el) => $el['center'], $acl);
$qb = $this->entityManager->createQueryBuilder();
$qb->from(Person::class, 'person')
->join('person.accompanyingPeriodParticipations', 'acppart')
->join('acppart.accompanyingPeriod', 'acp')
->andWhere($qb->expr()->neq('acp.step', "'" . AccompanyingPeriod::STEP_DRAFT . "'"))
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)'
)
)->setParameter('authorized_centers', $centers);
$this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date']));
$this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date']));
$qb
->addOrderBy('person.lastName')
->addOrderBy('person.firstName')
->addOrderBy('person.id')
->addOrderBy('acp.id');
return $qb;
}
public function requiredRole(): string
{
return PersonVoter::LISTS;
}
public function supportsModifiers()
{
return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE];
}
}

View File

@@ -1,317 +0,0 @@
<?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\PersonBundle\Export\Helper;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Export\Helper\DateTimeHelper;
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class ListAccompanyingPeriodHelper
{
public const FIELDS = [
'acpId',
'step',
'stepSince',
'openingDate',
'closingDate',
'referrer',
'referrerSince',
'administrativeLocation',
'locationIsPerson',
'locationIsTemp',
'locationPersonName',
'locationPersonId',
'origin',
'closingMotive',
'confidential',
'emergency',
'intensity',
'job',
'isRequestorPerson',
'isRequestorThirdParty',
'requestorPerson',
'requestorPersonId',
'requestorThirdParty',
'requestorThirdPartyId',
'scopes',
'socialIssues',
'acpCreatedAt',
'acpCreatedBy',
'acpUpdatedAt',
'acpUpdatedBy',
];
public function __construct(
private ExportAddressHelper $addressHelper,
private DateTimeHelper $dateTimeHelper,
private PersonRenderInterface $personRender,
private PersonRepository $personRepository,
private ThirdPartyRepository $thirdPartyRepository,
private ThirdPartyRender $thirdPartyRender,
private SocialIssueRepository $socialIssueRepository,
private SocialIssueRender $socialIssueRender,
private TranslatableStringHelperInterface $translatableStringHelper,
private TranslatorInterface $translator,
) {
}
public function getQueryKeys($data)
{
return array_merge(
ListAccompanyingPeriodHelper::FIELDS,
$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'acp_address_fields')
);
}
public function getLabels($key, array $values, $data)
{
if (str_starts_with($key, 'acp_address_fields')) {
return $this->addressHelper->getLabel($key, $values, $data, 'acp_address_fields');
}
switch ($key) {
case 'stepSince':
case 'openingDate':
case 'closingDate':
case 'referrerSince':
case 'acpCreatedAt':
case 'acpUpdatedAt':
return $this->dateTimeHelper->getLabel('export.list.acp.' . $key);
case 'origin':
case 'closingMotive':
case 'job':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR));
};
case 'locationPersonName':
case 'requestorPerson':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value || null === $person = $this->personRepository->find($value)) {
return '';
}
return $this->personRender->renderString($person, []);
};
case 'requestorThirdParty':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) {
return '';
}
return $this->thirdPartyRender->renderString($thirdparty, []);
};
case 'scopes':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return implode(
'|',
array_map(
fn ($s) => $this->translatableStringHelper->localize($s),
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
)
);
};
case 'socialIssues':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return implode(
'|',
array_map(
fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []),
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
)
);
};
case 'step':
return fn ($value) => match ($value) {
'_header' => 'export.list.acp.step',
null => '',
AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'),
AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'),
AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'),
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'),
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'),
default => $value,
};
case 'intensity':
return fn ($value) => match ($value) {
'_header' => 'export.list.acp.intensity',
null => '',
AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'),
AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'),
default => $value,
};
default:
return static function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return $value;
};
}
}
public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void
{
$qb->addSelect('acp.id AS acpId');
$qb->addSelect('acp.createdAt AS acpCreatedAt');
$qb->addSelect('acp.updatedAt AS acpUpdatedAt');
// add the regular fields
foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity'] as $field) {
$qb->addSelect(sprintf('acp.%s AS %s', $field, $field));
}
// add the field which are simple association
$qb
->leftJoin('acp.createdBy', "acp_created_by_t")
->addSelect('acp_created_by_t.label AS acpCreatedBy');
$qb
->leftJoin('acp.updatedBy', "acp_updated_by_t")
->addSelect('acp_updated_by_t.label AS acpUpdatedBy');
foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) {
$qb
->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t")
->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity));
}
// step at date
$qb
->addSelect('stepHistory.step AS step')
->addSelect('stepHistory.startDate AS stepSince')
->leftJoin('acp.stepHistories', 'stepHistory')
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte('stepHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate'))
)
);
// referree at date
$qb
->addSelect('referrer_t.label AS referrer')
->addSelect('userHistory.startDate AS referrerSince')
->leftJoin('acp.userHistories', 'userHistory')
->leftJoin('userHistory.user', 'referrer_t')
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('userHistory'),
$qb->expr()->andX(
$qb->expr()->lte('userHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate'))
)
)
);
// location of the acp
$qb
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson')
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp')
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName')
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId')
->leftJoin('acp.locationHistories', 'locationHistory')
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('locationHistory'),
$qb->expr()->andX(
$qb->expr()->lte('locationHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate'))
)
)
)
->leftJoin(
PersonHouseholdAddress::class,
'acpPersonAddress',
Join::WITH,
'locationHistory.personLocation = acpPersonAddress.person AND (acpPersonAddress.validFrom <= :calcDate AND (acpPersonAddress.validTo IS NULL OR acpPersonAddress.validTo > :calcDate))'
)
->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(acpPersonAddress.address)) = acp_address.id');
$this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'acp_address_fields');
// requestor
$qb
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson')
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty')
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId')
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId')
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson')
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty');
$qb
// scopes
->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes')
// social issues
->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues');
// add parameter
$qb->setParameter('calcDate', $calcDate);
}
}

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Helper;
use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\CivilityRepositoryInterface;
@@ -43,7 +42,7 @@ use function strlen;
class ListPersonHelper
{
public const FIELDS = [
'personId',
'id',
'civility',
'firstName',
'lastName',
@@ -115,26 +114,7 @@ class ListPersonHelper
}
/**
* Those keys are the "direct" keys, which are created when we decide to use to list all the keys.
*
* This method must be used in `getKeys` instead of the `self::FIELDS`
*
* @return array<string>
*/
public function getAllKeys(): array
{
return [
...array_filter(
ListPersonHelper::FIELDS,
fn (string $key) => !in_array($key, ['address_fields', 'lifecycleUpdate'], true)
),
...$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'),
...['createdAt', 'createdBy', 'updatedAt', 'updatedBy'],
];
}
/**
* @param array<value-of<self::FIELDS>> $fields
* @param array|value-of<self::FIELDS>[] $fields
*/
public function addSelect(QueryBuilder $qb, array $fields, DateTimeImmutable $computedDate): void
{
@@ -144,11 +124,6 @@ class ListPersonHelper
}
switch ($f) {
case 'personId':
$qb->addSelect('person.id AS personId');
break;
case 'countryOfBirth':
case 'nationality':
$qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f));
@@ -163,7 +138,25 @@ class ListPersonHelper
break;
case 'spokenLanguages':
$qb->addSelect('(SELECT AGGREGATE(language.id) FROM ' . Language::class . ' language WHERE language MEMBER OF person.spokenLanguages) AS spokenLanguages');
$qb
->leftJoin('person.spokenLanguages', 'spokenLanguage')
->addSelect('AGGREGATE(spokenLanguage.id) AS spokenLanguages')
->addGroupBy('person');
if (in_array('center', $fields, true)) {
$qb->addGroupBy('center');
}
if (in_array('address_fields', $fields, true)) {
$qb
->addGroupBy('address_fieldsid')
->addGroupBy('address_fieldscountry_t.id')
->addGroupBy('address_fieldspostcode_t.id');
}
if (in_array('household_id', $fields, true)) {
$qb->addGroupBy('household_id');
}
break;

View File

@@ -4,27 +4,35 @@ services:
autowire: true
## Indicators
Chill\PersonBundle\Export\Export\CountPerson:
chill.person.export.count_person:
class: Chill\PersonBundle\Export\Export\CountPerson
autowire: true
autoconfigure: true
tags:
- { name: chill.export, alias: count_person }
Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse:
chill.person.export.count_person_with_accompanying_course:
class: Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse
autowire: true
autoconfigure: true
tags:
- { name: chill.export, alias: count_person_with_accompanying_course }
Chill\PersonBundle\Export\Export\ListPerson:
autowire: true
autoconfigure: true
tags:
- { name: chill.export, alias: list_person }
Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod:
Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriod:
autowire: true
autoconfigure: true
tags:
- { name: chill.export, alias: list_person_with_acp }
Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriodDetails:
tags:
- { name: chill.export, alias: list_person_with_acp_details }
Chill\PersonBundle\Export\Export\ListAccompanyingPeriod:
autowire: true
autoconfigure: true
tags:
- { name: chill.export, alias: list_acp }

View File

@@ -998,8 +998,6 @@ notification:
Notify referrer: Notifier le référent
Notify any: Notifier d'autres utilisateurs
personId: Identifiant de l'usager
export:
export:
acp_stats:
@@ -1149,15 +1147,13 @@ export:
list:
person_with_acp:
List peoples having an accompanying period: Liste des usagers ayant un parcours d'accompagnement
List peoples having an accompanying period with period details: Liste des usagers concernés avec détail de chaque parcours
Create a list of people having an accompaying periods, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager
Create a list of people having an accompaying periods with details of period, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager. Ajoute les détails du parcours à la liste.
acp:
List of accompanying periods: Liste des parcours d'accompagnements
Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres.
Date of calculation for associated elements: Date de calcul des éléments associés
The associated referree, localisation, and other elements will be valid at this date: Les éléments associés, comme la localisation, le référent et d'autres éléments seront valides à cette date
acpId: Identifiant du parcours
id: Identifiant du parcours
openingDate: Date d'ouverture du parcours
closingDate: Date de fermeture du parcours
closingMotive: Motif de cloture
@@ -1165,14 +1161,14 @@ export:
confidential: Confidentiel
emergency: Urgent
intensity: Intensité
acpCreatedAt: Créé le
acpUpdatedAt: Dernière mise à jour le
createdAt: Créé le
updatedAt: Dernière mise à jour le
acpOrigin: Origine du parcours
origin: Origine du parcours
acpClosingMotive: Motif de fermeture
acpJob: Métier du parcours
acpCreatedBy: Créé par
acpUpdatedBy: Dernière modification par
createdBy: Créé par
updatedBy: Dernière modification par
administrativeLocation: Location administrative
step: Etape
stepSince: Dernière modification de l'étape
@@ -1180,7 +1176,7 @@ export:
referrerSince: Référent depuis le
locationIsPerson: Parcours localisé auprès d'un usager concerné
locationIsTemp: Parcours avec une localisation temporaire
locationPersonName: Usager auprès duquel le parcours est localisé
acpLocationPersonName: Usager auprès duquel le parcours est localisé
locationPersonId: Identifiant de l'usager auprès duquel le parcours est localisé
acpaddress_fieldscountry: Pays de l'adresse
isRequestorPerson: Le demandeur est-il un usager ?

View File

@@ -299,13 +299,17 @@ final class SingleTaskController extends AbstractController
$this->denyAccessUnlessGranted(TaskVoter::SHOW, null);
$filterOrder = $this->buildFilterOrder();
$filteredUsers = $filterOrder->getUserPickerData('userPicker');
$flags = array_merge(
$filterOrder->getCheckboxData('status'),
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
);
$nb = $this->singleTaskAclAwareRepository->countByAllViewable(
$filterOrder->getQueryString(),
$flags
$flags,
$filteredUsers
);
$paginator = $this->paginatorFactory->create($nb);
@@ -313,6 +317,7 @@ final class SingleTaskController extends AbstractController
$tasks = $this->singleTaskAclAwareRepository->findByAllViewable(
$filterOrder->getQueryString(),
$flags,
$filteredUsers,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
[
@@ -680,6 +685,7 @@ final class SingleTaskController extends AbstractController
->addSearchBox()
->addCheckbox('status', $statuses, $statuses, $statusTrans)
->addCheckbox('states', $states, ['new', 'in_progress'])
->addUserPicker('userPicker', 'Filter by user', ['multiple' => true, 'required' => false])
->build();
}

View File

@@ -51,7 +51,8 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
public function buildBaseQuery(
?string $pattern = null,
?array $flags = []
?array $flags = [],
?array $users = []
): QueryBuilder {
$qb = $this->em->createQueryBuilder();
$qb
@@ -62,6 +63,22 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
->setParameter('pattern', '%' . $pattern . '%');
}
if (count($users) > 0) {
$orXUser = $qb->expr()->orX();
foreach ($users as $key => $user) {
$orXUser->add(
$qb->expr()->eq('t.assignee', ':user_' . $key)
);
$qb->setParameter('user_' . $key, $user);
}
if ($orXUser->count() > 0) {
$qb->andWhere($orXUser);
}
}
if (count($flags) > 0) {
$orXDate = $qb->expr()->orX();
$orXState = $qb->expr()->orX();
@@ -183,9 +200,10 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
public function countByAllViewable(
?string $pattern = null,
?array $flags = []
?array $flags = [],
?array $users = []
): int {
$qb = $this->buildBaseQuery($pattern, $flags);
$qb = $this->buildBaseQuery($pattern, $flags, $users);
return $this
->addACLGlobal($qb)
@@ -231,11 +249,12 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
public function findByAllViewable(
?string $pattern = null,
?array $flags = [],
?array $users = [],
?int $start = 0,
?int $limit = 50,
?array $orderBy = []
): array {
$qb = $this->buildBaseQuery($pattern, $flags);
$qb = $this->buildBaseQuery($pattern, $flags, $users);
$qb = $this->addACLGlobal($qb);
return $this->getResult($qb, $start, $limit, $orderBy);

View File

@@ -18,7 +18,8 @@ interface SingleTaskAclAwareRepositoryInterface
{
public function countByAllViewable(
?string $pattern = null,
?array $flags = []
?array $flags = [],
?array $users = []
): int;
public function countByCourse(
@@ -38,6 +39,7 @@ interface SingleTaskAclAwareRepositoryInterface
public function findByAllViewable(
?string $pattern = null,
?array $flags = [],
?array $users = [],
?int $start = 0,
?int $limit = 50,
?array $orderBy = []

View File

@@ -27,8 +27,10 @@
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('page_task_list') }}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('page_task_list') }}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{% endblock %}

View File

@@ -65,6 +65,7 @@ Not assigned: Aucun utilisateur assigné
For person: Pour
By: Par
Any tasks: Aucune tâche
Filter by user: Filtrer par utilisateur(s)
# transitions - default task definition
"new": "nouvelle"