diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 376806292..ee6c308e0 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -188,6 +188,7 @@ Main location: Localisation principale Main scope: Cercle Main center: Centre user job: Métier de l'utilisateur +Job: Métier Choose a main center: Choisir un centre Choose a main scope: Choisir un cercle choose a job: Choisir un métier @@ -248,6 +249,7 @@ Country code: Code du pays # circles / scopes Choose the circle: Choisir le cercle +Scope: Service Scopes: Services #export diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EvaluationAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EvaluationAggregator.php new file mode 100644 index 000000000..f02368fde --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EvaluationAggregator.php @@ -0,0 +1,104 @@ +evaluationRepository = $evaluationRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + /** + * @inheritDoc + */ + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'Evaluation'; + } + + $e = $this->evaluationRepository->find($value); + + return $this->translatableStringHelper->localize( + $e->getTitle() + ); + }; + } + + /** + * @inheritDoc + */ + public function getQueryKeys($data): array + { + return ['evaluation_aggregator']; + } + + /** + * @inheritDoc + */ + public function buildForm(FormBuilderInterface $builder) + { + // no form + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return 'Group by evaluation'; + } + + /** + * @inheritDoc + */ + public function addRole() + { + return null; + } + + /** + * @inheritDoc + */ + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->join('acp.works', 'acpw') + ->join('acpw.accompanyingPeriodWorkEvaluations', 'we') + ; + + $qb->addSelect('IDENTITY(we.evaluation) AS evaluation_aggregator'); + + $groupby = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('evaluation_aggregator'); + } else { + $qb->groupBy('evaluation_aggregator'); + } + } + + /** + * @inheritDoc + */ + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OriginAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OriginAggregator.php new file mode 100644 index 000000000..2bb131435 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OriginAggregator.php @@ -0,0 +1,103 @@ +repository = $em->getRepository(Origin::class); + $this->translatableStringHelper = $translatableStringHelper; + } + + /** + * @inheritDoc + */ + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'Origin'; + } + + $o = $this->repository->find($value); + + return $this->translatableStringHelper->localize( + $o->getLabel() + ); + }; + } + + /** + * @inheritDoc + */ + public function getQueryKeys($data): array + { + return ['origin_aggregator']; + } + + /** + * @inheritDoc + */ + public function buildForm(FormBuilderInterface $builder) + { + // no form + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return 'Group by origin'; + } + + /** + * @inheritDoc + */ + public function addRole() + { + return null; + } + + /** + * @inheritDoc + */ + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->join('acp.origin', 'o'); + + $qb->addSelect('o.id AS origin_aggregator'); + + $groupby = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('origin_aggregator'); + } else { + $qb->groupBy('origin_aggregator'); + } + } + + /** + * @inheritDoc + */ + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialActionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialActionAggregator.php new file mode 100644 index 000000000..4be8c027d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialActionAggregator.php @@ -0,0 +1,99 @@ +actionRender = $actionRender; + $this->actionRepository = $actionRepository; + } + + /** + * @inheritDoc + */ + public function getLabels($key, array $values, $data) + { + return function($value) { + if ('_header' === $value) { + return 'Social action'; + } + + $sa = $this->actionRepository->find($value); + + return $this->actionRender->renderString($sa, []); + }; + } + + /** + * @inheritDoc + */ + public function getQueryKeys($data): array + { + return ['socialaction_aggregator']; + } + + /** + * @inheritDoc + */ + public function buildForm(FormBuilderInterface $builder) + { + // no form + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return 'Group by social action'; + } + + /** + * @inheritDoc + */ + public function addRole() + { + return null; + } + + /** + * @inheritDoc + */ + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->join('acp.works', 'acpw'); + + $qb->addSelect('IDENTITY(acpw.socialAction) AS socialaction_aggregator'); // DISTINCT ?? + + $groupby = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('socialaction_aggregator'); + } else { + $qb->groupBy('socialaction_aggregator'); + } + } + + /** + * @inheritDoc + */ + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialIssueAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialIssueAggregator.php new file mode 100644 index 000000000..fe2830d00 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialIssueAggregator.php @@ -0,0 +1,100 @@ +issueRepository = $issueRepository; + $this->issueRender = $issueRender; + } + + /** + * @inheritDoc + */ + public function getLabels($key, array $values, $data) + { + return function ($value): string { + + if ($value === '_header') { + return 'Social issues'; + } + + $i = $this->issueRepository->find($value); + + return $this->issueRender->renderString($i, []); + }; + } + + /** + * @inheritDoc + */ + public function getQueryKeys($data): array + { + return ['socialissue_aggregator']; + } + + /** + * @inheritDoc + */ + public function buildForm(FormBuilderInterface $builder) + { + // no form + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return 'Group by social issue'; + } + + /** + * @inheritDoc + */ + public function addRole() + { + return null; + } + + /** + * @inheritDoc + */ + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->join('acp.socialIssues', 'si'); + $qb->addSelect('si.id as socialissue_aggregator'); + + $groupBy = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('socialissue_aggregator'); + } else { + $qb->groupBy('socialissue_aggregator'); + } + } + + /** + * @inheritDoc + */ + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php new file mode 100644 index 000000000..9b21373a8 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php @@ -0,0 +1,142 @@ +translator = $translator; + } + + /** + * @inheritDoc + */ + public function getLabels($key, array $values, $data) + { + return function ($value): string { + switch ($value) { + + case AccompanyingPeriod::STEP_DRAFT: + return $this->translator->trans('Draft'); + + case AccompanyingPeriod::STEP_CONFIRMED: + return $this->translator->trans('Confirmed'); + + case AccompanyingPeriod::STEP_CLOSED: + return $this->translator->trans('Closed'); + + case '_header': + return 'Step'; + + default: + throw new LogicException(sprintf('The value %s is not valid', $value)); + } + }; + } + + /** + * @inheritDoc + */ + public function getQueryKeys($data): array + { + return ['step_aggregator']; + } + + /** + * @inheritDoc + */ + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('on_date', ChillDateType::class, [ + 'data' => new \DateTime(), + ]); + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return 'Group by step'; + } + + /** + * @inheritDoc + */ + public function addRole() + { + return null; + } + + /** + * @inheritDoc + */ + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->addSelect('acp.step AS step_aggregator'); + + $groupBy = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('step_aggregator'); + } else { + $qb->groupBy('step_aggregator'); + } + + // add date in where clause + $where = $qb->getDQLPart('where'); + + $clause = $qb->expr()->andX( + $qb->expr()->lte('acp.openingDate', ':ondate'), + $qb->expr()->orX( + $qb->expr()->gt('acp.closingDate', ':ondate'), + $qb->expr()->isNull('acp.closingDate') + ) + ); + + if ($where instanceof Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + $qb->setParameter('ondate', $data['on_date'], Types::DATE_MUTABLE); + } + + /** + * @inheritDoc + */ + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + /* + * TODO check if we need to add FilterInterface and DescribeAction Method to describe date filter ?? + * + public function describeAction($data, $format = 'string') + { + return [ + "Filtered by actives courses: active on %ondate%", [ + '%ondate%' => $data['on_date']->format('d-m-Y') + ] + ]; + } + */ +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php new file mode 100644 index 000000000..843464fb6 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php @@ -0,0 +1,101 @@ +jobRepository = $jobRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + /** + * @inheritDoc + */ + public function getLabels($key, array $values, $data) + { + return function($value): string { + if ($value === '_header') { + return 'Job'; + } + + $j = $this->jobRepository->find($value); + + return $this->translatableStringHelper->localize( + $j->getLabel() + ); + }; + } + + /** + * @inheritDoc + */ + public function getQueryKeys($data): array + { + return ['userjob_aggregator']; + } + + /** + * @inheritDoc + */ + public function buildForm(FormBuilderInterface $builder) + { + // no form + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return 'Group by user job'; + } + + /** + * @inheritDoc + */ + public function addRole() + { + return null; + } + + /** + * @inheritDoc + */ + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->join('acp.job', 'j'); + $qb->addSelect('IDENTITY(acp.job) AS userjob_aggregator'); + + $groupBy = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('userjob_aggregator'); + } else { + $qb->groupBy('userjob_aggregator'); + } + } + + /** + * @inheritDoc + */ + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserScopeAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserScopeAggregator.php new file mode 100644 index 000000000..3fef5b4a9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserScopeAggregator.php @@ -0,0 +1,99 @@ +scopeRepository = $scopeRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + /** + * @inheritDoc + */ + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ($value === '_header') { + return 'Scope'; + } + + $s = $this->scopeRepository->find($value); + + return $this->translatableStringHelper->localize($s->getName()); + }; + } + + /** + * @inheritDoc + */ + public function getQueryKeys($data): array + { + return ['userscope_aggregator']; + } + + /** + * @inheritDoc + */ + public function buildForm(FormBuilderInterface $builder) + { + // no form + } + + /** + * @inheritDoc + */ + public function getTitle(): string + { + return 'Group by user scope'; + } + + /** + * @inheritDoc + */ + public function addRole() + { + return null; + } + + /** + * @inheritDoc + */ + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->join('acp.scopes', 's'); + $qb->addSelect('s.id as userscope_aggregator'); + + $groupBy = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('userscope_aggregator'); + } else { + $qb->groupBy('userscope_aggregator'); + } + } + + /** + * @inheritDoc + */ + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CurrentUserJobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CurrentUserJobFilter.php index e3b63ff67..ddebbdc6f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CurrentUserJobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CurrentUserJobFilter.php @@ -20,23 +20,17 @@ use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Security\Core\Security; -use Symfony\Contracts\Translation\TranslatorInterface; class CurrentUserJobFilter implements FilterInterface { - - protected TranslatorInterface $translator; - private Security $security; private TranslatableStringHelper $translatableStringHelper; public function __construct( - TranslatorInterface $translator, TranslatableStringHelper $translatableStringHelper, Security $security ) { - $this->translator = $translator; $this->translatableStringHelper = $translatableStringHelper; $this->security = $security; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CurrentUserScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CurrentUserScopeFilter.php index 939a1368e..b41347026 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CurrentUserScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CurrentUserScopeFilter.php @@ -22,15 +22,9 @@ use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Security\Core\Security; -use Symfony\Contracts\Translation\TranslatorInterface; class CurrentUserScopeFilter implements FilterInterface { - /** - * @var TranslatorInterface - */ - protected $translator; - private Security $security; /** @@ -39,11 +33,9 @@ class CurrentUserScopeFilter implements FilterInterface private TranslatableStringHelper $translatableStringHelper; public function __construct( - TranslatorInterface $translator, TranslatableStringHelper $translatableStringHelper, Security $security ) { - $this->translator = $translator; $this->translatableStringHelper = $translatableStringHelper; $this->security = $security; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php index f47cc2ee1..8a18eb2fe 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php @@ -51,7 +51,7 @@ class GeographicalUnitStatFilter implements FilterInterface */ public function getTitle(): string { - return 'Filter by geographic unit'; + return 'Filter by geographical unit'; } /** diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php index bf0e5a7a5..3f61b017f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php @@ -11,15 +11,9 @@ use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Contracts\Translation\TranslatorInterface; class SocialActionFilter implements FilterInterface { - /** - * @var TranslatorInterface - */ - protected $translator; - /** * @var TranslatableStringHelper */ @@ -31,11 +25,9 @@ class SocialActionFilter implements FilterInterface private SocialActionRender $actionRender; public function __construct( - TranslatorInterface $translator, TranslatableStringHelper $translatableStringHelper, SocialActionRender $actionRender ) { - $this->translator = $translator; $this->translatableStringHelper = $translatableStringHelper; $this->actionRender = $actionRender; } diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml index 5a7b6c8d5..236c7fe96 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml @@ -149,4 +149,52 @@ services: tags: - { name: chill.export_filter, alias: accompanyingcourse_openbetweendates_filter } - ## Aggregators \ No newline at end of file + ## Aggregators + chill.person.export.aggregator_userscope: + class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\UserScopeAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_userscope_aggregator } + + chill.person.export.aggregator_userjob: + class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\UserJobAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_userjob_aggregator } + + chill.person.export.aggregator_socialissue: + class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\SocialIssueAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_socialissue_aggregator } + + chill.person.export.aggregator_step: + class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\StepAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_step_aggregator } + + chill.person.export.aggregator_socialaction: + class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\SocialActionAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_socialaction_aggregator } + + chill.person.export.aggregator_evaluation: + class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\EvaluationAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_evaluation_aggregator } + + chill.person.export.aggregator_origin: + class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\OriginAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_origin_aggregator } \ No newline at end of file diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index fdb38a041..be32253e0 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -203,6 +203,7 @@ This accompanying course is still a draft: Ce parcours est encore à l'état bro Associated peoples: Usagers concernés Resources: Interlocuteurs privilégiés Any requestor to this accompanying course: Aucun demandeur pour ce parcours +Social action: Action d'accompagnement Social actions: Actions d'accompagnement Last social actions: Les dernières actions d'accompagnement Social issue: Problématique sociale @@ -411,25 +412,34 @@ Having an accompanying period closed after this date: Ayant une période d'accom Filter by user scope: Filtrer par service du référent "Filtered by user main scope: only %scope%": "Filtré par service du référent: uniquement %scope%" +Group by user scope: Grouper par service du référent Filter by user job: Filtrer par métier du référent "Filtered by user job: only %job%": "Filtré par métier du référent: uniquement %job%" +Group by user job: Grouper par métier du référent Filter by social issue: Filtrer par problématiques sociales Accepted socialissues: Problématiques sociales "Filtered by socialissues: only %socialissues%": "Filtré par problématique sociale: uniquement %socialissues%" +Group by social issue: Grouper par problématiques sociales Filter by step: Filtrer par statut du parcours Accepted steps: Statuts +Step: Statut "Filtered by steps: only %step%": "Filtré par statut du parcours: uniquement %step%" +Group by step: Grouper par statut du parcours +Filter by geographical unit: Filtrer par zone géographique Filter by socialaction: Filtrer par action d'accompagnement Accepted socialactions: Actions d'accompagnement "Filtered by socialactions: only %socialactions%": "Filtré par action d'accompagnement: uniquement %socialactions%" +Group by social action: Grouper par action d'accompagnement Filter by evaluation: Filtrer par évaluation Accepted evaluations: Évaluations +Evaluation: Évaluation "Filtered by evaluations: only %evals%": "Filtré par évaluation: uniquement %evals%" +Group by evaluation: Grouper par évaluation Filter by activity type: Filtrer par type d'activité Accepted activitytypes: Types d'activités @@ -438,6 +448,7 @@ Accepted activitytypes: Types d'activités Filter by origin: Filtrer par origine du parcours Accepted origins: Origines "Filtered by origins: only %origins%": "Filtré par origine du parcours: uniquement %origins%" +Group by origin: Grouper par origine du parcours Filter by closing motive: Filtrer par motif de fermeture Accepted closingmotives: Motifs de clôture