diff --git a/exports_alias_conventions.md b/exports_alias_conventions.md index 62fc745d8..fd7844691 100644 --- a/exports_alias_conventions.md +++ b/exports_alias_conventions.md @@ -5,69 +5,70 @@ 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 | -| AccompanyingPeriodWork::class | | | acpw | -| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval | -| | User::class | acpw.referrers | acpwuser | -| | 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 | -| 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 | -| 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 | +|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:---------------------------------------| +| 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 | +| AccompanyingPeriodWork::class | | | acpw | +| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval | +| | User::class | acpw.referrers | acpwuser | +| | 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 | +| 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/phpstan-critical.neon b/phpstan-critical.neon index 1dc516834..bfbb2dc7c 100644 --- a/phpstan-critical.neon +++ b/phpstan-critical.neon @@ -5,11 +5,6 @@ parameters: count: 1 path: src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php - - - message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Entity\\\\Household\\\\PersonHouseholdAddress\\:\\:\\$relation\\.$#" - count: 1 - path: src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php - - message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Entity\\\\AccompanyingPeriod\\:\\:\\$work\\.$#" count: 1 @@ -30,11 +25,6 @@ parameters: count: 1 path: src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php - - - message: "#^Undefined variable\\: \\$choiceSlug$#" - count: 1 - path: src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php - - message: "#^Undefined variable\\: \\$choiceSlug$#" count: 1 diff --git a/phpstan-types.neon b/phpstan-types.neon index a0493ce0b..1aae06880 100644 --- a/phpstan-types.neon +++ b/phpstan-types.neon @@ -10,16 +10,6 @@ parameters: count: 1 path: src/Bundle/ChillActivityBundle/Entity/ActivityReasonCategory.php - - - message: "#^Method Chill\\\\ActivityBundle\\\\Export\\\\Export\\\\StatActivityDuration\\:\\:getDescription\\(\\) should return string but return statement is missing\\.$#" - count: 1 - path: src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php - - - - message: "#^Method Chill\\\\ActivityBundle\\\\Export\\\\Export\\\\StatActivityDuration\\:\\:getTitle\\(\\) should return string but return statement is missing\\.$#" - count: 1 - path: src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 1 @@ -330,21 +320,6 @@ parameters: count: 6 path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 1 - path: src/Bundle/ChillPersonBundle/Export/Aggregator/CountryOfBirthAggregator.php - - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 1 - path: src/Bundle/ChillPersonBundle/Export/Aggregator/NationalityAggregator.php - - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 2 - path: src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 1 diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php new file mode 100644 index 000000000..5c6656009 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php @@ -0,0 +1,68 @@ +addSelect('(SELECT COUNT(activity.id) FROM ' . Activity::class . ' activity WHERE activity.accompanyingPeriod = acp) AS activity_by_number_aggregator') + ->addGroupBy('activity_by_number_aggregator'); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + // No form needed + } + + public function getLabels($key, array $values, $data) + { + return static function ($value) { + if ('_header' === $value) { + return ''; + } + + if (null === $value) { + return ''; + } + + return $value; + }; + } + + public function getQueryKeys($data): array + { + return ['activity_by_number_aggregator']; + } + + public function getTitle(): string + { + return 'Group acp by activity number'; + } +} diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php new file mode 100644 index 000000000..7d77a7c69 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php @@ -0,0 +1,83 @@ +translator = $translator; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data): void + { + $qb->addSelect('activity.sentReceived AS activity_sentreceived_aggregator') + ->addGroupBy('activity_sentreceived_aggregator'); + } + + public function applyOn(): string + { + return Declarations::ACTIVITY; + } + + public function buildForm(FormBuilderInterface $builder): void + { + // No form needed + } + + public function getLabels($key, array $values, $data): callable + { + return function (?string $value): string { + if ('_header' === $value) { + return 'export.aggregator.activity.by_sent_received.Sent or received'; + } + + switch ($value) { + case null: + return ''; + + case 'sent': + return $this->translator->trans('export.aggregator.activity.by_sent_received.is sent'); + + case 'received': + return $this->translator->trans('export.aggregator.activity.by_sent_received.is received'); + + default: + throw new LogicException(sprintf('The value %s is not valid', $value)); + } + }; + } + + public function getQueryKeys($data): array + { + return ['activity_sentreceived_aggregator']; + } + + public function getTitle(): string + { + return 'export.aggregator.activity.by_sent_received.Group activity by sentreceived'; + } +} diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php new file mode 100644 index 000000000..570f42ae0 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php @@ -0,0 +1,57 @@ +andWhere(' + NOT EXISTS ( + SELECT 1 FROM ' . Activity::class . ' activity + WHERE activity.accompanyingPeriod = acp + ) + '); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + //no form needed + } + + public function describeAction($data, $format = 'string'): array + { + return ['Filtered acp which has no activities', []]; + } + + public function getTitle(): string + { + return 'Filter acp which has no activity'; + } +} diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php index dcdacd84a..b52ef441c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php @@ -39,9 +39,9 @@ class UsersJobFilter implements FilterInterface $qb ->andWhere( $qb->expr()->exists( - 'SELECT 1 FROM ' . Activity::class . ' activity_users_job_filter_act + 'SELECT 1 FROM ' . Activity::class . ' activity_users_job_filter_act JOIN activity_users_job_filter_act.users users WHERE users.userJob IN (:activity_users_job_filter_jobs) AND activity_users_job_filter_act = activity ' - ) + ) ) ->setParameter('activity_users_job_filter_jobs', $data['jobs']); } diff --git a/src/Bundle/ChillActivityBundle/Tests/Controller/ActivityControllerTest.php b/src/Bundle/ChillActivityBundle/Tests/Controller/ActivityControllerTest.php index 55a1442f1..8a30a6c9b 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Controller/ActivityControllerTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Controller/ActivityControllerTest.php @@ -369,8 +369,12 @@ final class ActivityControllerTest extends WebTestCase $center ); $reachableScopesId = array_intersect( - array_map(static function ($s) { return $s->getId(); }, $reachableScopesDelete), - array_map(static function ($s) { return $s->getId(); }, $reachableScopesUpdate) + array_map(static function ($s) { + return $s->getId(); + }, $reachableScopesDelete), + array_map(static function ($s) { + return $s->getId(); + }, $reachableScopesUpdate) ); if (count($reachableScopesId) === 0) { diff --git a/src/Bundle/ChillActivityBundle/Tests/Form/ActivityTypeTest.php b/src/Bundle/ChillActivityBundle/Tests/Form/ActivityTypeTest.php index d0b4b5bb2..b6cb6fc7b 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Form/ActivityTypeTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Form/ActivityTypeTest.php @@ -188,7 +188,9 @@ final class ActivityTypeTest extends KernelTestCase // map all the values in an array $values = array_map( - static function ($choice) { return $choice->value; }, + static function ($choice) { + return $choice->value; + }, $view['activity']['durationTime']->vars['choices'] ); diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 224075e6f..57153a193 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -120,6 +120,10 @@ services: tags: - { name: chill.export_filter, alias: 'activity_usersscope_filter' } + Chill\ActivityBundle\Export\Filter\ACPFilters\HasNoActivityFilter: + tags: + - { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' } + ## Aggregators Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator: tags: @@ -179,3 +183,11 @@ services: Chill\ActivityBundle\Export\Aggregator\ActivityUsersJobAggregator: tags: - { name: chill.export_aggregator, alias: activity_users_job_aggregator } + + Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByActivityNumberAggregator: + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_by_activity_number_aggregator } + + Chill\ActivityBundle\Export\Aggregator\SentReceivedAggregator: + tags: + - { name: chill.export_aggregator, alias: activity_sentreceived_aggregator } diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 5ce01c433..b41aeab17 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -260,8 +260,6 @@ activity is not emergency: l'activité n'est pas urgente Filter activity by sentreceived: Filtrer les activités par envoyé/reçu 'Filtered activity by sentreceived: only %sentreceived%': "Filtré par envoyé/reçu: uniquement %sentreceived%" Accepted sentreceived: '' -is sent: envoyé -is received: reçu Filter activity by linked socialaction: Filtrer les activités par action liée 'Filtered activity by linked socialaction: only %actions%': "Filtré par action liée: uniquement %actions%" Filter activity by linked socialissue: Filtrer les activités par problématique liée @@ -276,6 +274,10 @@ Creators: Créateurs Filter activity by userscope: Filtrer les activités par service du créateur 'Filtered activity by userscope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%" Accepted userscope: Services + +Filter acp which has no activity: Filtrer les parcours qui n’ont pas d’activité +Filtered acp which has no activities: Filtrer les parcours sans activité associée +Group acp by activity number: Grouper les parcours par nombre d’activité #aggregators Activity type: Type d'activité @@ -332,4 +334,10 @@ export: by_usersscope: Filter by users scope: Filtrer les activités 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%' - + aggregator: + activity: + by_sent_received: + Sent or received: Envoyé ou reçu + is sent: envoyé + is received: reçu + Group activity by sentreceived: Grouper les activités par envoyé / reçu diff --git a/src/Bundle/ChillAsideActivityBundle/src/DependencyInjection/ChillAsideActivityExtension.php b/src/Bundle/ChillAsideActivityBundle/src/DependencyInjection/ChillAsideActivityExtension.php index e81b887c0..927de1845 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/DependencyInjection/ChillAsideActivityExtension.php +++ b/src/Bundle/ChillAsideActivityBundle/src/DependencyInjection/ChillAsideActivityExtension.php @@ -30,6 +30,8 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte $loader->load('services.yaml'); $loader->load('services/form.yaml'); $loader->load('services/menu.yaml'); + $loader->load('services/security.yaml'); + $loader->load('services/export.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php new file mode 100644 index 000000000..6c91c9336 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php @@ -0,0 +1,82 @@ +asideActivityCategoryRepository = $asideActivityCategoryRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->addSelect('IDENTITY(aside.type) AS by_aside_activity_type_aggregator') + ->addGroupBy('by_aside_activity_type_aggregator'); + } + + public function applyOn(): string + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + // No form needed + } + + public function getLabels($key, array $values, $data) + { + $this->asideActivityCategoryRepository->findBy(['id' => $values]); + + return function ($value): string { + if ('_header' === $value) { + return 'export.aggregator.Aside activity type'; + } + + if (null === $value) { + return ''; + } + + $t = $this->asideActivityCategoryRepository->find($value); + + return $this->translatableStringHelper->localize($t->getTitle()); + }; + } + + public function getQueryKeys($data): array + { + return ['by_aside_activity_type_aggregator']; + } + + public function getTitle(): string + { + return 'export.aggregator.Group by aside activity type'; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Declarations.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Declarations.php new file mode 100644 index 000000000..8c808ea37 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Declarations.php @@ -0,0 +1,20 @@ +repository = $repository; + } + + public function buildForm(FormBuilderInterface $builder) + { + } + + public function getAllowedFormattersTypes(): array + { + return [FormatterInterface::TYPE_TABULAR]; + } + + public function getDescription(): string + { + return 'export.Count aside activities by various parameters.'; + } + + public function getGroup(): string + { + return 'export.Exports of aside activities'; + } + + public function getLabels($key, array $values, $data) + { + if ('export_result' !== $key) { + throw new LogicException("the key {$key} is not used by this export"); + } + + $labels = array_combine($values, $values); + $labels['_header'] = $this->getTitle(); + + return static function ($value) use ($labels) { + return $labels[$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.Count aside activities'; + } + + public function getType(): string + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $qb = $this->repository->createQueryBuilder('aside'); + + $qb->select('COUNT(DISTINCT aside.id) AS export_result'); + + return $qb; + } + + public function requiredRole(): string + { + return AsideActivityVoter::STATS; + } + + public function supportsModifiers(): array + { + return []; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php new file mode 100644 index 000000000..85b327795 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php @@ -0,0 +1,96 @@ +categoryRender = $categoryRender; + $this->asideActivityTypeRepository = $asideActivityTypeRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $clause = $qb->expr()->in('aside.type', ':types'); + + $qb->andWhere($clause); + $qb->setParameter('types', $data['types']); + } + + public function applyOn(): string + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder + ->add('types', EntityType::class, [ + 'class' => AsideActivityCategory::class, + 'choices' => $this->asideActivityTypeRepository->findAllActive(), + 'required' => false, + 'multiple' => true, + 'expanded' => false, + 'attr' => [ + 'class' => 'select2', + ], + 'choice_label' => function (AsideActivityCategory $category) { + $options = []; + + return $this->categoryRender->renderString($category, $options); + }, + ]); + } + + public function describeAction($data, $format = 'string'): array + { + $types = array_map( + fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getName()), + $this->asideActivityTypeRepository->findBy(['id' => $data['types']->toArray()]) + ); + + return ['export.filter.Filtered by aside activity type: only %type%', [ + '%type%' => implode(', ', $types), + ]]; + } + + public function getTitle(): string + { + return 'export.filter.Filter by aside activity type'; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php new file mode 100644 index 000000000..a7595281d --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php @@ -0,0 +1,131 @@ +translator = $translator; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $where = $qb->getDQLPart('where'); + $clause = $qb->expr()->between( + 'aside.date', + ':date_from', + ':date_to' + ); + + if ($where instanceof Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + $qb->setParameter('date_from', $data['date_from']); + $qb->setParameter('date_to', $data['date_to']); + } + + public function applyOn(): string + { + return Declarations::ASIDE_ACTIVITY_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder + ->add('date_from', ChillDateType::class, [ + 'label' => 'export.filter.Aside activities after this date', + 'data' => new DateTime(), + ]) + ->add('date_to', ChillDateType::class, [ + 'label' => 'export.filter.Aside activities before this date', + 'data' => new DateTime(), + ]); + + $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) { + /** @var \Symfony\Component\Form\FormInterface $filterForm */ + $filterForm = $event->getForm()->getParent(); + $enabled = $filterForm->get(FilterType::ENABLED_FIELD)->getData(); + + if (true === $enabled) { + // if the filter is enabled, add some validation + $form = $event->getForm(); + $date_from = $form->get('date_from')->getData(); + $date_to = $form->get('date_to')->getData(); + + // check that fields are not empty + if (null === $date_from) { + $form->get('date_from')->addError(new FormError( + $this->translator->trans('This field ' + . 'should not be empty') + )); + } + + if (null === $date_to) { + $form->get('date_to')->addError(new FormError( + $this->translator->trans('This field ' + . 'should not be empty') + )); + } + + // check that date_from is before date_to + if ( + (null !== $date_from && null !== $date_to) + && $date_from >= $date_to + ) { + $form->get('date_to')->addError(new FormError( + $this->translator->trans('export.filter.This date should be after ' + . 'the date given in "Implied in an aside activity after ' + . 'this date" field') + )); + } + } + }); + } + + public function describeAction($data, $format = 'string'): array + { + return ['export.filter.Filtered by aside activities between %dateFrom% and %dateTo%', [ + '%dateFrom%' => $data['date_from']->format('d-m-Y'), + '%dateTo%' => $data['date_to']->format('d-m-Y'), + ]]; + } + + public function getTitle(): string + { + return 'export.filter.Filter by aside activity date'; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/Repository/AsideActivityCategoryRepository.php b/src/Bundle/ChillAsideActivityBundle/src/Repository/AsideActivityCategoryRepository.php index 85ea9faf4..918cec586 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Repository/AsideActivityCategoryRepository.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Repository/AsideActivityCategoryRepository.php @@ -38,6 +38,11 @@ class AsideActivityCategoryRepository implements ObjectRepository return $this->repository->findAll(); } + public function findAllActive(): array + { + return $this->repository->findBy(['isActive' => true]); + } + /** * @param mixed|null $limit * @param mixed|null $offset diff --git a/src/Bundle/ChillAsideActivityBundle/src/Repository/AsideActivityRepository.php b/src/Bundle/ChillAsideActivityBundle/src/Repository/AsideActivityRepository.php index f2ad9c072..b3dc46f87 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Repository/AsideActivityRepository.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Repository/AsideActivityRepository.php @@ -12,46 +12,20 @@ declare(strict_types=1); namespace Chill\AsideActivityBundle\Repository; use Chill\AsideActivityBundle\Entity\AsideActivity; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; -final class AsideActivityRepository implements ObjectRepository +/** + * @method AsideActivity|null find($id, $lockMode = null, $lockVersion = null) + * @method AsideActivity|null findOneBy(array $criteria, array $orderBy = null) + * @method AsideActivity[] findAll() + * @method AsideActivity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class AsideActivityRepository extends ServiceEntityRepository { - private EntityRepository $repository; - - public function __construct(EntityManagerInterface $entityManager) + public function __construct(ManagerRegistry $registry) { - $this->repository = $entityManager->getRepository(AsideActivity::class); - } - - public function find($id): ?AsideActivity - { - return $this->repository->find($id); - } - - /** - * @return AsideActivity[] - */ - public function findAll(): array - { - return $this->repository->findAll(); - } - - /** - * @param mixed|null $limit - * @param mixed|null $offset - * - * @return AsideActivity[] - */ - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array - { - return $this->repository->findBy($criteria, $orderBy, $limit, $offset); - } - - public function findOneBy(array $criteria): ?AsideActivity - { - return $this->repository->findOneBy($criteria); + parent::__construct($registry, AsideActivity::class); } public function getClassName(): string diff --git a/src/Bundle/ChillAsideActivityBundle/src/Security/AsideActivityVoter.php b/src/Bundle/ChillAsideActivityBundle/src/Security/AsideActivityVoter.php new file mode 100644 index 000000000..6b308b5f2 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/Security/AsideActivityVoter.php @@ -0,0 +1,79 @@ +voterHelper = $voterHelperFactory + ->generate(self::class) + ->addCheckFor(Center::class, [self::STATS]) + ->build(); + } + + /** + * @return string[] + */ + public function getRoles(): array + { + return $this->getAttributes(); + } + + /** + * @return string[][] + */ + public function getRolesWithHierarchy(): array + { + return ['Aside activity' => $this->getRoles()]; + } + + /** + * @return string[] + */ + public function getRolesWithoutScope(): array + { + return $this->getAttributes(); + } + + protected function supports($attribute, $subject) + { + return $this->voterHelper->supports($attribute, $subject); + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + return $this->voterHelper->voteOnAttribute($attribute, $subject, $token); + } + + private function getAttributes(): array + { + return [self::STATS]; + } +} diff --git a/src/Bundle/ChillAsideActivityBundle/src/config/services.yaml b/src/Bundle/ChillAsideActivityBundle/src/config/services.yaml index 34bb6da33..2a7c30d7c 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/config/services.yaml +++ b/src/Bundle/ChillAsideActivityBundle/src/config/services.yaml @@ -20,3 +20,33 @@ services: resource: "../Controller" autowire: true autoconfigure: true + + + ## Exports + + # indicators + Chill\AsideActivityBundle\Export\Export\CountAsideActivity: + autowire: true + autoconfigure: true + tags: + - { name: chill.export, alias: count_asideactivity } + + # filters + Chill\AsideActivityBundle\Export\Filter\ByDateFilter: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: asideactivity_bydate_filter } + + Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: asideactivity_activitytype_filter } + + # aggregators + Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: asideactivity_activitytype_aggregator } diff --git a/src/Bundle/ChillAsideActivityBundle/src/config/services/export.yaml b/src/Bundle/ChillAsideActivityBundle/src/config/services/export.yaml new file mode 100644 index 000000000..1b6b05e1c --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/config/services/export.yaml @@ -0,0 +1,27 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + ## Indicators + Chill\AsideActivityBundle\Export\Export\CountAsideActivity: + tags: + - { name: chill.export, alias: 'count_aside_activity' } + + ## Filters + chill.aside_activity.export.date_filter: + class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter + tags: + - { name: chill.export_filter, alias: 'aside_activity_date_filter' } + + chill.aside_activity.export.type_filter: + class: Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter + tags: + - { name: chill.export_filter, alias: 'aside_activity_type_filter' } + + ## Aggregators + + chill.aside_activity.export.type_aggregator: + class: Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator + tags: + - { name: chill.export_aggregator, alias: activity_type_aggregator } \ No newline at end of file diff --git a/src/Bundle/ChillAsideActivityBundle/src/config/services/security.yaml b/src/Bundle/ChillAsideActivityBundle/src/config/services/security.yaml new file mode 100644 index 000000000..eb3327959 --- /dev/null +++ b/src/Bundle/ChillAsideActivityBundle/src/config/services/security.yaml @@ -0,0 +1,7 @@ +services: + Chill\AsideActivityBundle\Security\AsideActivityVoter: + autowire: true + autoconfigure: true + tags: + - { name: security.voter } + - { name: chill.role } \ No newline at end of file diff --git a/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml b/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml index 0a7be1fcd..35a4d6a22 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml +++ b/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml @@ -166,3 +166,21 @@ Aside activities: Activités annexes Aside activity types: Types d'activités annexes Aside activity type configuration: Configuration des categories d'activités annexes Aside activity configuration: Configuration des activités annexes + +# exports +export: + Exports of aside activities: Exports des activités annexes + Count aside activities: Nombre d'activités annexes + Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères + filter: + Filter by aside activity date: Filtrer les activités annexes par date + Filter by aside activity type: Filtrer les activités annexes par type d'activité + 'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%" + This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date" + Aside activities after this date: Actvitités annexes après cette date + Aside activities before this date: Actvitités annexes avant cette date + aggregator: + Group by aside activity type: Grouper les activités annexes par type d'activité + Aside activity type: Type d'activité annexe + + diff --git a/src/Bundle/ChillBudgetBundle/Config/ConfigRepository.php b/src/Bundle/ChillBudgetBundle/Config/ConfigRepository.php index 2497bcf21..f28a83a4d 100644 --- a/src/Bundle/ChillBudgetBundle/Config/ConfigRepository.php +++ b/src/Bundle/ChillBudgetBundle/Config/ConfigRepository.php @@ -31,7 +31,9 @@ class ConfigRepository public function getChargesKeys(bool $onlyActive = false): array { - return array_map(static function ($element) { return $element['key']; }, $this->getCharges($onlyActive)); + return array_map(static function ($element) { + return $element['key']; + }, $this->getCharges($onlyActive)); } /** @@ -50,7 +52,9 @@ class ConfigRepository public function getResourcesKeys(bool $onlyActive = false): array { - return array_map(static function ($element) { return $element['key']; }, $this->getResources($onlyActive)); + return array_map(static function ($element) { + return $element['key']; + }, $this->getResources($onlyActive)); } /** @@ -70,14 +74,18 @@ class ConfigRepository private function getCharges(bool $onlyActive = false): array { return $onlyActive ? - array_filter($this->charges, static function ($el) { return $el['active']; }) + array_filter($this->charges, static function ($el) { + return $el['active']; + }) : $this->charges; } private function getResources(bool $onlyActive = false): array { return $onlyActive ? - array_filter($this->resources, static function ($el) { return $el['active']; }) + array_filter($this->resources, static function ($el) { + return $el['active']; + }) : $this->resources; } diff --git a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php index 6ae7e049b..f02d2e64a 100644 --- a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php +++ b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php @@ -61,7 +61,9 @@ class SummaryBudget implements SummaryBudgetInterface ]; } - $personIds = $household->getCurrentPersons()->map(static function (Person $p) { return $p->getId(); }); + $personIds = $household->getCurrentPersons()->map(static function (Person $p) { + return $p->getId(); + }); $ids = implode(', ', array_fill(0, count($personIds), '?')); $parameters = [...$personIds, $household->getId()]; diff --git a/src/Bundle/ChillEventBundle/Tests/Controller/ParticipationControllerTest.php b/src/Bundle/ChillEventBundle/Tests/Controller/ParticipationControllerTest.php index dfb782a7d..fa1ecabbe 100644 --- a/src/Bundle/ChillEventBundle/Tests/Controller/ParticipationControllerTest.php +++ b/src/Bundle/ChillEventBundle/Tests/Controller/ParticipationControllerTest.php @@ -239,7 +239,9 @@ final class ParticipationControllerTest extends WebTestCase $this->personsIdsCache = array_merge( $this->personsIdsCache, $event->getParticipations()->map( - static function ($p) { return $p->getPerson()->getId(); } + static function ($p) { + return $p->getPerson()->getId(); + } ) ->toArray() ); @@ -303,7 +305,9 @@ final class ParticipationControllerTest extends WebTestCase $event = $this->getRandomEventWithMultipleParticipations(); $persons_id = implode(',', $event->getParticipations()->map( - static function ($p) { return $p->getPerson()->getId(); } + static function ($p) { + return $p->getPerson()->getId(); + } )->toArray()); $crawler = $this->client->request( @@ -329,7 +333,9 @@ final class ParticipationControllerTest extends WebTestCase $nbParticipations = $event->getParticipations()->count(); // get the persons_id participating on this event $persons_id = $event->getParticipations()->map( - static function ($p) { return $p->getPerson()->getId(); } + static function ($p) { + return $p->getPerson()->getId(); + } )->toArray(); // exclude the existing persons_ids from the new person $this->personsIdsCache = array_merge($this->personsIdsCache, $persons_id); diff --git a/src/Bundle/ChillMainBundle/Controller/ExportController.php b/src/Bundle/ChillMainBundle/Controller/ExportController.php index 96c9ed08d..84bf80c6b 100644 --- a/src/Bundle/ChillMainBundle/Controller/ExportController.php +++ b/src/Bundle/ChillMainBundle/Controller/ExportController.php @@ -11,23 +11,32 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; +use Chill\MainBundle\Entity\SavedExport; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Export\ExportManager; +use Chill\MainBundle\Form\SavedExportType; use Chill\MainBundle\Form\Type\Export\ExportType; use Chill\MainBundle\Form\Type\Export\FormatterType; use Chill\MainBundle\Form\Type\Export\PickCenterType; use Chill\MainBundle\Redis\ChillRedis; +use Chill\MainBundle\Security\Authorization\SavedExportVoter; +use Doctrine\ORM\EntityManagerInterface; use LogicException; use Psr\Log\LoggerInterface; +use RedisException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Contracts\Translation\TranslatorInterface; use function count; use function serialize; use function unserialize; @@ -38,35 +47,37 @@ use function unserialize; */ class ExportController extends AbstractController { + private EntityManagerInterface $entityManager; + /** * @var ExportManager */ - protected $exportManager; + private $exportManager; /** * @var FormFactoryInterface */ - protected $formFactory; + private $formFactory; /** * @var LoggerInterface */ - protected $logger; + private $logger; /** * @var ChillRedis */ - protected $redis; + private $redis; /** * @var SessionInterface */ - protected $session; + private $session; /** * @var TranslatorInterface */ - protected $translator; + private $translator; public function __construct( ChillRedis $chillRedis, @@ -74,8 +85,10 @@ class ExportController extends AbstractController FormFactoryInterface $formFactory, LoggerInterface $logger, SessionInterface $session, - TranslatorInterface $translator + TranslatorInterface $translator, + EntityManagerInterface $entityManager ) { + $this->entityManager = $entityManager; $this->redis = $chillRedis; $this->exportManager = $exportManager; $this->formFactory = $formFactory; @@ -142,6 +155,29 @@ class ExportController extends AbstractController ); } + /** + * @Route("/{_locale}/exports/generate-from-saved/{id}", name="chill_main_export_generate_from_saved") + * + * @throws RedisException + */ + public function generateFromSavedExport(SavedExport $savedExport): RedirectResponse + { + $this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport); + + $key = md5(uniqid((string) mt_rand(), false)); + + $this->redis->setEx($key, 3600, serialize($savedExport->getOptions())); + + return $this->redirectToRoute( + 'chill_main_export_download', + [ + 'alias' => $savedExport->getExportAlias(), + 'key' => $key, 'prevent_save' => true, + 'returnPath' => $this->generateUrl('chill_main_export_saved_list_my'), + ] + ); + } + /** * Render the list of available exports. */ @@ -203,6 +239,46 @@ class ExportController extends AbstractController } } + /** + * @Route("/{_locale}/export/save-from-key/{alias}/{key}", name="chill_main_export_save_from_key") + */ + public function saveFromKey(string $alias, string $key, Request $request): Response + { + $this->denyAccessUnlessGranted('ROLE_USER'); + $user = $this->getUser(); + + if (!$user instanceof User) { + throw new AccessDeniedHttpException(); + } + + $data = $this->rebuildRawData($key); + + $savedExport = new SavedExport(); + $savedExport + ->setOptions($data) + ->setExportAlias($alias) + ->setUser($user); + + $form = $this->createForm(SavedExportType::class, $savedExport); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->entityManager->persist($savedExport); + $this->entityManager->flush(); + + return $this->redirectToRoute('chill_main_export_index'); + } + + return $this->render( + '@ChillMain/SavedExport/new.html.twig', + [ + 'form' => $form->createView(), + 'saved_export' => $savedExport, + ] + ); + } + /** * create a form to show on different steps. * @@ -418,28 +494,7 @@ class ExportController extends AbstractController protected function rebuildData($key) { - if (null === $key) { - throw $this->createNotFoundException('key does not exists'); - } - - if ($this->redis->exists($key) !== 1) { - $this->addFlash('error', $this->translator->trans('This report is not available any more')); - - throw $this->createNotFoundException('key does not exists'); - } - - $serialized = $this->redis->get($key); - - if (false === $serialized) { - throw new LogicException('the key could not be reached from redis'); - } - - $rawData = unserialize($serialized); - - $this->logger->notice('[export] choices for an export unserialized', [ - 'key' => $key, - 'rawData' => json_encode($rawData), - ]); + $rawData = $this->rebuildRawData($key); $alias = $rawData['alias']; @@ -585,4 +640,32 @@ class ExportController extends AbstractController throw new LogicException("the step {$step} is not defined."); } } + + private function rebuildRawData(string $key): array + { + if (null === $key) { + throw $this->createNotFoundException('key does not exists'); + } + + if ($this->redis->exists($key) !== 1) { + $this->addFlash('error', $this->translator->trans('This report is not available any more')); + + throw $this->createNotFoundException('key does not exists'); + } + + $serialized = $this->redis->get($key); + + if (false === $serialized) { + throw new LogicException('the key could not be reached from redis'); + } + + $rawData = unserialize($serialized); + + $this->logger->notice('[export] choices for an export unserialized', [ + 'key' => $key, + 'rawData' => json_encode($rawData), + ]); + + return $rawData; + } } diff --git a/src/Bundle/ChillMainBundle/Controller/SavedExportController.php b/src/Bundle/ChillMainBundle/Controller/SavedExportController.php new file mode 100644 index 000000000..197fe253d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/SavedExportController.php @@ -0,0 +1,185 @@ +exportManager = $exportManager; + $this->entityManager = $entityManager; + $this->formFactory = $formBuilder; + $this->savedExportRepository = $savedExportRepository; + $this->security = $security; + $this->session = $session; + $this->templating = $templating; + $this->translator = $translator; + $this->urlGenerator = $urlGenerator; + } + + /** + * @Route("/{_locale}/exports/saved/{id}/delete", name="chill_main_export_saved_delete") + */ + public function delete(SavedExport $savedExport, Request $request): Response + { + if (!$this->security->isGranted(SavedExportVoter::DELETE, $savedExport)) { + throw new AccessDeniedHttpException(); + } + + $form = $this->formFactory->create(); + $form->add('submit', SubmitType::class, ['label' => 'Delete']); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->entityManager->remove($savedExport); + $this->entityManager->flush(); + + $this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Export is deleted')); + + return new RedirectResponse( + $this->urlGenerator->generate('chill_main_export_saved_list_my') + ); + } + + return new Response( + $this->templating->render( + '@ChillMain/SavedExport/delete.html.twig', + [ + 'saved_export' => $savedExport, + 'delete_form' => $form->createView(), + ] + ) + ); + } + + /** + * @Route("/{_locale}/exports/saved/{id}/edit", name="chill_main_export_saved_edit") + */ + public function edit(SavedExport $savedExport, Request $request): Response + { + if (!$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) { + throw new AccessDeniedHttpException(); + } + + $form = $this->formFactory->create(SavedExportType::class, $savedExport); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->entityManager->flush(); + + $this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Saved export is saved!')); + + return new RedirectResponse( + $this->urlGenerator->generate('chill_main_export_saved_list_my') + ); + } + + return new Response( + $this->templating->render( + '@ChillMain/SavedExport/edit.html.twig', + [ + 'form' => $form->createView(), + ] + ) + ); + } + + /** + * @Route("/{_locale}/exports/saved/my", name="chill_main_export_saved_list_my") + */ + public function list(): Response + { + $user = $this->security->getUser(); + + if (!$this->security->isGranted('ROLE_USER') || !$user instanceof User) { + throw new AccessDeniedHttpException(); + } + + $exports = $this->savedExportRepository->findByUser($user, ['title' => 'ASC']); + + // group by center + /** @var array $exportsGrouped */ + $exportsGrouped = []; + + foreach ($exports as $savedExport) { + $export = $this->exportManager->getExport($savedExport->getExportAlias()); + + $exportsGrouped[ + $export instanceof GroupedExportInterface + ? $this->translator->trans($export->getGroup()) : '_' + ][] = ['saved' => $savedExport, 'export' => $export]; + } + + ksort($exportsGrouped); + + return new Response( + $this->templating->render( + '@ChillMain/SavedExport/index.html.twig', + [ + 'grouped_exports' => $exportsGrouped, + 'total' => count($exports), + ] + ) + ); + } +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 9164607a7..dc50846fb 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -34,6 +34,8 @@ use Chill\MainBundle\Doctrine\DQL\Replace; use Chill\MainBundle\Doctrine\DQL\Similarity; use Chill\MainBundle\Doctrine\DQL\STContains; use Chill\MainBundle\Doctrine\DQL\StrictWordSimilarityOPS; +use Chill\MainBundle\Doctrine\DQL\STX; +use Chill\MainBundle\Doctrine\DQL\STY; use Chill\MainBundle\Doctrine\DQL\ToChar; use Chill\MainBundle\Doctrine\DQL\Unaccent; use Chill\MainBundle\Doctrine\ORM\Hydration\FlatHierarchyEntityHydrator; @@ -245,6 +247,8 @@ class ChillMainExtension extends Extension implements 'STRICT_WORD_SIMILARITY_OPS' => StrictWordSimilarityOPS::class, 'ST_CONTAINS' => STContains::class, 'JSONB_ARRAY_LENGTH' => JsonbArrayLength::class, + 'ST_X' => STX::class, + 'ST_Y' => STY::class, ], 'datetime_functions' => [ 'EXTRACT' => Extract::class, diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php new file mode 100644 index 000000000..d5d8d0a3f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php @@ -0,0 +1,37 @@ +field->dispatch($sqlWalker)); + } + + public function parse(Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->field = $parser->ArithmeticExpression(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php new file mode 100644 index 000000000..e827da543 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php @@ -0,0 +1,37 @@ +field->dispatch($sqlWalker)); + } + + public function parse(Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->field = $parser->ArithmeticExpression(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php new file mode 100644 index 000000000..34f16a0fb --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php @@ -0,0 +1,52 @@ +id = $id; + $this->unitName = $unitName; + $this->unitRefId = $unitRefId; + $this->layerId = $layerId; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/SavedExport.php b/src/Bundle/ChillMainBundle/Entity/SavedExport.php new file mode 100644 index 000000000..3ac361e81 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/SavedExport.php @@ -0,0 +1,133 @@ +id = Uuid::uuid4(); + } + + public function getDescription(): string + { + return $this->description; + } + + public function getExportAlias(): string + { + return $this->exportAlias; + } + + public function getId(): UuidInterface + { + return $this->id; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getUser(): User + { + return $this->user; + } + + public function setDescription(string $description): SavedExport + { + $this->description = $description; + + return $this; + } + + public function setExportAlias(string $exportAlias): SavedExport + { + $this->exportAlias = $exportAlias; + + return $this; + } + + public function setOptions(array $options): SavedExport + { + $this->options = $options; + + return $this; + } + + public function setTitle(string $title): SavedExport + { + $this->title = $title; + + return $this; + } + + public function setUser(User $user): SavedExport + { + $this->user = $user; + + return $this; + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php index ab9c2e893..0e5e339ea 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php @@ -20,11 +20,12 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use RuntimeException; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Response; -use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use function array_key_exists; use function array_keys; use function array_map; @@ -80,7 +81,7 @@ class SpreadsheetListFormatter implements FormatterInterface * * @uses appendAggregatorForm * - * @param type $exportAlias + * @param string $exportAlias */ public function buildForm( FormBuilderInterface $builder, @@ -144,8 +145,6 @@ class SpreadsheetListFormatter implements FormatterInterface $i = 1; foreach ($result as $row) { - $line = []; - if (true === $this->formatterData['numerotation']) { $worksheet->setCellValue('A' . ($i + 1), (string) $i); } @@ -155,13 +154,22 @@ class SpreadsheetListFormatter implements FormatterInterface foreach ($row as $key => $value) { $row = $a . ($i + 1); - if ($value instanceof DateTimeInterface) { - $worksheet->setCellValue($row, Date::PHPToExcel($value)); - $worksheet->getStyle($row) - ->getNumberFormat() - ->setFormatCode(NumberFormat::FORMAT_DATE_DDMMYYYY); + $formattedValue = $this->getLabel($key, $value); + + if ($formattedValue instanceof DateTimeInterface) { + $worksheet->setCellValue($row, Date::PHPToExcel($formattedValue)); + + if ($formattedValue->format('His') === '000000') { + $worksheet->getStyle($row) + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_DATE_DDMMYYYY); + } else { + $worksheet->getStyle($row) + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME); + } } else { - $worksheet->setCellValue($row, $this->getLabel($key, $value)); + $worksheet->setCellValue($row, $formattedValue); } ++$a; } @@ -259,6 +267,10 @@ class SpreadsheetListFormatter implements FormatterInterface foreach ($keys as $key) { // get an array with all values for this key if possible $values = array_map(static function ($v) use ($key) { + if (!array_key_exists($key, $v)) { + throw new RuntimeException(sprintf('This key does not exists: %s. Available keys are %s', $key, implode(', ', array_keys($v)))); + } + return $v[$key]; }, $this->result); // store the label in the labelsCache property diff --git a/src/Bundle/ChillMainBundle/Export/Helper/DateTimeHelper.php b/src/Bundle/ChillMainBundle/Export/Helper/DateTimeHelper.php new file mode 100644 index 000000000..86a2458b2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Helper/DateTimeHelper.php @@ -0,0 +1,60 @@ +translator = $translator; + } + + public function getLabel($header): callable + { + return function ($value) use ($header) { + if ('_header' === $value) { + return $this->translator->trans($header); + } + + if (null === $value) { + return ''; + } + + // warning: won't work with DateTimeImmutable as we reset time a few lines later + $date = DateTime::createFromFormat('Y-m-d', $value); + $hasTime = false; + + if (false === $date) { + $date = DateTime::createFromFormat('Y-m-d H:i:s', $value); + $hasTime = true; + } + + // check that the creation could occurs. + if (false === $date) { + throw new Exception(sprintf('The value %s could ' + . 'not be converted to %s', $value, DateTime::class)); + } + + if (!$hasTime) { + $date->setTime(0, 0, 0); + } + + return $date; + }; + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Helper/ExportAddressHelper.php b/src/Bundle/ChillMainBundle/Export/Helper/ExportAddressHelper.php new file mode 100644 index 000000000..701f9ed07 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Helper/ExportAddressHelper.php @@ -0,0 +1,277 @@ + self::F_COUNTRY, + 'postal_code' => self::F_POSTAL_CODE, + 'street' => self::F_STREET, + 'building' => self::F_BUILDING, + 'string' => self::F_AS_STRING, + 'geom' => self::F_GEOM, + 'attributes' => self::F_ATTRIBUTES, + ]; + + private const COLUMN_MAPPING = [ + 'country' => ['country'], + 'postal_code' => ['postcode_code', 'postcode_name'], + 'street' => ['street', 'streetNumber'], + 'building' => ['buildingName', 'corridor', 'distribution', 'extra', 'flat', 'floor', 'steps'], + 'string' => ['_as_string'], + 'attributes' => ['isNoAddress', 'confidential', 'id'], + 'geom' => ['_lat', '_lon'], + ]; + + private AddressRender $addressRender; + + private AddressRepository $addressRepository; + + private PropertyAccessor $propertyAccess; + + private TranslatableStringHelperInterface $translatableStringHelper; + + public function __construct( + AddressRepository $addressRepository, + TranslatableStringHelperInterface $translatableStringHelper, + AddressRender $addressRender + ) { + $this->addressRepository = $addressRepository; + $this->propertyAccess = PropertyAccess::createPropertyAccessor(); + $this->translatableStringHelper = $translatableStringHelper; + $this->addressRender = $addressRender; + } + + public function addSelectClauses(int $params, QueryBuilder $queryBuilder, $entityName = 'address', $prefix = 'add') + { + foreach (self::ALL as $key => $bitmask) { + if (($params & $bitmask) === $bitmask) { + foreach (self::COLUMN_MAPPING[$key] as $field) { + switch ($field) { + case 'id': + case '_as_string': + $queryBuilder->addSelect(sprintf('%s.id AS %s%s', $entityName, $prefix, $field)); + + break; + + case 'street': + case 'streetNumber': + case 'floor': + case 'corridor': + case 'steps': + case 'buildingName': + case 'flat': + case 'distribution': + case 'extra': + $queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $entityName, $field, $prefix, $field)); + + break; + + case 'country': + case 'postcode_name': + case 'postcode_code': + $postCodeAlias = sprintf('%spostcode_t', $prefix); + + if (!in_array($postCodeAlias, $queryBuilder->getAllAliases(), true)) { + $queryBuilder->leftJoin($entityName . '.postcode', $postCodeAlias); + } + + if ('postcode_name' === $field) { + $queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $postCodeAlias, 'name', $prefix, $field)); + + break; + } + + if ('postcode_code' === $field) { + $queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $postCodeAlias, 'code', $prefix, $field)); + + break; + } + + $countryAlias = sprintf('%scountry_t', $prefix); + + if (!in_array($countryAlias, $queryBuilder->getAllAliases(), true)) { + $queryBuilder->leftJoin(sprintf('%s.country', $postCodeAlias), $countryAlias); + } + + $queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $countryAlias, 'name', $prefix, $field)); + + break; + + case 'isNoAddress': + case 'confidential': + $queryBuilder->addSelect(sprintf('CASE WHEN %s.%s = \'TRUE\' THEN 1 ELSE 0 END AS %s%s', $entityName, $field, $prefix, $field)); + + break; + + case '_lat': + $queryBuilder->addSelect(sprintf('ST_Y(%s.point) AS %s%s', $entityName, $prefix, $field)); + + break; + + case '_lon': + $queryBuilder->addSelect(sprintf('ST_X(%s.point) AS %s%s', $entityName, $prefix, $field)); + + break; + + default: + throw new LogicException('This key is not supported: ' . $key); + } + } + } + } + } + + /** + * @param self::F_* $params + * + * @return array|string[] + */ + public function getKeys(int $params, string $prefix = ''): array + { + $prefixes = []; + + foreach (self::ALL as $key => $bitmask) { + if (($params & $bitmask) === $bitmask) { + $prefixes = array_merge( + $prefixes, + array_map( + static function ($item) use ($prefix) { + return $prefix . $item; + }, + self::COLUMN_MAPPING[$key] + ) + ); + } + } + + return $prefixes; + } + + public function getLabel($key, array $values, $data, string $prefix = '', string $translationPrefix = 'export.address_helper.'): callable + { + $sanitizedKey = substr($key, strlen($prefix)); + + switch ($sanitizedKey) { + case 'id': + case 'street': + case 'streetNumber': + case 'buildingName': + case 'corridor': + case 'distribution': + case 'extra': + case 'flat': + case 'floor': + case '_lat': + case '_lon': + case 'steps': + case 'postcode_code': + case 'postcode_name': + return static function ($value) use ($sanitizedKey, $translationPrefix) { + if ('_header' === $value) { + return $translationPrefix . $sanitizedKey; + } + + if (null === $value) { + return ''; + } + + return $value; + }; + + case 'country': + 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)); + }; + + case 'isNoAddress': + case 'confidential': + return static function ($value) use ($sanitizedKey, $translationPrefix) { + if ('_header' === $value) { + return $translationPrefix . $sanitizedKey; + } + + switch ($value) { + case null: + return ''; + + case true: + return 1; + + case false: + return 0; + + default: + throw new LogicException('this value is not supported for ' . $sanitizedKey . ': ' . $value); + } + }; + + case '_as_string': + return function ($value) use ($sanitizedKey, $translationPrefix) { + if ('_header' === $value) { + return $translationPrefix . $sanitizedKey; + } + + if (null === $value) { + return ''; + } + + $address = $this->addressRepository->find($value); + + return $this->addressRender->renderString($address, []); + }; + + default: + throw new LogicException('this key is not supported: ' . $sanitizedKey); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Helper/UserHelper.php b/src/Bundle/ChillMainBundle/Export/Helper/UserHelper.php new file mode 100644 index 000000000..98c4c5579 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Helper/UserHelper.php @@ -0,0 +1,43 @@ +userRender = $userRender; + $this->userRepository = $userRepository; + } + + public function getLabel($key, array $values, string $header): callable + { + return function ($value) use ($header) { + if ('_header' === $value) { + return $header; + } + + if (null === $value || null === $user = $this->userRepository->find($value)) { + return ''; + } + + return $this->userRender->renderString($user, []); + }; + } +} diff --git a/src/Bundle/ChillMainBundle/Form/DataMapper/RollingDateDataMapper.php b/src/Bundle/ChillMainBundle/Form/DataMapper/RollingDateDataMapper.php new file mode 100644 index 000000000..21d0c3fde --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/DataMapper/RollingDateDataMapper.php @@ -0,0 +1,45 @@ +setData($viewData->getRoll()); + $forms['fixedDate']->setData($viewData->getFixedDate()); + } + + public function mapFormsToData($forms, &$viewData): void + { + $forms = iterator_to_array($forms); + + $viewData = new RollingDate( + $forms['roll']->getData(), + $forms['fixedDate']->getData() + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Form/SavedExportType.php b/src/Bundle/ChillMainBundle/Form/SavedExportType.php new file mode 100644 index 000000000..16aa4e42e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/SavedExportType.php @@ -0,0 +1,40 @@ +add('title', TextType::class, [ + 'required' => true, + ]) + ->add('description', ChillTextareaType::class, [ + 'required' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'class' => SavedExport::class, + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php index ecd668d2b..46fa8799f 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php +++ b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php @@ -51,7 +51,9 @@ class EntityToJsonTransformer implements DataTransformerInterface } return array_map( - function ($item) { return $this->denormalizeOne($item); }, + function ($item) { + return $this->denormalizeOne($item); + }, $denormalized ); } diff --git a/src/Bundle/ChillMainBundle/Form/Type/PickRollingDateType.php b/src/Bundle/ChillMainBundle/Form/Type/PickRollingDateType.php new file mode 100644 index 000000000..2c90379f2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/Type/PickRollingDateType.php @@ -0,0 +1,66 @@ +add('roll', ChoiceType::class, [ + 'choices' => array_combine( + array_map(static fn (string $item) => 'rolling_date.' . $item, RollingDate::ALL_T), + RollingDate::ALL_T + ), + 'multiple' => false, + 'expanded' => false, + 'label' => 'rolling_date.roll_movement', + ]) + ->add('fixedDate', ChillDateType::class, [ + 'input' => 'datetime_immutable', + 'label' => 'rolling_date.fixed_date_date', + ]); + + $builder->setDataMapper(new RollingDateDataMapper()); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'class' => RollingDate::class, + 'empty_data' => new RollingDate(RollingDate::T_TODAY), + 'constraints' => [ + new Callback([$this, 'validate']), + ], + ]); + } + + public function validate($data, ExecutionContextInterface $context, $payload): void + { + /** @var RollingDate $data */ + if (RollingDate::T_FIXED_DATE === $data->getRoll() && null === $data->getFixedDate()) { + $context + ->buildViolation('rolling_date.When fixed date is selected, you must provide a date') + ->atPath('roll') + ->addViolation(); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Form/Type/ScopePickerType.php b/src/Bundle/ChillMainBundle/Form/Type/ScopePickerType.php index 1cba1ba58..f65677dc7 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/ScopePickerType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/ScopePickerType.php @@ -66,7 +66,9 @@ class ScopePickerType extends AbstractType $options['role'] instanceof Role ? $options['role']->getRole() : $options['role'], $options['center'] ), - static function (Scope $s) { return $s->isActive(); } + static function (Scope $s) { + return $s->isActive(); + } ); if (0 === count($items)) { diff --git a/src/Bundle/ChillMainBundle/Repository/CivilityRepository.php b/src/Bundle/ChillMainBundle/Repository/CivilityRepository.php index 385488839..70b4a2bc4 100644 --- a/src/Bundle/ChillMainBundle/Repository/CivilityRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/CivilityRepository.php @@ -12,19 +12,40 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\Civility; -use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; -/** - * @method Civility|null find($id, $lockMode = null, $lockVersion = null) - * @method Civility|null findOneBy(array $criteria, array $orderBy = null) - * @method Civility[] findAll() - * @method Civility[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) - */ -class CivilityRepository extends ServiceEntityRepository +class CivilityRepository implements CivilityRepositoryInterface { - public function __construct(ManagerRegistry $registry) + private EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager) { - parent::__construct($registry, Civility::class); + $this->repository = $entityManager->getRepository($this->getClassName()); + } + + public function find($id): ?Civility + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?Civility + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return Civility::class; } } diff --git a/src/Bundle/ChillMainBundle/Repository/CivilityRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/CivilityRepositoryInterface.php new file mode 100644 index 000000000..5d687ac7e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/CivilityRepositoryInterface.php @@ -0,0 +1,34 @@ +repository ->createQueryBuilder('gu') - ->select('PARTIAL gu.{id,unitName,unitRefId,layer}') + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class)) ->addOrderBy('IDENTITY(gu.layer)') ->addOrderBy(('gu.unitName')) ->getQuery() diff --git a/src/Bundle/ChillMainBundle/Repository/LanguageRepository.php b/src/Bundle/ChillMainBundle/Repository/LanguageRepository.php index b73d7c943..2e972442e 100644 --- a/src/Bundle/ChillMainBundle/Repository/LanguageRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/LanguageRepository.php @@ -14,15 +14,14 @@ namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\Language; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; -final class LanguageRepository implements ObjectRepository +final class LanguageRepository implements LanguageRepositoryInterface { private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) { - $this->repository = $entityManager->getRepository(Language::class); + $this->repository = $entityManager->getRepository($this->getClassName()); } public function find($id, $lockMode = null, $lockVersion = null): ?Language @@ -54,7 +53,7 @@ final class LanguageRepository implements ObjectRepository return $this->repository->findOneBy($criteria, $orderBy); } - public function getClassName() + public function getClassName(): string { return Language::class; } diff --git a/src/Bundle/ChillMainBundle/Repository/LanguageRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/LanguageRepositoryInterface.php new file mode 100644 index 000000000..397b264e4 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/LanguageRepositoryInterface.php @@ -0,0 +1,37 @@ + + */ +class SavedExportRepository implements SavedExportRepositoryInterface +{ + private EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->repository = $entityManager->getRepository($this->getClassName()); + } + + public function find($id): ?SavedExport + { + return $this->repository->find($id); + } + + /** + * @return array|SavedExport[] + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array + { + $qb = $this->repository->createQueryBuilder('se'); + + $qb + ->where($qb->expr()->eq('se.user', ':user')) + ->setParameter('user', $user); + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + foreach ($orderBy as $field => $order) { + $qb->addOrderBy('se.' . $field, $order); + } + + return $qb->getQuery()->getResult(); + } + + public function findOneBy(array $criteria): ?SavedExport + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return SavedExport::class; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/SavedExportRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/SavedExportRepositoryInterface.php new file mode 100644 index 000000000..3b168505f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/SavedExportRepositoryInterface.php @@ -0,0 +1,40 @@ + + */ +interface SavedExportRepositoryInterface extends ObjectRepository +{ + public function find($id): ?SavedExport; + + /** + * @return array|SavedExport[] + */ + public function findAll(): array; + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; + + /** + * @return array|SavedExport[] + */ + public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array; + + public function findOneBy(array $criteria): ?SavedExport; + + public function getClassName(): string; +} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Export/_navbar.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Export/_navbar.html.twig new file mode 100644 index 000000000..a7d7216b4 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Export/_navbar.html.twig @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Resources/views/Export/download.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Export/download.html.twig index 84eb85100..1e69c5a49 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Export/download.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Export/download.html.twig @@ -49,5 +49,14 @@ window.addEventListener("DOMContentLoaded", function(e) { data-download-text="{{ "Download your report"|trans|escape('html_attr') }}" >{{ "Waiting for your report"|trans ~ '...' }} - + + {% endblock content %} \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Resources/views/Export/layout.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Export/layout.html.twig index 9cf53993d..5ce433535 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Export/layout.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Export/layout.html.twig @@ -22,9 +22,11 @@ {% block content %} + + {{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'common'}) }} +
-

{{ 'Exports list'|trans }}

- +
{% for group, exports in grouped_exports %}{% if group != '_' %} @@ -32,13 +34,17 @@
{% for export_alias, export in exports %}
-

{{ export.title|trans }}

-

{{ export.description|trans }}

-

- - {{ 'Create an export'|trans }} - -

+
+
+

{{ export.title|trans }}

+

{{ export.description|trans }}

+

+ + {{ 'Create an export'|trans }} + +

+
+
{% endfor %}
@@ -52,13 +58,17 @@ {% for export_alias,export in grouped_exports['_'] %}
-

{{ export.title|trans }}

-

{{ export.description|trans }}

-

- - {{ 'Create an export'|trans }} - -

+
+
+

{{ export.title|trans }}

+

{{ export.description|trans }}

+

+ + {{ 'Create an export'|trans }} + +

+
+
{% endfor %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/SavedExport/delete.html.twig b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/delete.html.twig new file mode 100644 index 000000000..5d90699df --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/delete.html.twig @@ -0,0 +1,25 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block title 'saved_export.Delete saved ?'|trans %} + +{% block display_content %} +
+

{{ saved_export.title }}

+

{{ saved_export.description|chill_markdown_to_html }}

+ +
+{% endblock %} + +{% block content %} +
+ {{ include('@ChillMain/Util/confirmation_template.html.twig', + { + 'title' : 'saved_export.Delete saved ?'|trans, + 'confirm_question' : 'saved_export.Are you sure you want to delete this saved ?'|trans, + 'display_content' : block('display_content'), + 'cancel_route' : 'chill_main_export_saved_list_my', + 'cancel_parameters' : {}, + 'form' : delete_form + } ) }} +
+{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/SavedExport/edit.html.twig b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/edit.html.twig new file mode 100644 index 000000000..937d42201 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/edit.html.twig @@ -0,0 +1,21 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% block title %}{{ 'saved_export.Edit'|trans }}{% endblock %} + +{% block content %} +

{{ block('title') }}

+ + {{ form_start(form) }} + {{ form_row(form.title) }} + {{ form_row(form.description) }} + + + {{ form_end(form) }} +{% endblock %} \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Resources/views/SavedExport/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/index.html.twig new file mode 100644 index 000000000..17792ecd2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/index.html.twig @@ -0,0 +1,82 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %} + +{% block content %} +
+ + {{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }} + +
+ + {% if total == 0 %} +

{{ 'saved_export.Any saved export'|trans }}

+ {% endif %} + + {% for group, saveds in grouped_exports %} + {% if group != '_' %} +

{{ group }}

+
+ {% for s in saveds %} +
+
+
+

{{ s.saved.title }}

+

{{ s.export.title|trans }}

+ +
+ {{ s.saved.description|chill_markdown_to_html }} +
+ +
{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}
+ +
    +
  • +
  • +
  • +
+ +
+
+
+ {% endfor %} +
+ {% endif %} + {% endfor %} + + {% if grouped_exports|keys|length > 1 and grouped_exports['_']|length > 0 %} +

{{ 'Ungrouped exports'|trans }}

+ {% endif %} + +
+ {% for saveds in grouped_exports['_']|default([]) %} + {% for s in saveds %} +
+
+
+
+

{{ s.saved.title }}

+

{{ s.export.title|trans }}

+ +
+ {{ s.saved.description|chill_markdown_to_html }} +
+ +
{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}
+ +
    +
  • +
  • +
  • +
+ +
+
+
+
+ {% endfor %} + {% endfor %} +
+
+
+{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/SavedExport/new.html.twig b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/new.html.twig new file mode 100644 index 000000000..037adb0fc --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/new.html.twig @@ -0,0 +1,21 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% block title %}{{ 'saved_export.New'|trans }}{% endblock %} + +{% block content %} +

{{ block('title') }}

+ + {{ form_start(form) }} + {{ form_row(form.title) }} + {{ form_row(form.description) }} + + + {{ form_end(form) }} +{% endblock %} \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/SavedExportVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/SavedExportVoter.php new file mode 100644 index 000000000..667bced46 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Security/Authorization/SavedExportVoter.php @@ -0,0 +1,52 @@ +getUser() === $token->getUser(); + + default: + throw new UnexpectedValueException('attribute not supported: ' . $attribute); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDate.php b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDate.php new file mode 100644 index 000000000..e8427c62f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDate.php @@ -0,0 +1,101 @@ +roll = $roll; + $this->pivotDate = $pivotDate ?? new DateTimeImmutable('now'); + $this->fixedDate = $fixedDate; + } + + public function getFixedDate(): ?DateTimeImmutable + { + return $this->fixedDate; + } + + public function getPivotDate(): DateTimeImmutable + { + return $this->pivotDate; + } + + public function getRoll(): string + { + return $this->roll; + } +} diff --git a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php new file mode 100644 index 000000000..026ff7a8a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php @@ -0,0 +1,142 @@ +getRoll()) { + case RollingDate::T_MONTH_CURRENT_START: + return $this->toBeginOfMonth($rollingDate->getPivotDate()); + + case RollingDate::T_MONTH_NEXT_START: + return $this->toBeginOfMonth($rollingDate->getPivotDate()->add(new DateInterval('P1M'))); + + case RollingDate::T_MONTH_PREVIOUS_START: + return $this->toBeginOfMonth($rollingDate->getPivotDate()->sub(new DateInterval('P1M'))); + + case RollingDate::T_QUARTER_CURRENT_START: + return $this->toBeginOfQuarter($rollingDate->getPivotDate()); + + case RollingDate::T_QUARTER_NEXT_START: + return $this->toBeginOfQuarter($rollingDate->getPivotDate()->add(new DateInterval('P3M'))); + + case RollingDate::T_QUARTER_PREVIOUS_START: + return $this->toBeginOfQuarter($rollingDate->getPivotDate()->sub(new DateInterval('P3M'))); + + case RollingDate::T_WEEK_CURRENT_START: + return $this->toBeginOfWeek($rollingDate->getPivotDate()); + + case RollingDate::T_WEEK_NEXT_START: + return $this->toBeginOfWeek($rollingDate->getPivotDate()->add(new DateInterval('P1W'))); + + case RollingDate::T_WEEK_PREVIOUS_START: + return $this->toBeginOfWeek($rollingDate->getPivotDate()->sub(new DateInterval('P1W'))); + + case RollingDate::T_YEAR_CURRENT_START: + return $this->toBeginOfYear($rollingDate->getPivotDate()); + + case RollingDate::T_YEAR_PREVIOUS_START: + return $this->toBeginOfYear($rollingDate->getPivotDate()->sub(new DateInterval('P1Y'))); + + case RollingDate::T_YEAR_NEXT_START: + return $this->toBeginOfYear($rollingDate->getPivotDate()->add(new DateInterval('P1Y'))); + + case RollingDate::T_TODAY: + return $rollingDate->getPivotDate(); + + case RollingDate::T_FIXED_DATE: + if (null === $rollingDate->getFixedDate()) { + throw new LogicException('You must provide a fixed date when selecting a fixed date'); + } + + return $rollingDate->getFixedDate(); + + default: + throw new UnexpectedValueException(sprintf('%s rolling operation not supported', $rollingDate->getRoll())); + } + } + + private function toBeginOfMonth(DateTimeImmutable $date): DateTimeImmutable + { + return DateTimeImmutable::createFromFormat( + 'Y-m-d His', + sprintf('%s-%s-01 000000', $date->format('Y'), $date->format('m')) + ); + } + + private function toBeginOfQuarter(DateTimeImmutable $date): DateTimeImmutable + { + switch ((int) $date->format('n')) { + case 1: + case 2: + case 3: + $month = '01'; + + break; + + case 4: + case 5: + case 6: + $month = '04'; + + break; + + case 7: + case 8: + case 9: + $month = '07'; + + break; + + case 10: + case 11: + case 12: + $month = '10'; + + break; + + default: + throw new LogicException('this month is not valid: ' . $date->format('n')); + } + + return DateTimeImmutable::createFromFormat( + 'Y-m-d His', + sprintf('%s-%s-01 000000', $date->format('Y'), $month) + ); + } + + private function toBeginOfWeek(DateTimeImmutable $date): DateTimeImmutable + { + if (1 === $dayOfWeek = (int) $date->format('N')) { + return $date->setTime(0, 0, 0); + } + + return $date + ->sub(new DateInterval('P' . ($dayOfWeek - 1) . 'D')) + ->setTime(0, 0, 0); + } + + private function toBeginOfYear(DateTimeImmutable $date): DateTimeImmutable + { + return DateTimeImmutable::createFromFormat( + 'Y-m-d His', + sprintf('%s-01-01 000000', $date->format('Y')) + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php new file mode 100644 index 000000000..b20a5ced2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php @@ -0,0 +1,19 @@ +assertEquals($senderId, $notification->getSender()->getId()); $this->assertCount(count($addressesIds), $notification->getUnreadBy()); - $unreadIds = $notification->getUnreadBy()->map(static function (User $u) { return $u->getId(); }); + $unreadIds = $notification->getUnreadBy()->map(static function (User $u) { + return $u->getId(); + }); foreach ($addressesIds as $addresseeId) { $this->assertContains($addresseeId, $unreadIds); diff --git a/src/Bundle/ChillMainBundle/Tests/Export/ExportManagerTest.php b/src/Bundle/ChillMainBundle/Tests/Export/ExportManagerTest.php index b3f5eac85..701b3d14e 100644 --- a/src/Bundle/ChillMainBundle/Tests/Export/ExportManagerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Export/ExportManagerTest.php @@ -680,10 +680,12 @@ final class ExportManagerTest extends KernelTestCase return new ExportManager( $logger ?? self::$container->get('logger'), - $em ?? self::$container->get('doctrine.orm.entity_manager'), $authorizationChecker ?? self::$container->get('security.authorization_checker'), $authorizationHelper ?? self::$container->get('chill.main.security.authorization.helper'), - $tokenStorage + $tokenStorage, + [], + [], + [] ); } } diff --git a/src/Bundle/ChillMainBundle/Tests/Form/Type/PickRollingDateTypeTest.php b/src/Bundle/ChillMainBundle/Tests/Form/Type/PickRollingDateTypeTest.php new file mode 100644 index 000000000..771dd0310 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Form/Type/PickRollingDateTypeTest.php @@ -0,0 +1,53 @@ + 'year_previous_start', + 'fixedDate' => null, + ]; + + $form = $this->factory->create(PickRollingDateType::class); + + $form->submit($formData); + + $this->assertTrue($form->isSynchronized()); + + /** @var RollingDate $rollingDate */ + $rollingDate = $form->getData(); + + $this->assertInstanceOf(RollingDate::class, $rollingDate); + $this->assertEquals(RollingDate::T_YEAR_PREVIOUS_START, $rollingDate->getRoll()); + } + + protected function getExtensions(): array + { + $type = new PickRollingDateType(); + + return [ + new PreloadedExtension([$type], []), + ]; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Form/Type/ScopePickerTypeTest.php b/src/Bundle/ChillMainBundle/Tests/Form/Type/ScopePickerTypeTest.php index d64f68951..ce41c9a21 100644 --- a/src/Bundle/ChillMainBundle/Tests/Form/Type/ScopePickerTypeTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Form/Type/ScopePickerTypeTest.php @@ -96,7 +96,9 @@ final class ScopePickerTypeTest extends TypeTestCase $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class); $translatableStringHelper->localize(Argument::type('array'))->will( - static function ($args) { return $args[0]['fr']; } + static function ($args) { + return $args[0]['fr']; + } ); $type = new ScopePickerType( diff --git a/src/Bundle/ChillMainBundle/Tests/Security/Authorization/AuthorizationHelperTest.php b/src/Bundle/ChillMainBundle/Tests/Security/Authorization/AuthorizationHelperTest.php index 02d319042..cc5a49096 100644 --- a/src/Bundle/ChillMainBundle/Tests/Security/Authorization/AuthorizationHelperTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Security/Authorization/AuthorizationHelperTest.php @@ -211,7 +211,9 @@ final class AuthorizationHelperTest extends KernelTestCase $centerA ); - $usernames = array_map(static function (User $u) { return $u->getUsername(); }, $users); + $usernames = array_map(static function (User $u) { + return $u->getUsername(); + }, $users); $this->assertContains('center a_social', $usernames); } diff --git a/src/Bundle/ChillMainBundle/Tests/Services/RollingDate/RollingDateConverterTest.php b/src/Bundle/ChillMainBundle/Tests/Services/RollingDate/RollingDateConverterTest.php new file mode 100644 index 000000000..6df2c889b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Services/RollingDate/RollingDateConverterTest.php @@ -0,0 +1,101 @@ +converter = new RollingDateConverter(); + } + + public function generateDataConversionDate(): iterable + { + $format = 'Y-m-d His'; + + yield [RollingDate::T_MONTH_CURRENT_START, '2022-11-01 000000', $format]; + + yield [RollingDate::T_MONTH_NEXT_START, '2022-12-01 000000', $format]; + + yield [RollingDate::T_MONTH_PREVIOUS_START, '2022-10-01 000000', $format]; + + yield [RollingDate::T_QUARTER_CURRENT_START, '2022-10-01 000000', $format]; + + yield [RollingDate::T_QUARTER_NEXT_START, '2023-01-01 000000', $format]; + + yield [RollingDate::T_QUARTER_PREVIOUS_START, '2022-07-01 000000', $format]; + + yield [RollingDate::T_TODAY, '2022-11-07 000000', $format]; + + yield [RollingDate::T_WEEK_CURRENT_START, '2022-11-07 000000', $format]; + + yield [RollingDate::T_WEEK_NEXT_START, '2022-11-14 000000', $format]; + + yield [RollingDate::T_WEEK_PREVIOUS_START, '2022-10-31 000000', $format]; + + yield [RollingDate::T_YEAR_CURRENT_START, '2022-01-01 000000', $format]; + + yield [RollingDate::T_YEAR_NEXT_START, '2023-01-01 000000', $format]; + + yield [RollingDate::T_YEAR_PREVIOUS_START, '2021-01-01 000000', $format]; + } + + public function testConversionFixedDate() + { + $rollingDate = new RollingDate(RollingDate::T_FIXED_DATE, new DateTimeImmutable('2022-01-01')); + + $this->assertEquals( + '2022-01-01', + $this->converter->convert($rollingDate)->format('Y-m-d') + ); + } + + public function testConvertOnDateNow() + { + $rollingDate = new RollingDate(RollingDate::T_YEAR_PREVIOUS_START); + + $actual = $this->converter->convert($rollingDate); + + $this->assertEquals( + (int) (new DateTimeImmutable('now'))->format('Y') - 1, + (int) $actual->format('Y') + ); + $this->assertEquals(1, (int) $actual->format('m')); + $this->assertEquals(1, (int) $actual->format('d')); + } + + /** + * @dataProvider generateDataConversionDate + */ + public function testConvertOnPivotDate(string $roll, string $expectedDateTime, string $format) + { + $pivot = DateTimeImmutable::createFromFormat('Y-m-d His', '2022-11-07 000000'); + $rollingDate = new RollingDate($roll, null, $pivot); + + $this->assertEquals( + DateTime::createFromFormat($format, $expectedDateTime), + $this->converter->convert($rollingDate) + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php index 608ae6fa1..7d2cadc37 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php @@ -107,7 +107,9 @@ class NotificationOnTransition implements EventSubscriberInterface 'dest' => $subscriber, 'place' => $place, 'workflow' => $workflow, - 'is_dest' => in_array($subscriber->getId(), array_map(static function (User $u) { return $u->getId(); }, $entityWorkflow->futureDestUsers), true), + 'is_dest' => in_array($subscriber->getId(), array_map(static function (User $u) { + return $u->getId(); + }, $entityWorkflow->futureDestUsers), true), ]; $notification = new Notification(); diff --git a/src/Bundle/ChillMainBundle/config/services.yaml b/src/Bundle/ChillMainBundle/config/services.yaml index b44f4b51c..f99c80d2c 100644 --- a/src/Bundle/ChillMainBundle/config/services.yaml +++ b/src/Bundle/ChillMainBundle/config/services.yaml @@ -106,3 +106,8 @@ services: resource: '../Service/Import/' autowire: true autoconfigure: true + + Chill\MainBundle\Service\RollingDate\: + resource: '../Service/RollingDate/' + autowire: true + autoconfigure: true diff --git a/src/Bundle/ChillMainBundle/config/services/export.yaml b/src/Bundle/ChillMainBundle/config/services/export.yaml index d330f30f5..ea7328839 100644 --- a/src/Bundle/ChillMainBundle/config/services/export.yaml +++ b/src/Bundle/ChillMainBundle/config/services/export.yaml @@ -3,6 +3,9 @@ services: autowire: true autoconfigure: true + Chill\MainBundle\Export\Helper\: + resource: '../../Export/Helper' + chill.main.export_element_validator: class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator tags: diff --git a/src/Bundle/ChillMainBundle/config/services/security.yaml b/src/Bundle/ChillMainBundle/config/services/security.yaml index 3347871e3..824144470 100644 --- a/src/Bundle/ChillMainBundle/config/services/security.yaml +++ b/src/Bundle/ChillMainBundle/config/services/security.yaml @@ -50,6 +50,8 @@ services: tags: - { name: security.voter } + Chill\MainBundle\Security\Authorization\SavedExportVoter: ~ + Chill\MainBundle\Security\PasswordRecover\TokenManager: arguments: $secret: '%kernel.secret%' diff --git a/src/Bundle/ChillMainBundle/migrations/Version20221107212201.php b/src/Bundle/ChillMainBundle/migrations/Version20221107212201.php new file mode 100644 index 000000000..58285c333 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20221107212201.php @@ -0,0 +1,43 @@ +addSql('DROP TABLE chill_main_saved_export'); + } + + public function getDescription(): string + { + return 'Create table for saved exports'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE TABLE chill_main_saved_export (id UUID NOT NULL, user_id INT DEFAULT NULL, description TEXT DEFAULT \'\' NOT NULL, exportAlias TEXT DEFAULT \'\' NOT NULL, options JSONB DEFAULT \'[]\' NOT NULL, title TEXT DEFAULT \'\' NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C2029B22A76ED395 ON chill_main_saved_export (user_id)'); + $this->addSql('CREATE INDEX IDX_C2029B223174800F ON chill_main_saved_export (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_C2029B2265FF1AEC ON chill_main_saved_export (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_saved_export.id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN chill_main_saved_export.options IS \'(DC2Type:json)\''); + $this->addSql('COMMENT ON COLUMN chill_main_saved_export.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_saved_export.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B22A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B223174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B2265FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index b4ce910c4..fe4d5c0ed 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -39,6 +39,8 @@ Last updated by: Dernière mise à jour par on: "le " Last updated on: Dernière mise à jour le by_user: "par " +lifecycleUpdate: Evenements de création et mise à jour +address_fields: Données liées à l'adresse Edit: Modifier Update: Mettre à jour @@ -57,6 +59,7 @@ Until: Jusqu'au #elements used in software centers: centres Centers: Centres +center: centre comment: commentaire Comment: Commentaire Pinned comment: Commentaire épinglé @@ -79,17 +82,17 @@ Postal code: Code postal Valid from: Valide à partir du Choose a postal code: Choisir un code postal address: - address_homeless: L'adresse est-elle celle d'un domicile fixe ? - real address: Adresse d'un domicile - consider homeless: Cette adresse est incomplète + address_homeless: L'adresse est-elle celle d'un domicile fixe ? + real address: Adresse d'un domicile + consider homeless: Cette adresse est incomplète address more: - floor: ét - corridor: coul - steps: esc - flat: appart - buildingName: résidence - extra: "" - distribution: cedex + floor: ét + corridor: coul + steps: esc + flat: appart + buildingName: résidence + extra: "" + distribution: cedex Create a new address: Créer une nouvelle adresse Create an address: Créer une adresse Update address: Modifier l'adresse @@ -125,7 +128,7 @@ Location and location type: Localisations et types de localisation Back to the admin: Menu d'administration "Administration interface": Interface d'administration Welcome to the admin section !: > - Bienvenue dans l'interface d'administration ! + Bienvenue dans l'interface d'administration ! #permissions Permissions Menu: Gestion des droits @@ -236,6 +239,7 @@ Default for: Type de localisation par défaut pour none: aucun person: usager thirdparty: tiers +civility: civilité #admin section for civility abbreviation: abbréviation @@ -334,69 +338,69 @@ Impersonate: Incarner l'utilisateur Impersonate mode: Mode fantôme crud: - # general items - new: - button_action_form: Créer - link_edit: Modifier - save_and_close: Créer & fermer - save_and_show: Créer & voir - save_and_new: Créer & nouveau - success: Les données ont été créées - edit: - button_action_form: Enregistrer - back_to_view: Voir - save_and_close: Enregistrer & fermer - save_and_show: Enregistrer & voir - success: Les données ont été modifiées - delete: - success: Les données ont été supprimées - link_to_form: Supprimer - default: - success: Les données ont été enregistrées - view: - link_duplicate: Dupliquer - admin_user: - index: - title: Utilisateurs - add_new: Créer - title_edit: Modifier un utilisateur - title_new: Créer un utilisateur - admin_user_job: - index: - title: Métiers - add_new: Créer - title_new: Nouveau métier - title_edit: Modifier un métier - main_location_type: - index: - title: Liste des types de localisations - add_new: Ajouter un type de localisation - title_new: Nouveau type de localisation - title_edit: Modifier un type de localisation - main_location: - index: - title: Liste des localisations - add_new: Ajouter une localisation - title_new: Nouvelle localisation - title_edit: Modifier une localisation - main_language: - index: - title: Liste des langues - add_new: Ajouter une langue - title_new: Nouvelle langue - title_edit: Modifier une langue - main_country: - index: - title: Liste des pays - add_new: Ajouter un pays - title_new: Nouveau pays - title_edit: Modifier un pays - main_civility: - index: - title: Liste des civilités - add_new: Ajouter une civilité - title_new: Nouvelle civilité - title_edit: Modifier une civilité + # general items + new: + button_action_form: Créer + link_edit: Modifier + save_and_close: Créer & fermer + save_and_show: Créer & voir + save_and_new: Créer & nouveau + success: Les données ont été créées + edit: + button_action_form: Enregistrer + back_to_view: Voir + save_and_close: Enregistrer & fermer + save_and_show: Enregistrer & voir + success: Les données ont été modifiées + delete: + success: Les données ont été supprimées + link_to_form: Supprimer + default: + success: Les données ont été enregistrées + view: + link_duplicate: Dupliquer + admin_user: + index: + title: Utilisateurs + add_new: Créer + title_edit: Modifier un utilisateur + title_new: Créer un utilisateur + admin_user_job: + index: + title: Métiers + add_new: Créer + title_new: Nouveau métier + title_edit: Modifier un métier + main_location_type: + index: + title: Liste des types de localisations + add_new: Ajouter un type de localisation + title_new: Nouveau type de localisation + title_edit: Modifier un type de localisation + main_location: + index: + title: Liste des localisations + add_new: Ajouter une localisation + title_new: Nouvelle localisation + title_edit: Modifier une localisation + main_language: + index: + title: Liste des langues + add_new: Ajouter une langue + title_new: Nouvelle langue + title_edit: Modifier une langue + main_country: + index: + title: Liste des pays + add_new: Ajouter un pays + title_new: Nouveau pays + title_edit: Modifier un pays + main_civility: + index: + title: Liste des civilités + add_new: Ajouter une civilité + title_new: Nouvelle civilité + title_edit: Modifier une civilité No entities: Aucun élément @@ -515,3 +519,51 @@ notification: Remove an email: Supprimer l'adresse email Email with access link: Adresse email ayant reçu un lien d'accès +export: + address_helper: + id: Identifiant de l'adresse + street: Voie + streetNumber: Numéro de voie + buildingName: Résidence + corridor: Couloir + distribution: Distribution + extra: Extra + flat: Appartement + floor: Étage + postcode_code: Code postal + postcode_name: Libellé du code postal + country: Pays + _as_string: Adresse formattée + confidential: Adresse confidentielle ? + isNoAddress: Adresse incomplète ? + _lat: Latitude + _lon: Longitude + +rolling_date: + year_previous_start: Début de l'année précédente + quarter_previous_start: Début du trimestre précédent + month_previous_start: Début du mois précédent + week_previous_start: Début de la semaine précédente + year_current_start: Début de l'année courante + quarter_current_start: Début du trimestre courant + month_current_start: Début du mois courant + week_current_start: Début de la semaine courante + today: Aujourd'hui (aucune modification de la date courante) + year_next_start: Début de l'année suivante + quarter_next_start: Début du trimestre suivante + month_next_start: Début du mois suivant + week_next_start: Début de la semaine suivante + fixed_date: Date fixe + roll_movement: Modification par rapport à aujourd'hui + fixed_date_date: Date fixe + +saved_export: + Any saved export: Aucun rapport enregistré + New: Nouveau rapport enregistré + Edit: Modifier un rapport enregistré + Delete saved ?: Supprimer un rapport enregistré ? + Are you sure you want to delete this saved ?: Êtes-vous sûr·e de vouloir supprimer ce rapport ? + My saved exports: Mes rapports enregistrés + Export is deleted: Le rapport est supprimé + Saved export is saved!: Le rapport est enregistré + Created on %date%: Créé le %date% diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php b/src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php index 502613caf..5fecc04b6 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php @@ -84,7 +84,7 @@ class PersonHouseholdAddress public function getHousehold(): ?Household { - return $this->relation; + return $this->household; } public function getPerson(): ?Person diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index 1134674df..a477f9adb 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -1768,7 +1768,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI } /** - * @param type $spokenLanguages + * @param Collection $spokenLanguages */ public function setSpokenLanguages($spokenLanguages): self { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByActionNumberAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByActionNumberAggregator.php new file mode 100644 index 000000000..2dffccbbb --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByActionNumberAggregator.php @@ -0,0 +1,67 @@ +addSelect('(SELECT COUNT(acp_by_action_action.id) FROM ' . AccompanyingPeriodWork::class . ' acp_by_action_action WHERE acp_by_action_action.accompanyingPeriod = acp) AS acp_by_action_number_aggregator') + ->addGroupBy('acp_by_action_number_aggregator'); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + // No form needed + } + + public function getLabels($key, array $values, $data) + { + return static function ($value) { + if ('_header' === $value) { + return 'export.aggregator.course.by_number_of_action.Number of actions'; + } + + if (null === $value) { + return ''; + } + + return $value; + }; + } + + public function getQueryKeys($data): array + { + return ['acp_by_action_number_aggregator']; + } + + public function getTitle(): string + { + return 'Group by number of actions'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByHouseholdCompositionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByHouseholdCompositionAggregator.php index e96c8a228..f299762e6 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByHouseholdCompositionAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByHouseholdCompositionAggregator.php @@ -18,6 +18,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdComposition; use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Repository\Household\HouseholdCompositionTypeRepositoryInterface; +use DateTimeImmutable; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; @@ -96,7 +97,7 @@ class ByHouseholdCompositionAggregator implements AggregatorInterface $builder->add('date_calc', ChillDateType::class, [ 'label' => 'export.aggregator.course.by_household_composition.Calc date', 'input_format' => 'datetime_immutable', - 'data' => new \DateTimeImmutable('now'), + 'data' => new DateTimeImmutable('now'), ]); } diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/CreatorJobAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/CreatorJobAggregator.php new file mode 100644 index 000000000..6c958ca6c --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/CreatorJobAggregator.php @@ -0,0 +1,87 @@ +jobRepository = $jobRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('acp_creator', $qb->getAllAliases(), true)) { + $qb->leftJoin('acp.createdBy', 'acp_creator'); + } + + $qb->addSelect('IDENTITY(acp_creator.userJob) AS acp_creator_job_aggregator') + ->addGroupBy('acp_creator_job_aggregator'); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + // No form needed + } + + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'export.aggregator.course.by_creator_job.Creator\'s job'; + } + + if (null === $value || null === $j = $this->jobRepository->find($value)) { + return ''; + } + + return $this->translatableStringHelper->localize( + $j->getLabel() + ); + }; + } + + public function getQueryKeys($data): array + { + return ['acp_creator_job_aggregator']; + } + + public function getTitle(): string + { + return 'Group by creator job'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php index fc2fcc7b2..e17e9f9a5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php @@ -12,10 +12,11 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; use Chill\MainBundle\Export\AggregatorInterface; -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\Export\Declarations; -use DateTime; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -27,11 +28,15 @@ final class StepAggregator implements AggregatorInterface private const P = 'acp_step_agg_date'; + private RollingDateConverterInterface $rollingDateConverter; + private TranslatorInterface $translator; public function __construct( + RollingDateConverterInterface $rollingDateConverter, TranslatorInterface $translator ) { + $this->rollingDateConverter = $rollingDateConverter; $this->translator = $translator; } @@ -60,7 +65,7 @@ final class StepAggregator implements AggregatorInterface ) ) ) - ->setParameter(self::P, $data['on_date']) + ->setParameter(self::P, $this->rollingDateConverter->convert($data['on_date'])) ->addGroupBy('step_aggregator'); } @@ -71,8 +76,8 @@ final class StepAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('on_date', ChillDateType::class, [ - 'data' => new DateTime(), + $builder->add('on_date', PickRollingDateType::class, [ + 'data' => new RollingDate(RollingDate::T_TODAY), ]); } diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByEndDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByEndDateAggregator.php new file mode 100644 index 000000000..2f4275c49 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByEndDateAggregator.php @@ -0,0 +1,106 @@ + 'week', + 'by month' => 'month', + 'by year' => 'year', + ]; + + private const DEFAULT_CHOICE = 'year'; + + private TranslatorInterface $translator; + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + switch ($data['frequency']) { + case 'week': + $fmt = 'YYYY-IW'; + + break; + + case 'month': + $fmt = 'YYYY-MM'; + + break; + + case 'year': + $fmt = 'YYYY'; + + break; + + default: + throw new LogicException(sprintf("The frequency data '%s' is invalid.", $data['frequency'])); + } + + $qb->addSelect(sprintf("TO_CHAR(workeval.endDate, '%s') AS eval_by_end_date_aggregator", $fmt)); + $qb->addGroupBy(' eval_by_end_date_aggregator'); + $qb->addOrderBy(' eval_by_end_date_aggregator', 'ASC'); + } + + public function applyOn(): string + { + return Declarations::EVAL_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('frequency', ChoiceType::class, [ + 'choices' => self::CHOICES, + 'multiple' => false, + 'expanded' => true, + 'empty_data' => self::DEFAULT_CHOICE, + 'data' => self::DEFAULT_CHOICE, + ]); + } + + public function getLabels($key, array $values, $data) + { + return static function ($value): string { + if ('_header' === $value) { + return 'export.aggregator.eval.by_end_date.End date period'; + } + + if (null === $value) { + return ''; + } + + return $value; + }; + } + + public function getQueryKeys($data): array + { + return ['eval_by_end_date_aggregator']; + } + + public function getTitle(): string + { + return 'export.aggregator.eval.by_end_date.Group by end date evaluations'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByMaxDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByMaxDateAggregator.php new file mode 100644 index 000000000..9283fd9dc --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByMaxDateAggregator.php @@ -0,0 +1,106 @@ + 'week', + 'by month' => 'month', + 'by year' => 'year', + ]; + + private const DEFAULT_CHOICE = 'year'; + + private TranslatorInterface $translator; + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + switch ($data['frequency']) { + case 'week': + $fmt = 'YYYY-IW'; + + break; + + case 'month': + $fmt = 'YYYY-MM'; + + break; + + case 'year': + $fmt = 'YYYY'; + + break; + + default: + throw new LogicException(sprintf("The frequency data '%s' is invalid.", $data['frequency'])); + } + + $qb->addSelect(sprintf("TO_CHAR(workeval.maxDate, '%s') AS eval_by_max_date_aggregator", $fmt)); + $qb->addGroupBy(' eval_by_max_date_aggregator'); + $qb->addOrderBy(' eval_by_max_date_aggregator', 'ASC'); + } + + public function applyOn(): string + { + return Declarations::EVAL_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('frequency', ChoiceType::class, [ + 'choices' => self::CHOICES, + 'multiple' => false, + 'expanded' => true, + 'empty_data' => self::DEFAULT_CHOICE, + 'data' => self::DEFAULT_CHOICE, + ]); + } + + public function getLabels($key, array $values, $data) + { + return static function ($value): string { + if ('_header' === $value) { + return 'export.aggregator.eval.by_max_date.Max date'; + } + + if (null === $value) { + return ''; + } + + return $value; + }; + } + + public function getQueryKeys($data): array + { + return ['eval_by_max_date_aggregator']; + } + + public function getTitle(): string + { + return 'export.aggregator.eval.by_max_date.Group by max date evaluations'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByStartDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByStartDateAggregator.php new file mode 100644 index 000000000..cd183b25e --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByStartDateAggregator.php @@ -0,0 +1,106 @@ + 'week', + 'by month' => 'month', + 'by year' => 'year', + ]; + + private const DEFAULT_CHOICE = 'year'; + + private TranslatorInterface $translator; + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + switch ($data['frequency']) { + case 'week': + $fmt = 'YYYY-IW'; + + break; + + case 'month': + $fmt = 'YYYY-MM'; + + break; + + case 'year': + $fmt = 'YYYY'; + + break; + + default: + throw new LogicException(sprintf("The frequency data '%s' is invalid.", $data['frequency'])); + } + + $qb->addSelect(sprintf("TO_CHAR(workeval.startDate, '%s') AS eval_by_start_date_aggregator", $fmt)); + $qb->addGroupBy(' eval_by_start_date_aggregator'); + $qb->addOrderBy(' eval_by_start_date_aggregator', 'ASC'); + } + + public function applyOn(): string + { + return Declarations::EVAL_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('frequency', ChoiceType::class, [ + 'choices' => self::CHOICES, + 'multiple' => false, + 'expanded' => true, + 'empty_data' => self::DEFAULT_CHOICE, + 'data' => self::DEFAULT_CHOICE, + ]); + } + + public function getLabels($key, array $values, $data) + { + return static function ($value): string { + if ('_header' === $value) { + return 'export.aggregator.eval.by_start_date_period.Start date period'; + } + + if (null === $value) { + return ''; + } + + return $value; + }; + } + + public function getQueryKeys($data): array + { + return ['eval_by_start_date_aggregator']; + } + + public function getTitle(): string + { + return 'export.aggregator.eval.by_start_date_period.Group by start date evaluations'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/HavingEndDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/HavingEndDateAggregator.php new file mode 100644 index 000000000..e33bb326f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/HavingEndDateAggregator.php @@ -0,0 +1,81 @@ +translator = $translator; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->addSelect('CASE WHEN workeval.endDate IS NULL THEN true ELSE false END AS eval_enddate_aggregator') + ->addGroupBy('eval_enddate_aggregator'); + } + + public function applyOn(): string + { + return Declarations::EVAL_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + // No form needed + } + + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'export.aggregator.eval.by_end_date.Has end date ?'; + } + + switch ($value) { + case true: + return $this->translator->trans('export.aggregator.eval.by_end_date.enddate is specified'); + + case false: + return $this->translator->trans('export.aggregator.eval.by_end_date.enddate is not specified'); + + default: + throw new LogicException(sprintf('The value %s is not valid', $value)); + } + }; + } + + public function getQueryKeys($data): array + { + return ['eval_enddate_aggregator']; + } + + public function getTitle(): string + { + return 'export.aggregator.eval.by_end_date.Group evaluations having end date'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CurrentActionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CurrentActionAggregator.php new file mode 100644 index 000000000..58fcb2874 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CurrentActionAggregator.php @@ -0,0 +1,84 @@ +translator = $translator; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data): void + { + $qb + ->addSelect(' + (CASE WHEN acpw.endDate IS NULL THEN true ELSE false END) + AS acpw_current_action_aggregator + ') + ->addGroupBy('acpw_current_action_aggregator'); + } + + public function applyOn(): string + { + return Declarations::SOCIAL_WORK_ACTION_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + // No form needed + } + + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'export.aggregator.course_work.by_current_action.Current action ?'; + } + + switch ($value) { + case true: + return $this->translator->trans('export.aggregator.course_work.by_current_action.Current action'); + + case false: + return $this->translator->trans('export.aggregator.course_work.by_current_action.Not current action'); + + default: + throw new LogicException(sprintf('The value %s is not valid', $value)); + } + }; + } + + public function getQueryKeys($data): array + { + return ['acpw_current_action_aggregator']; + } + + public function getTitle(): string + { + return 'export.aggregator.course_work.by_current_action.Group by current actions'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php index 8c8744dfd..d07ee3639 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php @@ -38,7 +38,7 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder): void { - // TODO: Implement buildForm() method. + // Nothing to add here } public function getAllowedFormattersTypes(): array diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php new file mode 100644 index 000000000..28ad94bd3 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php @@ -0,0 +1,416 @@ +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->userHelper = $userHelper; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder + ->add('calc_date', ChillDateType::class, [ + 'input' => 'datetime_immutable', + 'label' => 'export.list.acp.Date of calculation for associated elements', + 'help' => 'export.list.acp.The associated referree, localisation, and other elements will be valid at this date', + 'required' => true, + ]); + } + + public function getAllowedFormattersTypes() + { + return [FormatterInterface::TYPE_LIST]; + } + + public function getDescription() + { + return 'export.list.acp.Generate a list of accompanying periods, filtered on different parameters.'; + } + + public function getGroup(): string + { + return 'Exports of accompanying courses'; + } + + public function getLabels($key, array $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)); + }; + + 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) + ) + ); + }; + + 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) + ) + ); + }; + + 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 array_merge( + self::FIELDS, + $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') + ); + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR); + } + + public function getTitle() + { + return 'export.list.acp.List of accompanying periods'; + } + + public function getType() + { + return Declarations::PERSON_TYPE; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $centers = array_map(static function ($el) { + return $el['center']; + }, $acl); + + $qb = $this->entityManager->createQueryBuilder(); + + $qb + ->from(AccompanyingPeriod::class, 'acp') + ->andWhere('acp.step != :list_acp_step') + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . AccompanyingPeriodParticipation::class . ' acl_count_part + JOIN ' . PersonCenterHistory::class . ' acl_count_person_history WITH IDENTITY(acl_count_person_history.person) = IDENTITY(acl_count_part.person) + WHERE acl_count_part.accompanyingPeriod = acp.id AND acl_count_person_history.center IN (:authorized_centers) + ' + ) + ) + ->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT) + ->setParameter('authorized_centers', $centers); + + $this->addSelectClauses($qb, $data['calc_date']); + + return $qb; + } + + public function requiredRole(): string + { + return PersonVoter::LISTS; + } + + public function supportsModifiers() + { + return [ + 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') + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('personAddress'), + $qb->expr()->andX( + $qb->expr()->lte('personAddress.validFrom', ':calcDate'), + $qb->expr()->orX($qb->expr()->isNull('personAddress.validTo'), $qb->expr()->gt('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); + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php index 5f5d95034..acaeb498c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php @@ -17,21 +17,22 @@ use Chill\CustomFieldsBundle\Service\CustomFieldProvider; 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\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Export\Declarations; +use Chill\PersonBundle\Export\Helper\ListPersonHelper; use Chill\PersonBundle\Security\Authorization\PersonVoter; -use DateTime; +use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query; -use Exception; +use PhpOffice\PhpSpreadsheet\Shared\Date; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Contracts\Translation\TranslatorInterface; use function addcslashes; use function array_key_exists; @@ -39,7 +40,7 @@ use function array_keys; use function array_merge; use function count; use function in_array; -use function strtolower; +use function strlen; use function uniqid; /** @@ -47,40 +48,35 @@ use function uniqid; */ class ListPerson implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface { - protected CustomFieldProvider $customFieldProvider; + private ExportAddressHelper $addressHelper; - protected EntityManagerInterface $entityManager; + private CustomFieldProvider $customFieldProvider; - protected array $fields = [ - 'id', 'firstName', 'lastName', 'birthdate', - 'placeOfBirth', 'gender', 'memo', 'email', 'phonenumber', - 'mobilenumber', 'contactInfo', 'countryOfBirth', 'nationality', - 'address_street_address_1', 'address_street_address_2', - 'address_valid_from', 'address_postcode_label', 'address_postcode_code', - 'address_country_name', 'address_country_code', 'address_isnoaddress', - ]; + private EntityManagerInterface $entityManager; - protected TranslatableStringHelper $translatableStringHelper; - - protected TranslatorInterface $translator; + private ListPersonHelper $listPersonHelper; private $slugs = []; + private TranslatableStringHelper $translatableStringHelper; + public function __construct( + ExportAddressHelper $addressHelper, + CustomFieldProvider $customFieldProvider, + ListPersonHelper $listPersonHelper, EntityManagerInterface $em, - TranslatorInterface $translator, - TranslatableStringHelper $translatableStringHelper, - CustomFieldProvider $customFieldProvider + TranslatableStringHelper $translatableStringHelper ) { - $this->entityManager = $em; - $this->translator = $translator; - $this->translatableStringHelper = $translatableStringHelper; + $this->addressHelper = $addressHelper; $this->customFieldProvider = $customFieldProvider; + $this->listPersonHelper = $listPersonHelper; + $this->entityManager = $em; + $this->translatableStringHelper = $translatableStringHelper; } public function buildForm(FormBuilderInterface $builder) { - $choices = array_combine($this->fields, $this->fields); + $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); foreach ($this->getCustomFields() as $cf) { $choices[$this->translatableStringHelper->localize($cf->getName())] @@ -96,7 +92,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou 'label' => 'Fields to include in export', 'choice_attr' => static function (string $val): array { // add a 'data-display-target' for address fields - if (substr($val, 0, 8) === 'address_') { + if (substr($val, 0, 7) === 'address' || 'center' === $val || 'household' === $val) { return ['data-display-target' => 'address_date']; } @@ -111,17 +107,15 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou } }, ])], + 'data' => array_values($choices), ]); // add a date field for addresses - $builder->add('address_date', DateType::class, [ - 'label' => 'Address valid at this date', - 'data' => new DateTime(), - 'attr' => ['class' => 'datepicker'], - 'widget' => 'single_text', - 'format' => 'dd-MM-yyyy', - 'required' => false, - 'block_name' => 'list_export_form_address_date', + $builder->add('address_date', ChillDateType::class, [ + 'label' => 'Data valid at this date', + 'help' => 'Data regarding center, addresses, and so on will be computed at this date', + 'data' => new DateTimeImmutable(), + 'input' => 'datetime_immutable', ]); } @@ -142,113 +136,35 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou public function getLabels($key, array $values, $data) { - switch ($key) { - case 'birthdate': - // for birthdate, we have to transform the string into a date - // to format the date correctly. - return static function ($value) { - if ('_header' === $value) { - return 'birthdate'; - } - - if (empty($value)) { - return ''; - } - - $date = DateTime::createFromFormat('Y-m-d', $value); - // check that the creation could occurs. - if (false === $date) { - throw new Exception(sprintf('The value %s could ' - . 'not be converted to %s', $value, DateTime::class)); - } - - return $date->format('d-m-Y'); - }; - - case 'gender': - // for gender, we have to translate men/women statement - return function ($value) { - if ('_header' === $value) { - return 'gender'; - } - - return $this->translator->trans($value); - }; - - case 'countryOfBirth': - case 'nationality': - $countryRepository = $this->entityManager - ->getRepository(\Chill\MainBundle\Entity\Country::class); - - // load all countries in a single query - $countryRepository->findBy(['countryCode' => $values]); - - return function ($value) use ($key, $countryRepository) { - if ('_header' === $value) { - return strtolower($key); - } - - if (null === $value) { - return $this->translator->trans('no data'); - } - - $country = $countryRepository->find($value); - - return $this->translatableStringHelper->localize( - $country->getName() - ); - }; - - case 'address_country_name': - return function ($value) use ($key) { - if ('_header' === $value) { - return strtolower($key); - } - - if (null === $value) { - return ''; - } - - return $this->translatableStringHelper->localize(json_decode($value, true)); - }; - - case 'address_isnoaddress': - return static function (?string $value): string { - if ('_header' === $value) { - return 'address.address_homeless'; - } - - if (null !== $value) { - return 'X'; - } - - return ''; - }; - - default: - // for fields which are associated with person - if (in_array($key, $this->fields, true)) { - return static function ($value) use ($key) { - if ('_header' === $value) { - return strtolower($key); - } - - return $value; - }; - } - - return $this->getLabelForCustomField($key, $values, $data); + if (in_array($key, $this->listPersonHelper->getAllPossibleFields(), true)) { + return $this->listPersonHelper->getLabels($key, $values, $data); } + + return $this->getLabelForCustomField($key, $values, $data); } public function getQueryKeys($data) { $fields = []; - foreach ($data['fields'] as $key) { - if (in_array($key, $this->fields, true)) { - $fields[] = $key; + foreach (ListPersonHelper::FIELDS as $key) { + if (!in_array($key, $data['fields'], true)) { + continue; } + + if (substr($key, 0, strlen('address_fields')) === 'address_fields') { + $fields = array_merge($fields, $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')); + + continue; + } + + if ('lifecycleUpdate' === $key) { + $fields = array_merge($fields, ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']); + + continue; + } + + $fields[] = $key; } // add the key from slugs and return @@ -270,6 +186,9 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou 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 function ($el) { @@ -284,40 +203,24 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou $qb = $this->entityManager->createQueryBuilder(); - foreach ($this->fields as $f) { - if (in_array($f, $data['fields'], true)) { - switch ($f) { - case 'countryOfBirth': - case 'nationality': - $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f)); + $qb + ->from(Person::class, 'person') + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . Person\PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)' + ) + ) + ->setParameter('authorized_centers', $centers); - break; + $fields = $data['fields']; - case 'address_street_address_1': - case 'address_street_address_2': - case 'address_valid_from': - case 'address_postcode_label': - case 'address_postcode_code': - case 'address_country_name': - case 'address_country_code': - case 'address_isnoaddress': - $qb->addSelect(sprintf( - 'GET_PERSON_ADDRESS_%s(person.id, :address_date) AS %s', - // get the part after address_ - strtoupper(substr($f, 8)), - $f - )); - $qb->setParameter('address_date', $data['address_date']); - - break; - - default: - $qb->addSelect(sprintf('person.%s as %s', $f, $f)); - } - } - } + $this->listPersonHelper->addSelect($qb, $fields, $data['address_date']); foreach ($this->getCustomFields() as $cf) { + if (!in_array($cf->getSlug(), $fields, true)) { + continue; + } + $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); if ($cfType instanceof CustomFieldChoice && $cfType->isMultiple($cf)) { @@ -345,12 +248,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou } } - $qb - ->from('ChillPersonBundle:Person', 'person') - ->join('person.center', 'center') - ->andWhere('center IN (:authorized_centers)') - ->setParameter('authorized_centers', $centers); - return $qb; } @@ -368,14 +265,14 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou { // get the field starting with address_ $addressFields = array_filter( - $this->fields, + ListPersonHelper::FIELDS, static fn (string $el): bool => substr($el, 0, 8) === 'address_' ); // check if there is one field starting with address in data if (count(array_intersect($data['fields'], $addressFields)) > 0) { // if a field address is checked, the date must not be empty - if (empty($data['address_date'])) { + if (!$data['address_date'] instanceof DateTimeImmutable) { $context ->buildViolation('You must set this date if an address is checked') ->atPath('address_date') @@ -456,7 +353,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou . ' | ' . $label; } - if ('_other' === $slugChoice && $cfType->isChecked($cf, $choiceSlug, $decoded)) { + if ('_other' === $slugChoice && $cfType->isChecked($cf, $slugChoice, $decoded)) { return $cfType->extractOtherValue($cf, $decoded); } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php new file mode 100644 index 000000000..bab67fb39 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php @@ -0,0 +1,220 @@ +addressHelper = $addressHelper; + $this->listPersonHelper = $listPersonHelper; + $this->entityManager = $em; + } + + public function buildForm(FormBuilderInterface $builder) + { + $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); + + // Add a checkbox to select fields + $builder->add('fields', ChoiceType::class, [ + 'multiple' => true, + 'expanded' => true, + 'choices' => $choices, + 'label' => 'Fields to include in export', + 'choice_attr' => static function (string $val): array { + // add a 'data-display-target' for address fields + if (substr($val, 0, 7) === 'address' || 'center' === $val || 'household' === $val) { + return ['data-display-target' => 'address_date']; + } + + return []; + }, + 'constraints' => [new Callback([ + 'callback' => static function ($selected, ExecutionContextInterface $context) { + if (count($selected) === 0) { + $context->buildViolation('You must select at least one element') + ->atPath('fields') + ->addViolation(); + } + }, + ])], + 'data' => array_values($choices), + ]); + + // add a date field for addresses + $builder->add('address_date', ChillDateType::class, [ + 'label' => 'Data valid at this date', + 'help' => 'Data regarding center, addresses, and so on will be computed at this date', + 'data' => new DateTimeImmutable(), + 'input' => 'datetime_immutable', + ]); + } + + 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, according to various filters.'; + } + + public function getGroup(): string + { + return 'Exports of persons'; + } + + public function getLabels($key, array $values, $data) + { + return $this->listPersonHelper->getLabels($key, $values, $data); + } + + public function getQueryKeys($data) + { + $fields = []; + + foreach (ListPersonHelper::FIELDS as $key) { + if (!in_array($key, $data['fields'], true)) { + continue; + } + + if (substr($key, 0, strlen('address_fields')) === 'address_fields') { + $fields = array_merge($fields, $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')); + + continue; + } + + if ('lifecycleUpdate' === $key) { + $fields = array_merge($fields, ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']); + + continue; + } + + $fields[] = $key; + } + + return $fields; + } + + 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'; + } + + 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 function ($el) { + return $el['center']; + }, $acl); + + // throw an error if any fields are present + if (!array_key_exists('fields', $data)) { + throw new \Doctrine\DBAL\Exception\InvalidArgumentException('any fields ' + . 'have been checked'); + } + + $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); + + $fields = $data['fields']; + + $this->listPersonHelper->addSelect($qb, $fields, $data['address_date']); + + return $qb; + } + + public function requiredRole(): string + { + return PersonVoter::LISTS; + } + + public function supportsModifiers() + { + return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE]; + } + + public function validateForm($data, ExecutionContextInterface $context) + { + // get the field starting with address_ + $addressFields = array_filter( + ListPersonHelper::FIELDS, + static fn (string $el): bool => substr($el, 0, 8) === 'address_' + ); + + // check if there is one field starting with address in data + if (count(array_intersect($data['fields'], $addressFields)) > 0) { + // if a field address is checked, the date must not be empty + if (!$data['address_date'] instanceof DateTimeImmutable) { + $context + ->buildViolation('You must set this date if an address is checked') + ->atPath('address_date') + ->addViolation(); + } + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorFilter.php new file mode 100644 index 000000000..b13947e60 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorFilter.php @@ -0,0 +1,66 @@ +getAllAliases(), true)) { + $qb->join('acp.createdBy', 'acp_creator'); + } + + $qb + ->andWhere($qb->expr()->in('acp_creator', ':creators')) + ->setParameter('creators', $data['accepted_creators']); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder + ->add('accepted_creators', PickUserDynamicType::class, [ + 'multiple' => true, + 'label' => false, + ]); + } + + public function describeAction($data, $format = 'string'): array + { + return [ + 'Filtered by creator: only %creators%', [ + '%creators%' => implode(', ', array_map(static fn (User $u) => $u->getLabel(), $data['accepted_creators'])), + ], ]; + } + + public function getTitle(): string + { + return 'Filter by creator'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorJobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorJobFilter.php new file mode 100644 index 000000000..62cf49d6d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorJobFilter.php @@ -0,0 +1,94 @@ +translatableStringHelper = $translatableStringHelper; + $this->userJobRepository = $userJobRepository; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('acp_creator', $qb->getAllAliases(), true)) { + $qb->join('acp.createdBy', 'acp_creator'); + } + + $qb + ->andWhere($qb->expr()->in('acp_creator.userJob', ':creator_job')) + ->setParameter('creator_job', $data['creator_job']); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('creator_job', EntityType::class, [ + 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), + 'choice_label' => function (UserJob $j) { + return $this->translatableStringHelper->localize( + $j->getLabel() + ); + }, + 'multiple' => true, + 'expanded' => true, + 'label' => 'Job', + ]); + } + + public function describeAction($data, $format = 'string'): array + { + $creatorJobs = []; + + foreach ($data['creator_job'] as $j) { + $creatorJobs[] = $this->translatableStringHelper->localize( + $j->getLabel() + ); + } + + return ['export.filter.course.creator_job.Filtered by creator job: only %jobs%', [ + '%jobs%' => implode(', ', $creatorJobs), + ]]; + } + + public function getTitle(): string + { + return 'Filter by creator job'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php index a1347ca99..ee420e63e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php @@ -13,8 +13,10 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\GeographicalUnit; +use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\ChillDateType; +use Chill\MainBundle\Repository\GeographicalUnitLayerRepositoryInterface; use Chill\MainBundle\Repository\GeographicalUnitRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; @@ -23,7 +25,7 @@ use Chill\PersonBundle\Export\Declarations; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; /** @@ -33,6 +35,8 @@ class GeographicalUnitStatFilter implements FilterInterface { private EntityManagerInterface $em; + private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository; + private GeographicalUnitRepositoryInterface $geographicalUnitRepository; private TranslatableStringHelperInterface $translatableStringHelper; @@ -40,10 +44,12 @@ class GeographicalUnitStatFilter implements FilterInterface public function __construct( EntityManagerInterface $em, GeographicalUnitRepositoryInterface $geographicalUnitRepository, + GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository, TranslatableStringHelperInterface $translatableStringHelper ) { $this->em = $em; $this->geographicalUnitRepository = $geographicalUnitRepository; + $this->geographicalUnitLayerRepository = $geographicalUnitLayerRepository; $this->translatableStringHelper = $translatableStringHelper; } @@ -62,7 +68,7 @@ class GeographicalUnitStatFilter implements FilterInterface WITH IDENTITY(acp_geog_filter_location_history.personLocation) = IDENTITY(acp_geog_filter_address_person_location.person) LEFT JOIN ' . Address::class . ' acp_geog_filter_address WITH COALESCE(IDENTITY(acp_geog_filter_address_person_location.address), IDENTITY(acp_geog_filter_location_history.addressLocation)) = acp_geog_filter_address.id - LEFT JOIN ' . GeographicalUnit::class . ' acp_geog_filter_units WITH ST_CONTAINS(acp_geog_units.geom, acp_geog_filter_address.point) = TRUE + LEFT JOIN ' . GeographicalUnit::class . ' acp_geog_filter_units WITH ST_CONTAINS(acp_geog_filter_units.geom, acp_geog_filter_address.point) = TRUE WHERE (acp_geog_filter_location_history.startDate <= :acp_geog_filter_date AND ( acp_geog_filter_location_history.endDate IS NULL OR acp_geog_filter_location_history.endDate < :acp_geog_filter_date @@ -78,7 +84,7 @@ class GeographicalUnitStatFilter implements FilterInterface $qb ->andWhere($qb->expr()->exists($subQueryDql)) ->setParameter('acp_geog_filter_date', $data['date_calc']) - ->setParameter('acp_geog_filter_units', $data['units']); + ->setParameter('acp_geog_filter_units', array_map(static fn (SimpleGeographicalUnitDTO $unitDTO) => $unitDTO->id, $data['units'])); } public function applyOn(): string @@ -95,13 +101,13 @@ class GeographicalUnitStatFilter implements FilterInterface 'data' => new DateTimeImmutable('today'), 'input' => 'datetime_immutable', ]) - ->add('units', EntityType::class, [ + ->add('units', ChoiceType::class, [ 'label' => 'Geographical unit', 'placeholder' => 'Select a geographical unit', - 'class' => GeographicalUnit::class, 'choices' => $this->geographicalUnitRepository->findAll(), - 'choice_label' => function (GeographicalUnit $item) { - return $this->translatableStringHelper->localize($item->getLayer()->getName()) . ' > ' . $item->getUnitName(); + 'choice_value' => static fn (SimpleGeographicalUnitDTO $item) => $item->id, + 'choice_label' => function (SimpleGeographicalUnitDTO $item) { + return $this->translatableStringHelper->localize($this->geographicalUnitLayerRepository->find($item->layerId)->getName()) . ' > ' . $item->unitName; }, 'attr' => [ 'class' => 'select2', @@ -117,8 +123,8 @@ class GeographicalUnitStatFilter implements FilterInterface '%units' => implode( ', ', array_map( - function (GeographicalUnit $item) { - return $this->translatableStringHelper->localize($item->getLayer()->getName()) . ' > ' . $item->getUnitName(); + function (SimpleGeographicalUnitDTO $item) { + return $this->translatableStringHelper->localize($this->geographicalUnitLayerRepository->find($item->layerId)->getName()) . ' > ' . $item->unitName; }, $data['units'] ) diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoActionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoActionFilter.php new file mode 100644 index 000000000..54c28d027 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoActionFilter.php @@ -0,0 +1,51 @@ +andWhere('NOT EXISTS (SELECT 1 FROM ' . AccompanyingPeriodWork::class . ' work WHERE work.accompanyingPeriod = acp)'); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + // no form + } + + public function describeAction($data, $format = 'string'): array + { + return ['Filtered acp which has no actions']; + } + + public function getTitle(): string + { + return 'Filter by which has no action'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoReferrerFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoReferrerFilter.php new file mode 100644 index 000000000..303c789af --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoReferrerFilter.php @@ -0,0 +1,73 @@ +andWhere(' + NOT EXISTS ( + SELECT 1 FROM ' . UserHistory::class . ' uh + WHERE uh.startDate <= :has_no_referrer_filter_date + AND ( + uh.endDate IS NULL + or uh.endDate > :has_no_referrer_filter_date + ) + AND uh.accompanyingPeriod = acp + ) + ') + ->setParameter('has_no_referrer_filter_date', $data['calc_date'], Types::DATE_IMMUTABLE); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder + ->add('calc_date', ChillDateType::class, [ + 'label' => 'Has no referrer on this date', + 'data' => new DateTimeImmutable(), + 'input' => 'datetime_immutable', + ]); + } + + public function describeAction($data, $format = 'string'): array + { + return ['Filtered acp which has no referrer on date: %date%', [ + '%date%' => $data['calc_date']->format('d-m-Y'), + ]]; + } + + public function getTitle(): string + { + return 'Filter by which has no referrer'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasTemporaryLocationFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasTemporaryLocationFilter.php new file mode 100644 index 000000000..2ae1ad2a0 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasTemporaryLocationFilter.php @@ -0,0 +1,105 @@ +join('acp.locationHistories', 'acp_having_temporarily_location') + ->andWhere('acp_having_temporarily_location.startDate <= :acp_having_temporarily_location_date + AND (acp_having_temporarily_location.endDate IS NULL OR acp_having_temporarily_location.endDate > :acp_having_temporarily_location_date)') + ->setParameter('acp_having_temporarily_location_date', $data['calc_date']); + + switch ($data['having_temporarily']) { + case true: + $qb->andWhere('acp_having_temporarily_location.addressLocation IS NOT NULL'); + + break; + + case false: + $qb->andWhere('acp_having_temporarily_location.personLocation IS NOT NULL'); + + break; + + default: + throw new LogicException('value not supported'); + } + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder + ->add('having_temporarily', ChoiceType::class, [ + 'choices' => [ + 'export.filter.course.having_temporarily.Having a temporarily location' => true, + 'export.filter.course.having_temporarily.Having a person\'s location' => false, + ], + 'choice_label' => static function ($choice) { + switch ($choice) { + case true: + return 'export.filter.course.having_temporarily.Having a temporarily location'; + + case false: + return 'export.filter.course.having_temporarily.Having a person\'s location'; + + default: + throw new LogicException('this choice is not supported'); + } + }, + ]) + ->add('calc_date', ChillDateType::class, [ + 'label' => 'export.filter.course.having_temporarily.Calculation date', + 'input' => 'datetime_immutable', + 'data' => new DateTimeImmutable(), + ]); + } + + public function describeAction($data, $format = 'string'): array + { + switch ($data['having_temporarily']) { + case true: + return ['export.filter.course.having_temporarily.Having a temporarily location', []]; + + case false: + return ['export.filter.course.having_temporarily.Having a person\'s location', []]; + + default: + throw new LogicException('value not supported'); + } + } + + public function getTitle(): string + { + return 'Filter by temporary location'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php index 5b8335c55..07ca1de75 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php @@ -12,15 +12,23 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Export\FilterInterface; -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\Export\Declarations; -use DateTime; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; class OpenBetweenDatesFilter implements FilterInterface { + private RollingDateConverterInterface $rollingDateConverter; + + public function __construct(RollingDateConverterInterface $rollingDateConverter) + { + $this->rollingDateConverter = $rollingDateConverter; + } + public function addRole(): ?string { return null; @@ -34,8 +42,8 @@ class OpenBetweenDatesFilter implements FilterInterface ); $qb->andWhere($clause); - $qb->setParameter('datefrom', $data['date_from'], Types::DATE_MUTABLE); - $qb->setParameter('dateto', $data['date_to'], Types::DATE_MUTABLE); + $qb->setParameter('datefrom', $this->rollingDateConverter->convert($data['date_from']), Types::DATE_IMMUTABLE); + $qb->setParameter('dateto', $this->rollingDateConverter->convert($data['date_to']), Types::DATE_IMMUTABLE); } public function applyOn(): string @@ -46,19 +54,19 @@ class OpenBetweenDatesFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { $builder - ->add('date_from', ChillDateType::class, [ - 'data' => new DateTime(), + ->add('date_from', PickRollingDateType::class, [ + 'data' => new RollingDate(RollingDate::T_MONTH_PREVIOUS_START), ]) - ->add('date_to', ChillDateType::class, [ - 'data' => new DateTime(), + ->add('date_to', PickRollingDateType::class, [ + 'data' => new RollingDate(RollingDate::T_TODAY), ]); } public function describeAction($data, $format = 'string'): array { return ['Filtered by opening dates: between %datefrom% and %dateto%', [ - '%datefrom%' => $data['date_from']->format('d-m-Y'), - '%dateto%' => $data['date_to']->format('d-m-Y'), + '%datefrom%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'), + '%dateto%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php index 141a1a2db..917e129bf 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Export\Declarations; use Chill\PersonBundle\Form\Type\PickSocialIssueType; @@ -31,15 +30,11 @@ class SocialIssueFilter implements FilterInterface private SocialIssueRender $socialIssueRender; - private TranslatableStringHelper $translatableStringHelper; - public function __construct( TranslatorInterface $translator, - TranslatableStringHelper $translatableStringHelper, SocialIssueRender $socialIssueRender ) { $this->translator = $translator; - $this->translatableStringHelper = $translatableStringHelper; $this->socialIssueRender = $socialIssueRender; } @@ -59,7 +54,7 @@ class SocialIssueFilter implements FilterInterface $qb->andWhere($clause) ->setParameter( 'socialissues', - SocialIssue::getDescendantsWithThisForIssues($data['accepted_socialissues']) + SocialIssue::getDescendantsWithThisForIssues($data['accepted_socialissues']->toArray()) ); } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilter.php index 2eb1b90b3..0c9b28e8c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilter.php @@ -12,31 +12,40 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Form\Type\PickRollingDateType; +use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Export\Declarations; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use function in_array; class StepFilter implements FilterInterface { + private const A = 'acp_filter_bystep_stephistories'; + private const DEFAULT_CHOICE = AccompanyingPeriod::STEP_CONFIRMED; + private const P = 'acp_step_filter_date'; + private const STEPS = [ 'Draft' => AccompanyingPeriod::STEP_DRAFT, 'Confirmed' => AccompanyingPeriod::STEP_CONFIRMED, 'Closed' => AccompanyingPeriod::STEP_CLOSED, ]; + private RollingDateConverterInterface $rollingDateConverter; + /** * @var TranslatorInterface */ - protected $translator; + private $translator; - public function __construct(TranslatorInterface $translator) + public function __construct(RollingDateConverterInterface $rollingDateConverter, TranslatorInterface $translator) { + $this->rollingDateConverter = $rollingDateConverter; $this->translator = $translator; } @@ -47,17 +56,25 @@ class StepFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { - $where = $qb->getDQLPart('where'); - $clause = $qb->expr()->eq('acp.step', ':step'); - - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); + if (!in_array(self::A, $qb->getAllAliases(), true)) { + $qb->leftJoin('acp.stepHistories', self::A); } - $qb->add('where', $where); - $qb->setParameter('step', $data['accepted_steps']); + $qb + ->andWhere( + $qb->expr()->andX( + $qb->expr()->lte(self::A . '.startDate', ':' . self::P), + $qb->expr()->orX( + $qb->expr()->isNull(self::A . '.endDate'), + $qb->expr()->lt(self::A . '.endDate', ':' . self::P) + ) + ) + ) + ->andWhere( + $qb->expr()->in(self::A . '.step', ':acp_filter_by_step_steps') + ) + ->setParameter(self::P, $this->rollingDateConverter->convert($data['calc_date'])) + ->setParameter('acp_filter_by_step_steps', $data['accepted_steps']); } public function applyOn() @@ -67,13 +84,17 @@ class StepFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('accepted_steps', ChoiceType::class, [ - 'choices' => self::STEPS, - 'multiple' => false, - 'expanded' => true, - 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, - ]); + $builder + ->add('accepted_steps', ChoiceType::class, [ + 'choices' => self::STEPS, + 'multiple' => false, + 'expanded' => true, + 'empty_data' => self::DEFAULT_CHOICE, + 'data' => self::DEFAULT_CHOICE, + ]) + ->add('calc_date', PickRollingDateType::class, [ + 'label' => 'export.acp.filter.by_step.date_calc', + ]); } public function describeAction($data, $format = 'string') diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByEndDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByEndDateFilter.php new file mode 100644 index 000000000..f828da109 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByEndDateFilter.php @@ -0,0 +1,69 @@ +andWhere('workeval.endDate BETWEEN :work_eval_by_end_date_start_date and :work_eval_by_end_date_end_date') + ->setParameter('work_eval_by_end_date_start_date', $data['start_date'], Types::DATE_IMMUTABLE) + ->setParameter('work_eval_by_end_date_end_date', $data['end_date'], Types::DATE_IMMUTABLE); + } + + public function applyOn(): string + { + return Declarations::EVAL_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + $builder + ->add('start_date', ChillDateType::class, [ + 'label' => 'start period date', + 'data' => new DateTimeImmutable('1 year ago'), + 'input' => 'datetime_immutable', + ]) + ->add('end_date', ChillDateType::class, [ + 'label' => 'end period date', + 'data' => new DateTimeImmutable(), + 'input' => 'datetime_immutable', + ]); + } + + public function describeAction($data, $format = 'string'): array + { + return ['Filtered by end date: between %start_date% and %end_date%', [ + '%start_date%' => $data['start_date']->format('d-m-Y'), + '%end_date%' => $data['end_date']->format('d-m-Y'), + ]]; + } + + public function getTitle(): string + { + return 'Filter by end date evaluations'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByStartDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByStartDateFilter.php new file mode 100644 index 000000000..f12b5e805 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByStartDateFilter.php @@ -0,0 +1,69 @@ +andWhere('workeval.startDate BETWEEN :work_eval_by_start_date_start_date and :work_eval_by_start_date_end_date') + ->setParameter('work_eval_by_start_date_start_date', $data['start_date'], Types::DATE_IMMUTABLE) + ->setParameter('work_eval_by_start_date_end_date', $data['end_date'], Types::DATE_IMMUTABLE); + } + + public function applyOn(): string + { + return Declarations::EVAL_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + $builder + ->add('start_date', ChillDateType::class, [ + 'label' => 'start period date', + 'data' => new DateTimeImmutable('1 year ago'), + 'input' => 'datetime_immutable', + ]) + ->add('end_date', ChillDateType::class, [ + 'label' => 'end period date', + 'data' => new DateTimeImmutable(), + 'input' => 'datetime_immutable', + ]); + } + + public function describeAction($data, $format = 'string'): array + { + return ['Filtered by start date: between %start_date% and %end_date%', [ + '%start_date%' => $data['start_date']->format('d-m-Y'), + '%end_date%' => $data['end_date']->format('d-m-Y'), + ]]; + } + + public function getTitle(): string + { + return 'Filter by start date evaluations'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/CurrentEvaluationsFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/CurrentEvaluationsFilter.php new file mode 100644 index 000000000..140f6c3cb --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/CurrentEvaluationsFilter.php @@ -0,0 +1,50 @@ +andWhere('workeval.endDate IS NULL'); + } + + public function applyOn(): string + { + return Declarations::EVAL_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + //no form needed + } + + public function describeAction($data, $format = 'string'): array + { + return ['Filtered by current evaluations']; + } + + public function getTitle(): string + { + return 'Filter by current evaluations'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php index 96a9f5a1c..03d9ab568 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php @@ -12,27 +12,33 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter\PersonFilters; use Chill\MainBundle\Entity\GeographicalUnit; +use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO; use Chill\MainBundle\Form\Type\ChillDateType; +use Chill\MainBundle\Repository\GeographicalUnitLayerRepositoryInterface; use Chill\MainBundle\Repository\GeographicalUnitRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; use Chill\PersonBundle\Export\Declarations; use DateTimeImmutable; use Doctrine\ORM\QueryBuilder; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; class GeographicalUnitFilter implements \Chill\MainBundle\Export\FilterInterface { + private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository; + private GeographicalUnitRepositoryInterface $geographicalUnitRepository; private TranslatableStringHelperInterface $translatableStringHelper; public function __construct( GeographicalUnitRepositoryInterface $geographicalUnitRepository, + GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository, TranslatableStringHelperInterface $translatableStringHelper ) { $this->geographicalUnitRepository = $geographicalUnitRepository; + $this->geographicalUnitLayerRepository = $geographicalUnitLayerRepository; $this->translatableStringHelper = $translatableStringHelper; } @@ -65,7 +71,7 @@ class GeographicalUnitFilter implements \Chill\MainBundle\Export\FilterInterface $qb->expr()->exists($subQuery) ) ->setParameter('person_filter_geog_date', $data['date_calc']) - ->setParameter('person_filter_geog_units', $data['units']); + ->setParameter('person_filter_geog_units', array_map(static fn (SimpleGeographicalUnitDTO $unitDTO) => $unitDTO->id, $data['units'])); } public function applyOn() @@ -82,13 +88,13 @@ class GeographicalUnitFilter implements \Chill\MainBundle\Export\FilterInterface 'data' => new DateTimeImmutable('today'), 'input' => 'datetime_immutable', ]) - ->add('units', EntityType::class, [ + ->add('units', ChoiceType::class, [ 'label' => 'Geographical unit', 'placeholder' => 'Select a geographical unit', - 'class' => GeographicalUnit::class, 'choices' => $this->geographicalUnitRepository->findAll(), - 'choice_label' => function (GeographicalUnit $item) { - return $this->translatableStringHelper->localize($item->getLayer()->getName()) . ' > ' . $item->getUnitName(); + 'choice_value' => static fn (SimpleGeographicalUnitDTO $item) => $item->id, + 'choice_label' => function (SimpleGeographicalUnitDTO $item) { + return $this->translatableStringHelper->localize($this->geographicalUnitLayerRepository->find($item->layerId)->getName()) . ' > ' . $item->unitName; }, 'attr' => [ 'class' => 'select2', @@ -106,10 +112,10 @@ class GeographicalUnitFilter implements \Chill\MainBundle\Export\FilterInterface 'units' => implode( ', ', array_map( - function (GeographicalUnit $item) { - return $this->translatableStringHelper->localize($item->getLayer()->getName()) . ' > ' . $item->getUnitName(); + function (SimpleGeographicalUnitDTO $item) { + return $this->translatableStringHelper->localize($this->geographicalUnitLayerRepository->find($item->layerId)->getName()) . ' > ' . $item->unitName; }, - $data['units']->toArray() + $data['units'] ) ), ], diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CurrentActionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CurrentActionFilter.php new file mode 100644 index 000000000..cbdb64d8d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CurrentActionFilter.php @@ -0,0 +1,50 @@ +andWhere('acpw.endDate IS NULL'); + } + + public function applyOn(): string + { + return Declarations::SOCIAL_WORK_ACTION_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + //no form + } + + public function describeAction($data, $format = 'string'): array + { + return ['Filtered by current action']; + } + + public function getTitle(): string + { + return 'Filter by current actions'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php new file mode 100644 index 000000000..de8870674 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php @@ -0,0 +1,441 @@ +addressHelper = $addressHelper; + $this->centerRepository = $centerRepository; + $this->civilityRepository = $civilityRepository; + $this->countryRepository = $countryRepository; + $this->languageRepository = $languageRepository; + $this->maritalStatusRepository = $maritalStatusRepository; + $this->translatableStringHelper = $translatableStringHelper; + $this->translator = $translator; + $this->userRepository = $userRepository; + } + + /** + * @param array|value-of[] $fields + */ + public function addSelect(QueryBuilder $qb, array $fields, DateTimeImmutable $computedDate): void + { + foreach (ListPersonHelper::FIELDS as $f) { + if (!in_array($f, $fields, true)) { + continue; + } + + switch ($f) { + case 'countryOfBirth': + case 'nationality': + $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f)); + + break; + + case 'address_fields': + $this->addCurrentAddressAt($qb, $computedDate); + $qb->leftJoin('personHouseholdAddress.address', 'personAddress'); + $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'personAddress', 'address_fields'); + + break; + + case '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; + + case 'household_id': + $qb + ->addSelect('IDENTITY(personHouseholdAddress.household) AS household_id'); + + $this->addCurrentAddressAt($qb, $computedDate); + + break; + + case 'center': + $qb + ->addSelect('IDENTITY(centerHistory.center) AS center') + ->leftJoin('person.centerHistory', 'centerHistory') + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('centerHistory'), + $qb->expr()->andX( + $qb->expr()->lte('centerHistory.startDate', ':address_date'), + $qb->expr()->orX( + $qb->expr()->isNull('centerHistory.endDate'), + $qb->expr()->gte('centerHistory.endDate', ':address_date') + ) + ) + ) + ) + ->setParameter('address_date', $computedDate); + + break; + + case 'lifecycleUpdate': + $qb + ->addSelect('person.createdAt AS createdAt') + ->addSelect('IDENTITY(person.createdBy) AS createdBy') + ->addSelect('person.updatedAt AS updatedAt') + ->addSelect('IDENTITY(person.updatedBy) AS updatedBy'); + + break; + + case 'genderComment': + $qb->addSelect('person.genderComment.comment AS genderComment'); + + break; + + case 'maritalStatus': + $qb->addSelect('IDENTITY(person.maritalStatus) AS maritalStatus'); + + break; + + case 'maritalStatusComment': + $qb->addSelect('person.maritalStatusComment.comment AS maritalStatusComment'); + + break; + + case 'civility': + $qb->addSelect('IDENTITY(person.civility) AS civility'); + + break; + + default: + $qb->addSelect(sprintf('person.%s as %s', $f, $f)); + } + } + } + + /** + * @return array|string[] + */ + public function getAllPossibleFields(): array + { + return array_merge( + self::FIELDS, + ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'], + $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') + ); + } + + public function getLabels($key, array $values, $data): callable + { + if (substr($key, 0, strlen('address_fields')) === 'address_fields') { + return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); + } + + switch ($key) { + case 'center': + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + if (null === $value || null === $center = $this->centerRepository->find($value)) { + return ''; + } + + return $center->getName(); + }; + + case 'birthdate': + case 'deathdate': + case 'maritalStatusDate': + case 'createdAt': + case 'updatedAt': + // for birthdate, we have to transform the string into a date + // to format the date correctly. + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + if (null === $value) { + return ''; + } + + // warning: won't work with DateTimeImmutable as we reset time a few lines later + $date = DateTime::createFromFormat('Y-m-d', $value); + $hasTime = false; + + if (false === $date) { + $date = DateTime::createFromFormat('Y-m-d H:i:s', $value); + $hasTime = true; + } + + // check that the creation could occurs. + if (false === $date) { + throw new Exception(sprintf('The value %s could ' + . 'not be converted to %s', $value, DateTime::class)); + } + + if (!$hasTime) { + $date->setTime(0, 0, 0); + } + + return $date; + }; + + case 'createdBy': + case 'updatedBy': + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + if (null === $value) { + return ''; + } + + return $this->userRepository->find($value)->getLabel(); + }; + + case 'civility': + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + if (null === $value) { + return ''; + } + + $civility = $this->civilityRepository->find($value); + + if (null === $civility) { + return ''; + } + + return $this->translatableStringHelper->localize($civility->getName()); + }; + + case 'gender': + // for gender, we have to translate men/women statement + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + return $this->translator->trans($value); + }; + + case 'maritalStatus': + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + if (null === $value) { + return ''; + } + + $maritalStatus = $this->maritalStatusRepository->find($value); + + return $this->translatableStringHelper->localize($maritalStatus->getName()); + }; + + case 'spokenLanguages': + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + if (null === $value) { + return ''; + } + + $ids = json_decode($value); + + return + implode( + '|', + array_map(function ($id) { + if (null === $id) { + return ''; + } + + $lang = $this->languageRepository->find($id); + + if (null === $lang) { + return null; + } + + return $this->translatableStringHelper->localize($lang->getName()); + }, $ids) + ); + }; + + case 'countryOfBirth': + case 'nationality': + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + if (null === $value) { + return ''; + } + + $country = $this->countryRepository->find($value); + + return $this->translatableStringHelper->localize( + $country->getName() + ); + }; + + default: + if (!in_array($key, self::getAllPossibleFields(), true)) { + throw new RuntimeException("this key is not supported by this helper: {$key}"); + } + + // for fields which are associated with person + return function ($value) use ($key) { + if ('_header' === $value) { + return $this->translator->trans($key); + } + + return $value; + }; + } + } + + private function addCurrentAddressAt(QueryBuilder $qb, DateTimeImmutable $date): void + { + if (!(in_array('personHouseholdAddress', $qb->getAllAliases(), true))) { + $qb + ->leftJoin('person.householdAddresses', 'personHouseholdAddress') + ->andWhere( + $qb->expr()->orX( + // no address at this time + $qb->expr()->isNull('personHouseholdAddress'), + // there is one address... + $qb->expr()->andX( + $qb->expr()->lte('personHouseholdAddress.validFrom', ':address_date'), + $qb->expr()->orX( + $qb->expr()->isNull('personHouseholdAddress.validTo'), + $qb->expr()->gt('personHouseholdAddress.validTo', ':address_date') + ) + ) + ) + ) + ->setParameter('address_date', $date); + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/MaritalStatusRepository.php b/src/Bundle/ChillPersonBundle/Repository/MaritalStatusRepository.php index 1df23fa31..8697f9346 100644 --- a/src/Bundle/ChillPersonBundle/Repository/MaritalStatusRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/MaritalStatusRepository.php @@ -14,9 +14,8 @@ namespace Chill\PersonBundle\Repository; use Chill\PersonBundle\Entity\MaritalStatus; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; -class MaritalStatusRepository implements ObjectRepository +class MaritalStatusRepository implements MaritalStatusRepositoryInterface { private EntityRepository $repository; diff --git a/src/Bundle/ChillPersonBundle/Repository/MaritalStatusRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/MaritalStatusRepositoryInterface.php new file mode 100644 index 000000000..1f51060e9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/MaritalStatusRepositoryInterface.php @@ -0,0 +1,27 @@ +person->id; }, + static function ($participation) { + return $participation->person->id; + }, $data->participations ); diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateTest.php index 8b2f4c141..bb70866e2 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateTest.php @@ -294,23 +294,49 @@ final class PersonControllerUpdateTest extends WebTestCase public function validTextFieldsProvider() { return [ - ['firstName', 'random Value', static function (Person $person) { return $person->getFirstName(); }], - ['lastName', 'random Value', static function (Person $person) { return $person->getLastName(); }], + ['firstName', 'random Value', static function (Person $person) { + return $person->getFirstName(); + }], + ['lastName', 'random Value', static function (Person $person) { + return $person->getLastName(); + }], // reminder: this value is capitalized - ['placeOfBirth', 'A PLACE', static function (Person $person) { return $person->getPlaceOfBirth(); }], - ['birthdate', '1980-12-15', static function (Person $person) { return $person->getBirthdate()->format('Y-m-d'); }], + ['placeOfBirth', 'A PLACE', static function (Person $person) { + return $person->getPlaceOfBirth(); + }], + ['birthdate', '1980-12-15', static function (Person $person) { + return $person->getBirthdate()->format('Y-m-d'); + }], // TODO test on phonenumber update // ['phonenumber', '+32123456789', static function (Person $person) { return $person->getPhonenumber(); }], - ['memo', 'jfkdlmq jkfldmsq jkmfdsq', static function (Person $person) { return $person->getMemo(); }], - ['countryOfBirth', 'BE', static function (Person $person) { return $person->getCountryOfBirth()->getCountryCode(); }], - ['nationality', 'FR', static function (Person $person) { return $person->getNationality()->getCountryCode(); }], - ['placeOfBirth', '', static function (Person $person) { return $person->getPlaceOfBirth(); }], - ['birthdate', '', static function (Person $person) { return $person->getBirthdate(); }], + ['memo', 'jfkdlmq jkfldmsq jkmfdsq', static function (Person $person) { + return $person->getMemo(); + }], + ['countryOfBirth', 'BE', static function (Person $person) { + return $person->getCountryOfBirth()->getCountryCode(); + }], + ['nationality', 'FR', static function (Person $person) { + return $person->getNationality()->getCountryCode(); + }], + ['placeOfBirth', '', static function (Person $person) { + return $person->getPlaceOfBirth(); + }], + ['birthdate', '', static function (Person $person) { + return $person->getBirthdate(); + }], //['phonenumber', '', static function (Person $person) { return $person->getPhonenumber(); }], - ['memo', '', static function (Person $person) { return $person->getMemo(); }], - ['countryOfBirth', null, static function (Person $person) { return $person->getCountryOfBirth(); }], - ['nationality', null, static function (Person $person) { return $person->getNationality(); }], - ['gender', Person::FEMALE_GENDER, static function (Person $person) { return $person->getGender(); }], + ['memo', '', static function (Person $person) { + return $person->getMemo(); + }], + ['countryOfBirth', null, static function (Person $person) { + return $person->getCountryOfBirth(); + }], + ['nationality', null, static function (Person $person) { + return $person->getNationality(); + }], + ['gender', Person::FEMALE_GENDER, static function (Person $person) { + return $person->getGender(); + }], ]; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php index 7423b5388..403f89dd4 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateWithHiddenFieldsTest.php @@ -197,12 +197,24 @@ final class PersonControllerUpdateWithHiddenFieldsTest extends WebTestCase public function validTextFieldsProvider() { return [ - ['firstName', 'random Value', static function (Person $person) { return $person->getFirstName(); }], - ['lastName', 'random Value', static function (Person $person) { return $person->getLastName(); }], - ['birthdate', '15-12-1980', static function (Person $person) { return $person->getBirthdate()->format('d-m-Y'); }], - ['memo', 'jfkdlmq jkfldmsq jkmfdsq', static function (Person $person) { return $person->getMemo(); }], - ['birthdate', '', static function (Person $person) { return $person->getBirthdate(); }], - ['gender', Person::FEMALE_GENDER, static function (Person $person) { return $person->getGender(); }], + ['firstName', 'random Value', static function (Person $person) { + return $person->getFirstName(); + }], + ['lastName', 'random Value', static function (Person $person) { + return $person->getLastName(); + }], + ['birthdate', '15-12-1980', static function (Person $person) { + return $person->getBirthdate()->format('d-m-Y'); + }], + ['memo', 'jfkdlmq jkfldmsq jkmfdsq', static function (Person $person) { + return $person->getMemo(); + }], + ['birthdate', '', static function (Person $person) { + return $person->getBirthdate(); + }], + ['gender', Person::FEMALE_GENDER, static function (Person $person) { + return $person->getGender(); + }], ]; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Household/MembersEditorTest.php b/src/Bundle/ChillPersonBundle/Tests/Household/MembersEditorTest.php index 9c0e884a5..a58560bdd 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Household/MembersEditorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Household/MembersEditorTest.php @@ -111,7 +111,9 @@ final class MembersEditorTest extends TestCase $this->assertCount(1, $notSharing); $this->assertCount(1, $sharings); - $getPerson = static function (HouseholdMember $m) { return $m->getPerson(); }; + $getPerson = static function (HouseholdMember $m) { + return $m->getPerson(); + }; $this->assertContains($person, $notSharing->map($getPerson)); } @@ -151,7 +153,9 @@ final class MembersEditorTest extends TestCase $this->assertCount(1, $notSharing); $this->assertCount(0, $sharings); - $getPerson = static function (HouseholdMember $m) { return $m->getPerson(); }; + $getPerson = static function (HouseholdMember $m) { + return $m->getPerson(); + }; $this->assertContains($person, $notSharing->map($getPerson)); } diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/RelationshipDocGenNormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/RelationshipDocGenNormalizerTest.php index a6bab133a..05d2eace1 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/RelationshipDocGenNormalizerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/RelationshipDocGenNormalizerTest.php @@ -118,7 +118,9 @@ final class RelationshipDocGenNormalizerTest extends TestCase { $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class); $translatableStringHelper->localize(Argument::type('array'))->will( - static function ($args) { return $args[0][array_keys($args[0])[0]]; } + static function ($args) { + return $args[0][array_keys($args[0])[0]]; + } ); $normalizer = new RelationshipDocGenNormalizer( diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml index 04d5d8371..71eecf4ff 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml @@ -103,6 +103,32 @@ services: tags: - { name: chill.export_filter, alias: accompanyingcourse_openbetweendates_filter } + chill.person.export.filter_has_temporary_location: + class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\HasTemporaryLocationFilter + tags: + - { name: chill.export_filter, alias: accompanyingcourse_has_temporary_location_filter } + + chill.person.export.filter_has_no_referrer: + class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\HasNoReferrerFilter + tags: + - { name: chill.export_filter, alias: accompanyingcourse_has_no_referrer_filter } + + chill.person.export.filter_has_no_action: + class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\HasNoActionFilter + tags: + - { name: chill.export_filter, alias: accompanyingcourse_has_no_action_filter } + + chill.person.export.filter_creator: + class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\CreatorFilter + tags: + - { name: chill.export_filter, alias: accompanyingcourse_creator_filter } + + chill.person.export.filter_creator_job: + class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\CreatorJobFilter + tags: + - { name: chill.export_filter, alias: accompanyingcourse_creator_job_filter } + + ## Aggregators chill.person.export.aggregator_referrer_scope: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ScopeAggregator @@ -191,3 +217,11 @@ services: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ByHouseholdCompositionAggregator: tags: - { name: chill.export_aggregator, alias: accompanyingcourse_by_household_compo_aggregator } + + Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ByActionNumberAggregator: + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_by_action_number_aggregator } + + Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\CreatorJobAggregator: + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_creator_job_aggregator } diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_evaluation.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_evaluation.yaml index e21e0b6c1..f58b7112b 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_evaluation.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_evaluation.yaml @@ -23,6 +23,31 @@ services: tags: - { name: chill.export_filter, alias: accompanyingcourse_maxdate_filter } + Chill\PersonBundle\Export\Filter\EvaluationFilters\ByStartDateFilter: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: evaluation_bystartdate_filter } + + Chill\PersonBundle\Export\Filter\EvaluationFilters\ByEndDateFilter: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: evaluation_byenddate_filter } + + Chill\PersonBundle\Export\Filter\EvaluationFilters\MaxDateFilter: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: evaluation_bymaxdate_filter } + + Chill\PersonBundle\Export\Filter\EvaluationFilters\CurrentEvaluationsFilter: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: evaluation_currentevaluations_filter } + + ## Aggregators chill.person.export.aggregator_evaluationtype: class: Chill\PersonBundle\Export\Aggregator\EvaluationAggregators\EvaluationTypeAggregator @@ -30,4 +55,28 @@ services: autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_evaluationtype_aggregator } - \ No newline at end of file + + Chill\PersonBundle\Export\Aggregator\EvaluationAggregators\ByStartDateAggregator: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: evaluation_bystartdate_aggregator } + + Chill\PersonBundle\Export\Aggregator\EvaluationAggregators\ByEndDateAggregator: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: evaluation_byenddate_aggregator } + + Chill\PersonBundle\Export\Aggregator\EvaluationAggregators\ByMaxDateAggregator: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: evaluation_bymaxdate_aggregator } + + Chill\PersonBundle\Export\Aggregator\EvaluationAggregators\HavingEndDateAggregator: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: evaluation_byend_date_aggregator } + diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index ffffaf175..b2b182c5b 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -15,13 +15,24 @@ services: tags: - { name: chill.export, alias: count_person_with_accompanying_course } - chill.person.export.list_person: - class: Chill\PersonBundle\Export\Export\ListPerson + Chill\PersonBundle\Export\Export\ListPerson: autowire: true autoconfigure: true tags: - { name: chill.export, alias: list_person } + Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriod: + autowire: true + autoconfigure: true + tags: + - { name: chill.export, alias: list_person_with_acp } + + Chill\PersonBundle\Export\Export\ListAccompanyingPeriod: + autowire: true + autoconfigure: true + tags: + - { name: chill.export, alias: list_acp } + chill.person.export.list_person.duplicate: class: Chill\PersonBundle\Export\Export\ListPersonDuplicate arguments: diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml index 450899659..13c51762f 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml @@ -37,6 +37,12 @@ services: tags: - { name: chill.export_filter, alias: social_work_actions_treatingagent_filter } + Chill\PersonBundle\Export\Filter\SocialWorkFilters\CurrentActionFilter: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: social_work_actions_current_filter } + ## AGGREGATORS chill.person.export.aggregator_action_type: class: Chill\PersonBundle\Export\Aggregator\SocialWorkAggregators\ActionTypeAggregator @@ -86,3 +92,9 @@ services: autoconfigure: true tags: - { name: chill.export_aggregator, alias: social_work_actions_goal_result_aggregator } + + Chill\PersonBundle\Export\Aggregator\SocialWorkAggregators\CurrentActionAggregator: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: social_work_actions_current_aggregator } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index f3fd6c430..bdedea895 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -89,6 +89,16 @@ Any person selected: Aucune personne sélectionnée Create a household and add an address: Ajouter une adresse pour une personne non suivie et seule dans un ménage A new household will be created. The person will be member of this household.: Un nouveau ménage va être créé. La personne sera membre de ce ménage. Comment on the gender: Commentaire sur le genre +genderComment: Commentaire sur le genre +maritalStatus: État civil +maritalStatusComment: Commentaire sur l'état civil +maritalStatusDate: Date de l'état civil +memo: Commentaire +numberOfChildren: Nombre d'enfants +contactInfo: Commentaire des contacts +spokenLanguages: Langues parlées + + # dédoublonnage Old person: Doublon @@ -339,6 +349,8 @@ List peoples: Liste des personnes Create a list of people according to various filters.: Crée une liste des personnes selon différents filtres. Fields to include in export: Champs à inclure dans l'export Address valid at this date: Addresse valide à cette date +Data valid at this date: Données valides à cette date +Data regarding center, addresses, and so on will be computed at this date: Les données concernant le centre, l'adresse, le ménage, sera calculé à cette date. List duplicates: Liste des doublons Create a list of duplicate people: Créer la liste des personnes détectées comme doublons. Count people participating in an accompanying course: Nombre de personnes concernées par un parcours @@ -552,6 +564,30 @@ Date from: Date de début Date to: Date de fin "Filtered by opening dates: between %datefrom% and %dateto%": "Filtrer les parcours ouverts entre deux dates: entre le %datefrom% et le %dateto%" +Filter by temporary location: Filtrer les parcours avec une localisation temporaire +Filter by which has no referrer: Filtrer les parcours sans référent +"Filtered acp which has no referrer on date: %date%": "Filtré les parcours sans référent à cette date: %date%" +Has no referrer on this date: N'a pas de référent à cette date +Filter by which has no action: Filtrer les parcours qui n’ont pas d’actions +Filtered acp which has no actions: 'Filtré: uniquement les parcours qui n''ont pas d''actions' +Group by number of actions: Grouper les parcours par nombre d’actions +Filter by creator: Filtrer les parcours par créateur +'Filtered by creator: only %creators%': 'Filtré par créateur: uniquement %creators%' +Filter by creator job: Filtrer les parcours par métier du créateur +'Filtered by creator job: only %jobs%': 'Filtré par métier du créateur: uniquement %jobs%' +Group by creator job: Grouper les parcours par métier du créateur + +Filter by current actions: Filtrer les actions en cours +Filtered by current action: 'Filtré: uniquement les actions en cours (sans date de fin)' +Filter by start date evaluations: Filtrer les évaluations par date de début +Filter by end date evaluations: Filtrer les évaluations par date de fin +start period date: Date de début de la période +end period date: Date de fin de la période +"Filtered by start date: between %start_date% and %end_date%": "Filtré par la date de début: comprise entre %start_date% et %end_date%" +"Filtered by end date: between %start_date% and %end_date%": "Filtré par la date de fin: comprise entre %start_date% et %end_date%" +Filter by current evaluations: Filtrer les évaluations en cours +"Filtered by current evaluations": "Filtré: uniquement les évaluations en cours" + ## social actions filters/aggr Filter by treating agent scope: Filtrer les actions par service de l'agent traitant "Filtered by treating agent scope: only %scopes%": "Filtré par service de l'agent traitant: uniquement %scopes%" @@ -589,6 +625,7 @@ maxdate is specified: la date d'échéance est spécifiée maxdate is not specified: la date d'échéance n'est pas spécifiée "Filtered by maxdate: only %choice%": "Filtré par date d'échéance: uniquement si %choice%" + ## household filters/aggr Filter by composition: Filtrer les ménages par composition familiale Accepted composition: Composition familiale @@ -716,6 +753,7 @@ socialAction: defaultNotificationDelay: Délai de notification par défaut socialIssue: Problématique sociale +household_id: Identifiant du ménage household: allowHolder: Peut être titulaire shareHousehold: Membre du ménage @@ -968,12 +1006,86 @@ export: Household composition: Composition du ménage Group course by household composition: Grouper les parcours par composition familiale des ménages des usagers concernés Calc date: Date de calcul de la composition du ménage + by_number_of_action: + Number of actions: Nombre d'actions + by_creator_job: + Creator's job: Métier du créateur + course_work: + by_current_action: + Current action ?: Action en cours ? + Group by current actions: Grouper les actions en cours + Current action: Action en cours + Not current action: Action terminée + eval: + by_end_date: + Has end date ?: Évaluation en cours ? + Group evaluations having end date: Grouper les évaluations en cours (avec ou sans date de fin) + enddate is specified: la date de fin est spécifiée + enddate is not specified: la date de fin n'est pas spécifiée + Group by end date evaluations: Grouper les évaluations par semaine/mois/année de la date de fin + End date period: Fin (par periode) + by_start_date_period: + Start date period: Début (par periode) + Group by start date evaluations: Grouper les évaluations par semaine/mois/année de la date de début + by_max_date: + Group by max date evaluations: Grouper les évaluations par semaine/mois/année de la date d'échéance + Max date: Date d'échéance + filter: course: by_user_scope: Computation date for referrer: Date à laquelle le référent était actif by_referrer: Computation date for referrer: Date à laquelle le référent était actif + having_temporarily: + Having a temporarily location: Ayant une localisation temporaire + Having a person's location: Ayant une localisation auprès d'un usager + Calculation date: Date de la localisation + creator_job: + 'Filtered by creator job: only %jobs%': 'Filtré par métier du créateur: seulement %jobs%' + list: + person_with_acp: + List peoples having an accompanying period: Liste des personnes ayant un parcours d'accompagnement + Create a list of people having an accompaying periods, according to various filters.: Génère une liste des personnes ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager + acp: + List of accompanying periods: Liste de périodes d'accompagnements + Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des périodes 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 + id: Identifiant du parcours + openingDate: Date d'ouverture du parcours + closingDate: Date de fermeture du parcours + confidential: Confidentiel + emergency: Urgent + intensity: Intensité + createdAt: Créé le + updatedAt: Dernière mise à jour le + acpOrigin: Origine du parcours + acpClosingMotive: Motif de fermeture + acpJob: Métier du parcours + createdBy: Créé par + updatedBy: Dernière modification par + administrativeLocation: Location administrative + step: Etape + stepSince: Dernière modification de l'étape + referrer: Référent + referrerSince: Référent depuis le + locationIsPerson: Parcours localisé auprès d'un usager concerné + locationIsTemp: Parcours avec une localisation temporaire + 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 ? + isRequestorThirdParty: Le demandeur est-il un tiers ? + requestorPersonId: Identifiant du demandeur personne + requestorThirdPartyId: Identifiant du tiers + acprequestorPerson: Nom du demandeur personne + acprequestorThirdPaty: Nom du demandeur tiers + scopes: Services + socialIssues: Problématiques sociales + + + social_action: and children: et dérivés diff --git a/src/Bundle/ChillReportBundle/Tests/Timeline/TimelineProviderTest.php b/src/Bundle/ChillReportBundle/Tests/Timeline/TimelineProviderTest.php index e76688357..6091c51a8 100644 --- a/src/Bundle/ChillReportBundle/Tests/Timeline/TimelineProviderTest.php +++ b/src/Bundle/ChillReportBundle/Tests/Timeline/TimelineProviderTest.php @@ -67,7 +67,9 @@ final class TimelineProviderTest extends WebTestCase self::$em ->getRepository(\Chill\MainBundle\Entity\Scope::class) ->findAll(), - static function (Scope $scope) { return $scope->getName()['en'] === 'social'; } + static function (Scope $scope) { + return $scope->getName()['en'] === 'social'; + } ); $report = (new Report())