diff --git a/.changes/unreleased/Feature-20240830-104731.yaml b/.changes/unreleased/Feature-20240830-104731.yaml new file mode 100644 index 000000000..83b750d6a --- /dev/null +++ b/.changes/unreleased/Feature-20240830-104731.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Add export aggregator to aggregate activities by household + filter persons + that are not part of an accompanyingperiod during a certain timeframe. +time: 2024-08-30T10:47:31.29306704+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/HouseholdAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/HouseholdAggregator.php new file mode 100644 index 000000000..ab32c278e --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/HouseholdAggregator.php @@ -0,0 +1,99 @@ +householdRepository->find($value)) { + return ''; + } + + return $household->getId(); + }; + } + + public function getQueryKeys($data) + { + return ['activity_household_agg']; + } + + public function getTitle() + { + return 'export.aggregator.person.by_household.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->join( + HouseholdMember::class, + 'activity_household_agg_household_member', + Join::WITH, + $qb->expr()->andX( + $qb->expr()->eq('activity_household_agg_household_member.person', 'activity.person'), + $qb->expr()->lte('activity_household_agg_household_member.startDate', 'activity.date'), + $qb->expr()->orX( + $qb->expr()->gte('activity_household_agg_household_member.endDate', 'activity.date'), + $qb->expr()->isNull('activity_household_agg_household_member.endDate') + ) + ) + ); + + $qb->join( + Household::class, + 'activity_household_agg_household', + Join::WITH, + $qb->expr()->eq('activity_household_agg_household_member.household', 'activity_household_agg_household') + ); + + $qb + ->addSelect('activity_household_agg_household.id AS activity_household_agg') + ->addGroupBy('activity_household_agg'); + } + + public function applyOn() + { + return Declarations::ACTIVITY_PERSON; + } +} diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php index f7b5f3a8a..de1713542 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php @@ -19,6 +19,7 @@ use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\GroupedExportInterface; use Chill\MainBundle\Export\ListInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; +use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Export\Declarations as PersonDeclarations; use Doctrine\DBAL\Exception\InvalidArgumentException; use Doctrine\ORM\EntityManagerInterface; @@ -44,6 +45,7 @@ class ListActivity implements ListInterface, GroupedExportInterface 'person_firstname', 'person_lastname', 'person_id', + 'household_id', ]; private readonly bool $filterStatsByCenters; @@ -189,19 +191,26 @@ class ListActivity implements ListInterface, GroupedExportInterface { $centers = array_map(static fn ($el) => $el['center'], $acl); - // throw an error if any fields are present + // throw an error if no fields are present if (!\array_key_exists('fields', $data)) { - throw new InvalidArgumentException('Any fields have been checked.'); + throw new InvalidArgumentException('No fields have been checked.'); } $qb = $this->entityManager->createQueryBuilder(); $qb ->from('ChillActivityBundle:Activity', 'activity') - ->join('activity.person', 'actperson'); + ->join('activity.person', 'person') + ->join( + HouseholdMember::class, + 'householdmember', + Query\Expr\Join::WITH, + 'person = householdmember.person AND householdmember.startDate <= activity.date AND (householdmember.endDate IS NULL OR householdmember.endDate > activity.date)' + ) + ->join('householdmember.household', 'household'); if ($this->filterStatsByCenters) { - $qb->join('actperson.centerHistory', 'centerHistory'); + $qb->join('person.centerHistory', 'centerHistory'); $qb->where( $qb->expr()->andX( $qb->expr()->lte('centerHistory.startDate', 'activity.date'), @@ -224,17 +233,22 @@ class ListActivity implements ListInterface, GroupedExportInterface break; case 'person_firstname': - $qb->addSelect('actperson.firstName AS person_firstname'); + $qb->addSelect('person.firstName AS person_firstname'); break; case 'person_lastname': - $qb->addSelect('actperson.lastName AS person_lastname'); + $qb->addSelect('person.lastName AS person_lastname'); break; case 'person_id': - $qb->addSelect('actperson.id AS person_id'); + $qb->addSelect('person.id AS person_id'); + + break; + + case 'household_id': + $qb->addSelect('household.id AS household_id'); break; @@ -284,7 +298,7 @@ class ListActivity implements ListInterface, GroupedExportInterface return ActivityStatsVoter::LISTS; } - public function supportsModifiers() + public function supportsModifiers(): array { return [ Declarations::ACTIVITY, diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php index 1a006ba97..2d3282ad1 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php @@ -73,7 +73,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt $qb->andWhere( $qb->expr()->exists( - 'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod" + 'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp" ) ); diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php index eb2312c0e..2f734d237 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php @@ -39,7 +39,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem return null; } - public function alterQuery(QueryBuilder $qb, $data) + public function alterQuery(QueryBuilder $qb, $data): void { // create a subquery for activity $sqb = $qb->getEntityManager()->createQueryBuilder(); @@ -121,7 +121,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem ]; } - public function describeAction($data, $format = 'string') + public function describeAction($data, $format = 'string'): array { return [ [] === $data['reasons'] ? @@ -141,7 +141,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem ]; } - public function getTitle() + public function getTitle(): string { return 'export.filter.activity.person_between_dates.title'; } diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index b52911295..eb2d106f9 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -243,3 +243,7 @@ services: Chill\ActivityBundle\Export\Aggregator\PersonAggregators\PersonAggregator: tags: - { name: chill.export_aggregator, alias: activity_person_agg } + + Chill\ActivityBundle\Export\Aggregator\PersonAggregators\HouseholdAggregator: + tags: + - { name: chill.export_aggregator, alias: activity_household_agg } diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 7e72bc016..a4d73728b 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -428,6 +428,9 @@ export: by_person: title: Grouper les échanges par usager (dossier d'usager dans lequel l'échange est enregistré) person: Usager + by_household: + title: Grouper les échanges par ménage + household: Identifiant ménage acp: by_activity_type: title: Grouper les parcours par type d'échange diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutParticipationBetweenDatesFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutParticipationBetweenDatesFilter.php new file mode 100644 index 000000000..d1e3e9b2f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutParticipationBetweenDatesFilter.php @@ -0,0 +1,90 @@ +andWhere( + $qb->expr()->not( + $qb->expr()->exists( + 'SELECT 1 FROM '.AccompanyingPeriodParticipation::class." {$p}_acp JOIN {$p}_acp.accompanyingPeriod {$p}_acpp ". + "WHERE {$p}_acp.person = person ". + "AND OVERLAPSI({$p}_acp.startDate, {$p}_acp.endDate), (:{$p}_date_after, :{$p}_date_before) = TRUE ". + "AND OVERLAPSI({$p}_acpp.openingDate, {$p}_acpp.closingDate), (:{$p}_date_after, :{$p}_date_before) = TRUE" + ) + ) + ) + ->setParameter("{$p}_date_after", $this->rollingDateConverter->convert($data['date_after']), Types::DATE_IMMUTABLE) + ->setParameter("{$p}_date_before", $this->rollingDateConverter->convert($data['date_before']), Types::DATE_IMMUTABLE); + } + + public function applyOn(): string + { + return Declarations::PERSON_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('date_after', PickRollingDateType::class, [ + 'label' => 'export.filter.person.without_participation_between_dates.date_after', + ]); + + $builder->add('date_before', PickRollingDateType::class, [ + 'label' => 'export.filter.person.without_participation_between_dates.date_before', + ]); + } + + public function getFormDefaultData(): array + { + return [ + 'date_after' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'date_before' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function describeAction($data, $format = 'string') + { + return ['exports.filter.person.without_participation_between_dates.Filtered by having no participations during period: between', [ + 'dateafter' => $this->rollingDateConverter->convert($data['date_after']), + 'datebefore' => $this->rollingDateConverter->convert($data['date_before']), + ]]; + } + + public function getTitle() + { + return 'export.filter.person.without_participation_between_dates.title'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/PersonFilters/WithoutParticipationBetweenDatesFilterTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/PersonFilters/WithoutParticipationBetweenDatesFilterTest.php new file mode 100644 index 000000000..09d809bf8 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/PersonFilters/WithoutParticipationBetweenDatesFilterTest.php @@ -0,0 +1,62 @@ +filter = self::getContainer()->get(WithoutParticipationBetweenDatesFilter::class); + } + + public function getFilter() + { + return $this->filter; + } + + public static function getFormData(): array + { + return [ + [ + 'date_after' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'date_before' => new RollingDate(RollingDate::T_TODAY), + ], + ]; + } + + public static function getQueryBuilders(): iterable + { + self::bootKernel(); + $em = self::getContainer()->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('person.id') + ->from(Person::class, 'person'), + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index 263e172d0..fe8116b04 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -124,6 +124,10 @@ services: tags: - { name: chill.export_filter, alias: person_with_participation_between_dates_filter } + Chill\PersonBundle\Export\Filter\PersonFilters\WithoutParticipationBetweenDatesFilter: + tags: + - { name: chill.export_filter, alias: person_without_participation_between_dates_filter } + ## Aggregators chill.person.export.aggregator_nationality: class: Chill\PersonBundle\Export\Aggregator\PersonAggregators\NationalityAggregator diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index fdc79e4a0..9b2edf573 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -136,6 +136,9 @@ exports: Filtered by person\'s geographical unit (based on address) computed at date, only units: "Filtré par zone géographique sur base de l'adresse, calculé à {datecalc, date, short}, seulement les zones suivantes: {units}" filter: + person: + without_participation_between_dates: + "Filtered by having no participations during period: between": "Uniquement les usagers qui n'ont été concerné par aucun parcours entre le {dateafter, date, short} et le {datebefore, date, short}" course: not_having_address_reference: describe: >- diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 490927679..ba642c883 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1184,6 +1184,10 @@ export: date_before: Concerné par un parcours avant le title: Filtrer les usagers ayant été associés à un parcours ouverts un jour dans la période de temps indiquée 'Filtered by participations during period: between %dateafter% and %datebefore%': 'Filtré par personne concerné par un parcours dans la periode entre: %dateafter% et %datebefore%' + without_participation_between_dates: + date_after: Après le + date_before: Avant le + title: Filtrer les usagers n'ayant été associés à aucun parcours course: not_having_address_reference: