diff --git a/.changes/unreleased/Feature-20240129-133319.yaml b/.changes/unreleased/Feature-20240129-133319.yaml new file mode 100644 index 000000000..ff2b96ebf --- /dev/null +++ b/.changes/unreleased/Feature-20240129-133319.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: 'Add capability to generate export about change of steps of accompanying period, and generate exports for this' +time: 2024-01-29T13:33:19.190365565+01:00 +custom: + Issue: "244" diff --git a/exports_alias_conventions.md b/exports_alias_conventions.md index eb0545702..d4d034314 100644 --- a/exports_alias_conventions.md +++ b/exports_alias_conventions.md @@ -5,72 +5,74 @@ Add condition with distinct alias on each export join clauses (Indicators + Filt These are alias conventions : -| Entity | Join | Attribute | Alias | -|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:---------------------------------------| -| AccompanyingPeriod::class | | | acp | -| | AccompanyingPeriodWork::class | acp.works | acpw | -| | AccompanyingPeriodParticipation::class | acp.participations | acppart | -| | Location::class | acp.administrativeLocation | acploc | -| | ClosingMotive::class | acp.closingMotive | acpmotive | -| | UserJob::class | acp.job | acpjob | -| | Origin::class | acp.origin | acporigin | -| | Scope::class | acp.scopes | acpscope | -| | SocialIssue::class | acp.socialIssues | acpsocialissue | -| | User::class | acp.user | acpuser | -| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories | -| | AccompanyingPeriodInfo::class | not existing (using custom WITH clause) | acpinfo | -| AccompanyingPeriodWork::class | | | acpw | -| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval | -| | SocialAction::class | acpw.socialAction | acpwsocialaction | -| | Goal::class | acpw.goals | goal | -| | Result::class | acpw.results | result | -| AccompanyingPeriodParticipation::class | | | acppart | -| | Person::class | acppart.person | partperson | -| AccompanyingPeriodWorkEvaluation::class | | | workeval | -| | Evaluation::class | workeval.evaluation | eval | -| AccompanyingPeriodInfo::class | | | acpinfo | -| | User::class | acpinfo.user | acpinfo_user | -| Goal::class | | | goal | -| | Result::class | goal.results | goalresult | -| Person::class | | | person | -| | Center::class | person.center | center | -| | HouseholdMember::class | partperson.householdParticipations | householdmember | -| | MaritalStatus::class | person.maritalStatus | personmarital | -| | VendeePerson::class | | vp | -| | VendeePersonMineur::class | | vpm | -| | CurrentPersonAddress::class | person.currentPersonAddress | currentPersonAddress (on a given date) | -| ResidentialAddress::class | | | resaddr | -| | ThirdParty::class | resaddr.hostThirdParty | tparty | -| ThirdParty::class | | | tparty | -| | ThirdPartyCategory::class | tparty.categories | tpartycat | -| HouseholdMember::class | | | householdmember | -| | Household::class | householdmember.household | household | -| | Person::class | householdmember.person | memberperson | -| | | memberperson.center | membercenter | -| Household::class | | | household | -| | HouseholdComposition::class | household.compositions | composition | -| Activity::class | | | activity | -| | Person::class | activity.person | actperson | -| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp | -| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity | -| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity | -| | ActivityType::class | activity.activityType | acttype | -| | Location::class | activity.location | actloc | -| | SocialAction::class | activity.socialActions | actsocialaction | -| | SocialIssue::class | activity.socialIssues | actsocialssue | -| | ThirdParty::class | activity.thirdParties | acttparty | -| | User::class | activity.user | actuser | -| | User::class | activity.users | actusers | -| | ActivityReason::class | activity.reasons | actreasons | -| | Center::class | actperson.center | actcenter | -| | Person::class | activity.createdBy | actcreator | -| ActivityReason::class | | | actreasons | -| | ActivityReasonCategory::class | actreason.category | actreasoncat | -| Calendar::class | | | cal | -| | CancelReason::class | cal.cancelReason | calcancel | -| | Location::class | cal.location | calloc | -| | User::class | cal.user | caluser | -| VendeePerson::class | | | vp | -| | SituationProfessionelle::class | vp.situationProfessionelle | vpprof | -| | StatutLogement::class | vp.statutLogement | vplog | -| | TempsDeTravail::class | vp.tempsDeTravail | vptt | +| Entity | Join | Attribute | Alias | +|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:-------------------------------------------| +| AccompanyingPeriodStepHistory::class | | | acpstephistory (contexte ACP_STEP_HISTORY) | +| | AccompanyingPeriod::class | acpstephistory.period | acp | +| AccompanyingPeriod::class | | | acp | +| | AccompanyingPeriodWork::class | acp.works | acpw | +| | AccompanyingPeriodParticipation::class | acp.participations | acppart | +| | Location::class | acp.administrativeLocation | acploc | +| | ClosingMotive::class | acp.closingMotive | acpmotive | +| | UserJob::class | acp.job | acpjob | +| | Origin::class | acp.origin | acporigin | +| | Scope::class | acp.scopes | acpscope | +| | SocialIssue::class | acp.socialIssues | acpsocialissue | +| | User::class | acp.user | acpuser | +| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories | +| | AccompanyingPeriodInfo::class | not existing (using custom WITH clause) | acpinfo | +| AccompanyingPeriodWork::class | | | acpw | +| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval | +| | SocialAction::class | acpw.socialAction | acpwsocialaction | +| | Goal::class | acpw.goals | goal | +| | Result::class | acpw.results | result | +| AccompanyingPeriodParticipation::class | | | acppart | +| | Person::class | acppart.person | partperson | +| AccompanyingPeriodWorkEvaluation::class | | | workeval | +| | Evaluation::class | workeval.evaluation | eval | +| AccompanyingPeriodInfo::class | | | acpinfo | +| | User::class | acpinfo.user | acpinfo_user | +| Goal::class | | | goal | +| | Result::class | goal.results | goalresult | +| Person::class | | | person | +| | Center::class | person.center | center | +| | HouseholdMember::class | partperson.householdParticipations | householdmember | +| | MaritalStatus::class | person.maritalStatus | personmarital | +| | VendeePerson::class | | vp | +| | VendeePersonMineur::class | | vpm | +| | CurrentPersonAddress::class | person.currentPersonAddress | currentPersonAddress (on a given date) | +| ResidentialAddress::class | | | resaddr | +| | ThirdParty::class | resaddr.hostThirdParty | tparty | +| ThirdParty::class | | | tparty | +| | ThirdPartyCategory::class | tparty.categories | tpartycat | +| HouseholdMember::class | | | householdmember | +| | Household::class | householdmember.household | household | +| | Person::class | householdmember.person | memberperson | +| | | memberperson.center | membercenter | +| Household::class | | | household | +| | HouseholdComposition::class | household.compositions | composition | +| Activity::class | | | activity | +| | Person::class | activity.person | actperson | +| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp | +| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity | +| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity | +| | ActivityType::class | activity.activityType | acttype | +| | Location::class | activity.location | actloc | +| | SocialAction::class | activity.socialActions | actsocialaction | +| | SocialIssue::class | activity.socialIssues | actsocialssue | +| | ThirdParty::class | activity.thirdParties | acttparty | +| | User::class | activity.user | actuser | +| | User::class | activity.users | actusers | +| | ActivityReason::class | activity.reasons | actreasons | +| | Center::class | actperson.center | actcenter | +| | Person::class | activity.createdBy | actcreator | +| ActivityReason::class | | | actreasons | +| | ActivityReasonCategory::class | actreason.category | actreasoncat | +| Calendar::class | | | cal | +| | CancelReason::class | cal.cancelReason | calcancel | +| | Location::class | cal.location | calloc | +| | User::class | cal.user | caluser | +| VendeePerson::class | | | vp | +| | SituationProfessionelle::class | vp.situationProfessionelle | vpprof | +| | StatutLogement::class | vp.statutLogement | vplog | +| | TempsDeTravail::class | vp.tempsDeTravail | vptt | diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php index a2ced7fee..6acca44e3 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php @@ -62,7 +62,7 @@ class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle\Contr $workflow = $this->registry->get($accompanyingCourse); if ($workflow->can($accompanyingCourse, 'close')) { - $workflow->apply($accompanyingCourse, 'close'); + $workflow->apply($accompanyingCourse, 'close', ['closing_motive' => $form['closingMotive']->getData()]); $em->flush(); diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 3d5c0bc64..ebbfedeb3 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -98,6 +98,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac $loader->load('services/exports_accompanying_course.yaml'); $loader->load('services/exports_social_actions.yaml'); $loader->load('services/exports_evaluation.yaml'); + $loader->load('services/exports_accompanying_period_step_history.yaml'); } $loader->load('services/exports_household.yaml'); diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 9c88a4a69..4f9c480d0 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -1449,7 +1449,7 @@ class AccompanyingPeriod implements return $this; } - public function setStep(string $step): self + public function setStep(string $step, array $context = []): self { $previous = $this->step; @@ -1464,7 +1464,7 @@ class AccompanyingPeriod implements $history = new AccompanyingPeriodStepHistory(); $history->setStep($this->step)->setStartDate(new \DateTimeImmutable('now')); - $this->addStepHistory($history); + $this->addStepHistory($history, $context); } return $this; @@ -1507,11 +1507,14 @@ class AccompanyingPeriod implements return $this; } - private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory): self + private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory, array $context = []): self { if (!$this->stepHistories->contains($stepHistory)) { $this->stepHistories[] = $stepHistory; $stepHistory->setPeriod($this); + if (($context['closing_motive'] ?? null) instanceof ClosingMotive) { + $stepHistory->setClosingMotive($context['closing_motive']); + } $this->ensureStepContinuity(); } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php index 15b2f0d2e..c25c11e69 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php @@ -59,6 +59,13 @@ class AccompanyingPeriodStepHistory implements TrackCreationInterface, TrackUpda */ private string $step; + /** + * @ORM\ManyToOne(targetEntity=ClosingMotive::class) + * + * @ORM\JoinColumn(nullable=true) + */ + private ?ClosingMotive $closingMotive = null; + public function getEndDate(): ?\DateTimeImmutable { return $this->endDate; @@ -114,4 +121,16 @@ class AccompanyingPeriodStepHistory implements TrackCreationInterface, TrackUpda return $this; } + + public function getClosingMotive(): ?ClosingMotive + { + return $this->closingMotive; + } + + public function setClosingMotive(?ClosingMotive $closingMotive): self + { + $this->closingMotive = $closingMotive; + + return $this; + } } diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByClosingMotiveAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByClosingMotiveAggregator.php new file mode 100644 index 000000000..b5776ebeb --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByClosingMotiveAggregator.php @@ -0,0 +1,84 @@ +closingMotiveRepository->find((int) $value)) { + return ''; + } + + return $this->closingMotiveRender->renderString($closingMotive, []); + }; + } + + public function getQueryKeys($data) + { + return [ + self::KEY, + ]; + } + + public function getTitle() + { + return 'export.aggregator.step_history.by_closing_motive.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->addSelect('IDENTITY(acpstephistory.closingMotive) AS '.self::KEY) + ->addGroupBy(self::KEY); + } + + public function applyOn() + { + return Declarations::ACP_STEP_HISTORY; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByDateAggregator.php new file mode 100644 index 000000000..fbd80c7a5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByDateAggregator.php @@ -0,0 +1,90 @@ +add('frequency', ChoiceType::class, [ + 'choices' => array_combine( + array_map(fn (DateGroupingChoiceEnum $c) => 'export.enum.frequency.'.$c->value, DateGroupingChoiceEnum::cases()), + array_map(fn (DateGroupingChoiceEnum $c) => $c->value, DateGroupingChoiceEnum::cases()), + ), + 'label' => 'export.aggregator.course.by_opening_date.frequency', + 'multiple' => false, + 'expanded' => true, + ]); + } + + public function getFormDefaultData(): array + { + return ['frequency' => DateGroupingChoiceEnum::YEAR->value]; + } + + public function getLabels($key, array $values, mixed $data) + { + return function (?string $value): string { + if ('_header' === $value) { + return 'export.aggregator.step_history.by_date.header'; + } + + if (null === $value || '' === $value) { + return ''; + } + + return $value; + }; + } + + public function getQueryKeys($data) + { + return [self::KEY]; + } + + public function getTitle() + { + return 'export.aggregator.step_history.by_date.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $p = self::KEY; + + $qb->addSelect(sprintf("TO_CHAR(acpstephistory.startDate, '%s') AS {$p}", $data['frequency'])); + $qb->addGroupBy($p); + $qb->addOrderBy($p, 'DESC'); + } + + public function applyOn() + { + return Declarations::ACP_STEP_HISTORY; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByStepAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByStepAggregator.php new file mode 100644 index 000000000..005d012f9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByStepAggregator.php @@ -0,0 +1,86 @@ +translator->trans('accompanying_period.'.$step); + }; + } + + public function getQueryKeys($data) + { + return [ + self::KEY, + ]; + } + + public function getTitle() + { + return 'export.aggregator.step_history.by_step.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->addSelect('acpstephistory.step AS '.self::KEY) + ->addGroupBy(self::KEY); + } + + public function applyOn() + { + return Declarations::ACP_STEP_HISTORY; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Declarations.php b/src/Bundle/ChillPersonBundle/Export/Declarations.php index 4186861cb..83673234c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Declarations.php +++ b/src/Bundle/ChillPersonBundle/Export/Declarations.php @@ -18,6 +18,8 @@ abstract class Declarations { final public const ACP_TYPE = 'accompanying_period'; + final public const ACP_STEP_HISTORY = 'accompanying_period_step_history'; + final public const EVAL_TYPE = 'evaluation'; final public const HOUSEHOLD_TYPE = 'household'; diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourseStepHistory.php b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourseStepHistory.php new file mode 100644 index 000000000..4f620fa34 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourseStepHistory.php @@ -0,0 +1,139 @@ +repository = $em->getRepository(AccompanyingPeriod::class); + $this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; + } + + public function buildForm(FormBuilderInterface $builder): void + { + // Nothing to add here + } + + public function getFormDefaultData(): array + { + return []; + } + + public function getAllowedFormattersTypes(): array + { + return [FormatterInterface::TYPE_TABULAR]; + } + + public function getDescription(): string + { + return 'export.export.acp_closing.description'; + } + + public function getGroup(): string + { + return 'Exports of accompanying courses'; + } + + public function getLabels($key, array $values, $data) + { + if ('export_result' !== $key) { + throw new \LogicException("the key {$key} is not used by this export"); + } + + return fn ($value) => '_header' === $value ? $this->getTitle() : $value; + } + + public function getQueryKeys($data): array + { + return ['export_result']; + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); + } + + public function getTitle(): string + { + return 'export.export.acp_closing.title'; + } + + public function getType(): string + { + return Declarations::ACP_TYPE; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []): QueryBuilder + { + $centers = array_map(static fn ($el) => $el['center'], $acl); + + $qb = $this->em->createQueryBuilder() + ->select('COUNT(DISTINCT acpstephistory.id) As export_result') + ->from(AccompanyingPeriod\AccompanyingPeriodStepHistory::class, 'acpstephistory') + ->join('acpstephistory.period', 'acp'); + + $qb + ->leftJoin('acp.participations', 'acppart') + ->leftJoin('acppart.person', 'person'); + + if ($this->filterStatsByCenters) { + $qb + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person + AND acl_count_person_history.center IN (:authorized_centers) + ' + ) + ) + ->setParameter('authorized_centers', $centers); + } + + AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb); + + return $qb; + } + + public function requiredRole(): string + { + return AccompanyingPeriodVoter::STATS; + } + + public function supportsModifiers(): array + { + return [ + Declarations::ACP_TYPE, + Declarations::PERSON_TYPE, + Declarations::ACP_STEP_HISTORY, + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByDateFilter.php new file mode 100644 index 000000000..8026f4573 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByDateFilter.php @@ -0,0 +1,96 @@ +add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.step_history.by_date.start_date_label', + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.step_history.by_date.end_date_label', + ]); + } + + 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 [ + 'exports.filter.step_history.by_date.description', + [ + 'start_date' => $this->rollingDateConverter->convert($data['start_date']), + 'end_date' => $this->rollingDateConverter->convert($data['end_date']), + ], + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $startDate = 'acp_step_history_by_date_start_filter'; + $endDate = 'acp_step_history_by_date_end_filter'; + + $qb + ->andWhere( + "acpstephistory.startDate >= :{$startDate} AND (acpstephistory.endDate < :{$endDate} OR acpstephistory.endDate IS NULL)" + ) + ->setParameter($startDate, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($endDate, $this->rollingDateConverter->convert($data['end_date'])); + } + + public function applyOn() + { + return Declarations::ACP_STEP_HISTORY; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByStepFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByStepFilter.php new file mode 100644 index 000000000..337b27e8a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByStepFilter.php @@ -0,0 +1,87 @@ +add('steps', ChoiceType::class, [ + 'choices' => array_combine( + array_map(static fn (string $step): string => 'accompanying_period.'.$step, $steps), + $steps + ), + 'label' => 'export.filter.step_history.by_step.pick_steps', + 'multiple' => true, + 'expanded' => true, + ]); + } + + public function getFormDefaultData(): array + { + return [ + 'steps' => [], + ]; + } + + public function describeAction($data, $format = 'string') + { + return [ + 'export.filter.step_history.by_step.description', + [ + '%steps%' => implode(', ', array_map(fn (string $step) => $this->translator->trans('accompanying_period.'.$step), $data['steps'])), + ], + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->andWhere('acpstephistory.step IN (:acpstephistory_by_step_filter_steps)') + ->setParameter('acpstephistory_by_step_filter_steps', $data['steps']); + } + + public function applyOn() + { + return Declarations::ACP_STEP_HISTORY; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php index 5fdb51c15..9e383b789 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php @@ -312,4 +312,34 @@ final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase $this->assertNull($period->getRequestorPerson()); $this->assertNull($period->getRequestor()); } + + public function testSetStep(): void + { + $period = new AccompanyingPeriod(); + + $period->setStep(AccompanyingPeriod::STEP_CONFIRMED); + + self::assertEquals(AccompanyingPeriod::STEP_CONFIRMED, $period->getStep()); + self::assertCount(1, $period->getStepHistories()); + + $period->setStep(AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT); + + self::assertEquals(AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT, $period->getStep()); + self::assertCount(2, $period->getStepHistories()); + + $periodInactiveSteps = $period->getStepHistories()->filter(fn (AccompanyingPeriod\AccompanyingPeriodStepHistory $h) => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT === $h->getStep()); + self::assertCount(1, $periodInactiveSteps); + + $period->setStep(AccompanyingPeriod::STEP_CLOSED, ['closing_motive' => $closingMotive = new AccompanyingPeriod\ClosingMotive()]); + + self::assertEquals(AccompanyingPeriod::STEP_CLOSED, $period->getStep()); + self::assertCount(3, $period->getStepHistories()); + + $periodClosedSteps = $period->getStepHistories()->filter(fn (AccompanyingPeriod\AccompanyingPeriodStepHistory $h) => AccompanyingPeriod::STEP_CLOSED === $h->getStep()); + self::assertCount(1, $periodClosedSteps); + + $periodClosedStep = $periodClosedSteps->first(); + + self::assertSame($closingMotive, $periodClosedStep->getClosingMotive()); + } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByClosingMotiveAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByClosingMotiveAggregatorTest.php new file mode 100644 index 000000000..2ce63af34 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByClosingMotiveAggregatorTest.php @@ -0,0 +1,68 @@ +closingMotiveRender = self::$container->get(ClosingMotiveRender::class); + $this->closingMotiveRepository = self::$container->get(ClosingMotiveRepositoryInterface::class); + } + + public function getAggregator() + { + return new ByClosingMotiveAggregator( + $this->closingMotiveRepository, + $this->closingMotiveRender + ); + } + + public function getFormData() + { + return [ + [], + ]; + } + + public function getQueryBuilders() + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $qb = $em->createQueryBuilder() + ->select('COUNT(DISTINCT acpstephistory.id) As export_result') + ->from(AccompanyingPeriodStepHistory::class, 'acpstephistory') + ->join('acpstephistory.period', 'acp'); + + return [ + $qb, + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByDateAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByDateAggregatorTest.php new file mode 100644 index 000000000..74a30d1a6 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByDateAggregatorTest.php @@ -0,0 +1,61 @@ + DateGroupingChoiceEnum::YEAR->value, + ], + [ + 'frequency' => DateGroupingChoiceEnum::WEEK->value, + ], + [ + 'frequency' => DateGroupingChoiceEnum::MONTH->value, + ], + ]; + } + + public function getQueryBuilders() + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $qb = $em->createQueryBuilder() + ->select('COUNT(DISTINCT acpstephistory.id) As export_result') + ->from(AccompanyingPeriodStepHistory::class, 'acpstephistory') + ->join('acpstephistory.period', 'acp'); + + return [ + $qb, + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByStepAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByStepAggregatorTest.php new file mode 100644 index 000000000..65752fea2 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByStepAggregatorTest.php @@ -0,0 +1,58 @@ +get(EntityManagerInterface::class); + + $qb = $em->createQueryBuilder() + ->select('COUNT(DISTINCT acpstephistory.id) As export_result') + ->from(AccompanyingPeriodStepHistory::class, 'acpstephistory') + ->join('acpstephistory.period', 'acp'); + + return [ + $qb, + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Export/CountAccompanyingCourseStepHistoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Export/CountAccompanyingCourseStepHistoryTest.php new file mode 100644 index 000000000..19adee99f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Export/CountAccompanyingCourseStepHistoryTest.php @@ -0,0 +1,48 @@ +get(EntityManagerInterface::class); + + yield new CountAccompanyingCourseStepHistory($em, $this->getParameters(true)); + yield new CountAccompanyingCourseStepHistory($em, $this->getParameters(false)); + } + + public function getFormData(): array + { + return [[]]; + } + + public function getModifiersCombination() + { + return [[Declarations::ACP_TYPE], [Declarations::ACP_TYPE, Declarations::ACP_STEP_HISTORY]]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByDateFilterTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByDateFilterTest.php new file mode 100644 index 000000000..978bf9488 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByDateFilterTest.php @@ -0,0 +1,67 @@ +rollingDateConverter = self::$container->get(RollingDateConverterInterface::class); + } + + public function getFilter() + { + return new ByDateFilter($this->rollingDateConverter); + } + + public function getFormData() + { + return [ + [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ], + ]; + } + + public function getQueryBuilders() + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $qb = $em->createQueryBuilder() + ->select('COUNT(DISTINCT acpstephistory.id) As export_result') + ->from(AccompanyingPeriodStepHistory::class, 'acpstephistory') + ->join('acpstephistory.period', 'acp'); + + return [ + $qb, + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByStepFilterTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByStepFilterTest.php new file mode 100644 index 000000000..2924be7e7 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByStepFilterTest.php @@ -0,0 +1,66 @@ + [AccompanyingPeriod::STEP_CONFIRMED, AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG], + ], + [ + 'steps' => [AccompanyingPeriod::STEP_CLOSED], + ], + ]; + } + + public function getQueryBuilders() + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $qb = $em->createQueryBuilder() + ->select('COUNT(DISTINCT acpstephistory.id) As export_result') + ->from(AccompanyingPeriodStepHistory::class, 'acpstephistory') + ->join('acpstephistory.period', 'acp'); + + return [ + $qb, + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_period_step_history.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_period_step_history.yaml new file mode 100644 index 000000000..09da707c9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_period_step_history.yaml @@ -0,0 +1,31 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + # exports + Chill\PersonBundle\Export\Export\CountAccompanyingCourseStepHistory: + tags: + - { name: chill.export, alias: count_acpstephistory } + + # filters + Chill\PersonBundle\Export\Filter\AccompanyingPeriodStepHistoryFilters\ByDateFilter: + tags: + - { name: chill.export_filter, alias: acpstephistory_filter_by_date } + + Chill\PersonBundle\Export\Filter\AccompanyingPeriodStepHistoryFilters\ByStepFilter: + tags: + - { name: chill.export_filter, alias: acpstephistory_filter_by_step } + + # aggregators + Chill\PersonBundle\Export\Aggregator\AccompanyingPeriodStepHistoryAggregators\ByClosingMotiveAggregator: + tags: + - { name: chill.export_aggregator, alias: acpstephistory_agg_by_closing_motive } + + Chill\PersonBundle\Export\Aggregator\AccompanyingPeriodStepHistoryAggregators\ByDateAggregator: + tags: + - { name: chill.export_aggregator, alias: acpstephistory_agg_by_date } + + Chill\PersonBundle\Export\Aggregator\AccompanyingPeriodStepHistoryAggregators\ByStepAggregator: + tags: + - { name: chill.export_aggregator, alias: acpstephistory_agg_by_step } diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20240123161457.php b/src/Bundle/ChillPersonBundle/migrations/Version20240123161457.php new file mode 100644 index 000000000..4c19e6c8f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20240123161457.php @@ -0,0 +1,52 @@ +addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD closingMotive_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT FK_84D514AC504CB38D FOREIGN KEY (closingMotive_id) REFERENCES chill_person_accompanying_period_closingmotive (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT FK_84D514AC65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql(<<<'EOF' + WITH last_step AS ( + SELECT * FROM ( + SELECT *, rank() OVER (partition by period_id ORDER BY startdate DESC, id DESC) AS r FROM chill_person_accompanying_period_step_history cpapsh + ) as sq + WHERE r = 1 + ) + UPDATE chill_person_accompanying_period_step_history + SET closingMotive_id = chill_person_accompanying_period.closingmotive_id + FROM last_step, chill_person_accompanying_period + WHERE last_step.period_id = chill_person_accompanying_period_step_history.period_id AND chill_person_accompanying_period.id = chill_person_accompanying_period_step_history.period_id + AND last_step.step = 'CLOSED'; + EOF); + $this->addSql('CREATE INDEX IDX_84D514AC504CB38D ON chill_person_accompanying_period_step_history (closingMotive_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history DROP CONSTRAINT FK_84D514AC504CB38D'); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history DROP CONSTRAINT FK_84D514AC65FF1AEC'); + $this->addSql('DROP INDEX IDX_84D514AC504CB38D'); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history DROP closingMotive_id'); + } +} diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index f94f9d578..95cfe5b8e 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -136,14 +136,20 @@ 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: - work: - by_treating_agent: - Filtered by treating agent at date: >- - Les agents traitant au { agent_at, date, medium }, seulement {agents} course: not_having_address_reference: describe: >- Uniquement les parcours qui ne sont pas localisés à une adresse de référence, à la date du {date_calc, date, medium} + work: + by_treating_agent: + Filtered by treating agent at date: >- + Les agents traitant au { agent_at, date, medium }, seulement {agents} + + step_history: + by_date: + description: >- + Changements de statuts filtrés par date: après le { start_date, date, medium } (inclus), avant le { end_date, date, medium } + Les agents traitants au { agent_at, date, medium }, seulement {agents} diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index e4995d207..50c6a87d5 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -785,7 +785,9 @@ accompanying_period: dates_from_%opening_date%_to_%closing_date%: Ouvert du %opening_date% au %closing_date% DRAFT: Brouillon CONFIRMED: Confirmé - CLOSED: Clotûré + CLOSED: Clôturé + CONFIRMED_INACTIVE_SHORT: Hors file active + CONFIRMED_INACTIVE_LONG: Pré-archivé emergency: Urgent occasional: ponctuel regular: régulier @@ -985,6 +987,9 @@ export: YYYY-MM: par mois YYYY: par année export: + acp_closing: + title: Nombre de changements de statuts de parcours + description: Compte le nombre de changements de statuts de parcours. Cet export est indiqué pour obtenir le nombre de parcours ouverts ou fermés pendant une période de temps (un parcours pouvant être clôturé, puis ré-ouvert pendant la période de temps indiquée) acp_stats: avg_duration: Moyenne de la durée de participation de chaque usager concerné count_participations: Nombre de participations distinctes @@ -1037,6 +1042,18 @@ export: at_date: Date de calcul de l'adresse header: Code postal + step_history: + by_step: + title: Grouper les changements de statut du parcours par étape + header: Nouveau statut du parcours + by_date: + title: Grouper les changements de statut du parcours par date + header: Date du changement de statut du parcours + date_grouping_label: Grouper par + by_closing_motive: + title: Grouper les changements de statut du parcours par motif de clôture + header: Motif de clôture + course: by-user: title: Grouper les parcours par usager participant @@ -1128,6 +1145,16 @@ export: by_geog_unit: Filtered by person's geographical unit (based on address) computed at %datecalc%, only %units%: Filtré par unité géographique (sur base de l'adresse), calculé le %datecalc%, seulement %units% + step_history: + by_step: + title: Filtrer les changements de statut du parcours par étape + pick_steps: Nouvelles étapes + description: "Filtré par étape: seulement %steps%" + by_date: + title: Filtrer les changements de statut du parcours par date + start_date_label: Changements après le + end_date_label: Changements avant le + person: by_composition: Filter by household composition: Filtrer les usagers par composition du ménage