diff --git a/.changes/unreleased/Feature-20250130-120459.yaml b/.changes/unreleased/Feature-20250130-120459.yaml new file mode 100644 index 000000000..0e348a9e6 --- /dev/null +++ b/.changes/unreleased/Feature-20250130-120459.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Add possibility to export a csv with all social issues and social actions +time: 2025-01-30T12:04:59.754998541+01:00 +custom: + Issue: "343" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index b7ce359e1..6c52d962f 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -735,6 +735,25 @@ export: steps: Escaliers _lat: Latitude _lon: Longitude + social_action_list: + id: Identifiant de l'action + social_issue_id: Identifiant de la problématique sociale + social_issue: Problématique sociale + social_issue_ordering: Ordre de la problématique sociale + action_label: Action d'accompagnement + action_ordering: Ordre + goal_label: Objectif + goal_id: Identifiant de l'objectif + goal_result_label: Résultat + goal_result_id: Identifiant du résultat + result_without_goal_label: Résultat (sans objectif) + result_without_goal_id: Identifiant du résultat (sans objectif) + evaluation_title: Évaluation + evaluation_id: Identifiant de l'évaluation + evaluation_url: Adresse URL externe (évaluation) + evaluation_delay_month: Délai de notification (mois) + evaluation_delay_week: Délai de notification (semaine) + evaluation_delay_day: Délai de notification (jours) rolling_date: year_previous_start: Début de l'année précédente diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialActionCSVExportController.php b/src/Bundle/ChillPersonBundle/Controller/SocialActionCSVExportController.php new file mode 100644 index 000000000..95217b391 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/SocialActionCSVExportController.php @@ -0,0 +1,66 @@ + 'csv'])] + public function socialActionList(Request $request, string $_format = 'csv'): StreamedResponse + { + if (!$this->security->isGranted('ROLE_ADMIN')) { + throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list'); + } + + $actions = $this->socialActionRepository->findAllOrdered(); + + $csv = $this->socialActionCSVExportService->generateCsv($actions); + + return new StreamedResponse( + function () use ($csv) { + foreach ($csv->chunk(1024) as $chunk) { + echo $chunk; + flush(); + } + }, + Response::HTTP_OK, + [ + 'Content-Encoding' => 'none', + 'Content-Type' => 'text/csv; charset=UTF-8', + 'Content-Disposition' => 'attachment; filename=results.csv', + ] + ); + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialIssueCSVExportController.php b/src/Bundle/ChillPersonBundle/Controller/SocialIssueCSVExportController.php new file mode 100644 index 000000000..8f966e366 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/SocialIssueCSVExportController.php @@ -0,0 +1,66 @@ + 'csv'])] + public function socialIssueList(Request $request, string $_format = 'csv'): StreamedResponse + { + if (!$this->security->isGranted('ROLE_ADMIN')) { + throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list'); + } + + $socialIssues = $this->socialIssueRepository->findAllOrdered(); + + $csv = $this->socialIssueCSVExportService->generateCsv($socialIssues); + + return new StreamedResponse( + function () use ($csv) { + foreach ($csv->chunk(1024) as $chunk) { + echo $chunk; + flush(); + } + }, + Response::HTTP_OK, + [ + 'Content-Encoding' => 'none', + 'Content-Type' => 'text/csv; charset=UTF-8', + 'Content-Disposition' => 'attachment; users.csv', + ] + ); + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWorkExportController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWorkExportController.php deleted file mode 100644 index b70010362..000000000 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWorkExportController.php +++ /dev/null @@ -1,149 +0,0 @@ - 'csv'])] - public function socialIssueList(Request $request, string $_format = 'csv'): StreamedResponse - { - if (!$this->security->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list'); - } - - $socialIssues = $this->socialIssueRepository->findAll(); - - $socialIssues = array_map(fn ($issue) => [ - 'id' => $issue->getId(), - 'title' => $this->socialIssueRender->renderString($issue, []), - 'ordering' => $issue->getOrdering(), - 'desactivationDate' => $issue->getDesactivationDate(), - ], $socialIssues); - - $csv = Writer::createFromPath('php://temp', 'r+'); - $csv->insertOne( - array_map( - fn (string $e) => $this->translator->trans($e), - [ - 'Id', - 'Title', - 'Ordering', - 'goal.desactivationDate', - ] - ) - ); - - $csv->addFormatter(fn (array $row) => null !== ($row['desactivationDate'] ?? null) ? array_merge($row, ['desactivationDate' => $row['desactivationDate']->format('Y-m-d')]) : $row); - $csv->insertAll($socialIssues); - - return new StreamedResponse( - function () use ($csv) { - foreach ($csv->chunk(1024) as $chunk) { - echo $chunk; - flush(); - } - }, - Response::HTTP_OK, - [ - 'Content-Encoding' => 'none', - 'Content-Type' => 'text/csv; charset=UTF-8', - 'Content-Disposition' => 'attachment; users.csv', - ] - ); - } - - /** - * @throws UnavailableStream - * @throws CannotInsertRecord - * @throws Exception - */ - #[Route(path: '/{_locale}/admin/social-work/social-action/export/list.{_format}', name: 'chill_person_social_action_export_list', requirements: ['_format' => 'csv'])] - public function socialActionList(Request $request, string $_format = 'csv'): StreamedResponse - { - if (!$this->security->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list'); - } - - $socialActions = $this->socialActionRepository->findAll(); - - $socialActions = array_map(fn ($action) => [ - 'id' => $action->getId(), - 'title' => $this->socialActionRender->renderString($action, []), - 'desactivationDate' => $action->getDesactivationDate(), - 'socialIssue' => $this->socialIssueRender->renderString($action->getIssue(), []), - 'ordering' => $action->getOrdering(), - ], $socialActions); - - $csv = Writer::createFromPath('php://temp', 'r+'); - $csv->insertOne( - array_map( - fn (string $e) => $this->translator->trans($e), - [ - 'Id', - 'Title', - 'goal.desactivationDate', - 'Social issue', - 'Ordering', - ] - ) - ); - - $csv->addFormatter(fn (array $row) => null !== ($row['desactivationDate'] ?? null) ? array_merge($row, ['desactivationDate' => $row['desactivationDate']->format('Y-m-d')]) : $row); - $csv->insertAll($socialActions); - - return new StreamedResponse( - function () use ($csv) { - foreach ($csv->chunk(1024) as $chunk) { - echo $chunk; - flush(); - } - }, - Response::HTTP_OK, - [ - 'Content-Encoding' => 'none', - 'Content-Type' => 'text/csv; charset=UTF-8', - 'Content-Disposition' => 'attachment; users.csv', - ] - ); - } -} diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php index fe530e84b..4d37cb3d4 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialActionRepository.php @@ -44,6 +44,14 @@ final readonly class SocialActionRepository implements ObjectRepository return $this->repository->findAll(); } + public function findAllOrdered(): array + { + return $this->repository->createQueryBuilder('si') + ->orderBy('si.ordering', 'ASC') + ->getQuery() + ->getResult(); + } + /** * @return array|SocialAction[] */ diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php index c9bb3a189..40ec35855 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php @@ -39,6 +39,14 @@ final readonly class SocialIssueRepository implements ObjectRepository return $this->repository->findAll(); } + public function findAllOrdered(): array + { + return $this->repository->createQueryBuilder('si') + ->orderBy('si.ordering', 'ASC') + ->getQuery() + ->getResult(); + } + /** * @return array|SocialIssue[] */ diff --git a/src/Bundle/ChillPersonBundle/Service/SocialWork/SocialActionCSVExportService.php b/src/Bundle/ChillPersonBundle/Service/SocialWork/SocialActionCSVExportService.php new file mode 100644 index 000000000..adff4a588 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Service/SocialWork/SocialActionCSVExportService.php @@ -0,0 +1,104 @@ + $actions + */ + public function generateCsv(array $actions): Writer + { + // CSV headers + $headers = array_map( + fn (string $tr) => $this->translator->trans('export.social_action_list.'.$tr), + array_keys($this->formatRow(new SocialAction())) + ); + + $csv = Writer::createFromPath('php://temp', 'w+'); + $csv->insertOne($headers); + + foreach ($actions as $action) { + if ($action->getGoals()->isEmpty() && $action->getResults()->isEmpty() && $action->getEvaluations()->isEmpty()) { + $csv->insertOne($this->formatRow($action)); + } + + foreach ($action->getGoals() as $goal) { + if ($goal->getResults()->isEmpty()) { + $csv->insertOne($this->formatRow($action, $goal)); + } + + foreach ($goal->getResults() as $goalResult) { + $csv->insertOne($this->formatRow($action, $goal, $goalResult)); + } + } + + foreach ($action->getResults() as $result) { + if ($result->getGoals()->isEmpty()) { + $csv->insertOne($this->formatRow($action, null, null, $result)); + } + } + + foreach ($action->getEvaluations() as $evaluation) { + $csv->insertOne($this->formatRow($action, evaluation: $evaluation)); + } + } + + return $csv; + } + + private function formatRow( + SocialAction $action, + ?Goal $goal = null, + ?Result $goalResult = null, + ?Result $resultWithoutGoal = null, + ?Evaluation $evaluation = null, + ): array { + return [ + 'action_id' => $action->getId(), + 'social_issue_id' => $action->getIssue()?->getId(), + 'problematique_label' => null !== $action->getIssue() ? $this->socialIssueRender->renderString($action->getIssue(), []) : null, + 'social_issue_ordering' => null !== $action->getIssue() ? $action->getIssue()->getOrdering() : null, + 'action_label' => $this->socialActionRender->renderString($action, []), + 'action_ordering' => $action->getOrdering(), + 'goal_label' => null !== $goal ? $this->stringHelper->localize($goal->getTitle()) : null, + 'goal_id' => $goal?->getId(), + 'goal_result_label' => null !== $goalResult ? $this->stringHelper->localize($goalResult->getTitle()) : null, + 'goal_result_id' => $goalResult?->getId(), + 'result_without_goal_label' => null !== $resultWithoutGoal ? $this->stringHelper->localize($resultWithoutGoal->getTitle()) : null, + 'result_without_goal_id' => $resultWithoutGoal?->getId(), + 'evaluation_title' => null !== $evaluation ? $this->stringHelper->localize($evaluation->getTitle()) : null, + 'evaluation_id' => $evaluation?->getId(), + 'evaluation_url' => $evaluation?->getUrl(), + 'evaluation_delay_month' => $evaluation?->getDelay()?->format('%m'), + 'evaluation_delay_week' => $evaluation?->getDelay()?->format('%w'), + 'evaluation_delay_day' => $evaluation?->getDelay()?->format('%d'), + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Service/SocialWork/SocialIssueCSVExportService.php b/src/Bundle/ChillPersonBundle/Service/SocialWork/SocialIssueCSVExportService.php new file mode 100644 index 000000000..be4cc0706 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Service/SocialWork/SocialIssueCSVExportService.php @@ -0,0 +1,73 @@ +insertOne( + array_map( + fn (string $e) => $this->translator->trans($e), + [ + 'Id', + 'Label', + 'Social issue', + 'socialIssue.isParent?', + 'socialIssue.Parent id', + ] + ) + ); + + foreach ($issues as $issue) { + $csv->insertOne($this->formatRow($issue)); + } + + + return $csv; + } + + private function formatRow( + SocialIssue $issue, + ): array { + return + [ + 'id' => $issue->getId(), + 'label' => $this->stringHelper->localize($issue->getTitle()), + 'title' => $this->socialIssueRender->renderString($issue, []), + 'isParent' => $issue->hasChildren() ? 'X' : '', + 'parent_id' => null !== $issue->getParent() ? $issue->getParent()->getId() : '', + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php index af8622f67..571d36ed9 100644 --- a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php +++ b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php @@ -19,7 +19,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; /** * @implements ChillEntityRenderInterface */ -final readonly class SocialIssueRender implements ChillEntityRenderInterface +class SocialIssueRender implements ChillEntityRenderInterface { public const AND_CHILDREN_MENTION = 'show_and_children_mention'; @@ -37,7 +37,7 @@ final readonly class SocialIssueRender implements ChillEntityRenderInterface */ public const SHOW_AND_CHILDREN = 'show_and_children'; - public function __construct(private TranslatableStringHelper $translatableStringHelper, private \Twig\Environment $engine, private TranslatorInterface $translator) {} + public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {} public function renderBox($socialIssue, array $options): string { diff --git a/src/Bundle/ChillPersonBundle/Tests/Exporters/SocialActionCSVExporterTest.php b/src/Bundle/ChillPersonBundle/Tests/Exporters/SocialActionCSVExporterTest.php new file mode 100644 index 000000000..d450c9690 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Exporters/SocialActionCSVExporterTest.php @@ -0,0 +1,102 @@ +createMock(TranslatorInterface::class); + $translator->method('trans')->willReturnArgument(0); + $socialIssueRender = $this->createMock(SocialIssueRender::class); + $socialIssueRender->method('renderString')->willReturnCallback(static fn (SocialIssue $socialIssue) => $socialIssue->getTitle()['fr'] ?? ''); + $socialActionRender = $this->createMock(SocialActionRender::class); + $socialActionRender->method('renderString')->willReturnCallback(static fn (SocialAction $socialAction) => $socialAction->getTitle()['fr'] ?? ''); + $stringHelper = $this->createMock(TranslatableStringHelperInterface::class); + $stringHelper->method('localize') + ->willReturnCallback(static fn (array $messages) => $messages['fr'] ?? 'not found'); + + $exporter = new SocialActionCSVExportService($socialIssueRender, $socialActionRender, $stringHelper, $translator); + + // Mock social issue + + // Création d'une instance réelle de SocialIssue + $socialIssue = new SocialIssue(); + $socialIssue->setTitle(['fr' => 'Issue Title']); // Exemple de définition d'une propriété + + // Création d'une instance réelle de SocialAction sans objectifs ni résultats + $actionWithoutGoalsOrResults = new SocialAction(); + $actionWithoutGoalsOrResults->setIssue($socialIssue); + $actionWithoutGoalsOrResults->setTitle(['fr' => 'Action without goals or results']); + + // Création d'une instance réelle de SocialAction avec des objectifs et des résultats + $goalWithResult = new Goal(); + $resultWithAction = new Result(); + $goalWithResult->addResult($resultWithAction); + + $actionWithGoalsAndResults = new SocialAction(); + $actionWithGoalsAndResults->setIssue($socialIssue); + $actionWithGoalsAndResults->setTitle(['fr' => 'Action with goals and results']); + $actionWithGoalsAndResults->addGoal($goalWithResult); + + // Création d'une instance réelle de SocialAction avec des objectifs mais sans résultats + $goalWithoutResult = new Goal(); + $actionWithGoalsNoResults = new SocialAction(); + $actionWithGoalsNoResults->setIssue($socialIssue); + $actionWithGoalsNoResults->setTitle(['fr' => 'Action with goals and no results']); + $actionWithGoalsNoResults->addGoal($goalWithoutResult); + + // Création d'une instance réelle de SocialAction avec des résultats mais sans objectifs + $resultWithNoAction = new Result(); + $resultWithNoAction->setTitle(['fr' => 'Result without objectives']); + $actionWithResultsNoGoals = new SocialAction(); + $actionWithResultsNoGoals->setIssue($socialIssue); + $actionWithResultsNoGoals->setTitle(['fr' => 'Action with results and no goals']); + $actionWithResultsNoGoals->addResult($resultWithNoAction); + + // generate + $csv = $exporter->generateCsv([$actionWithGoalsAndResults, $actionWithoutGoalsOrResults, + $actionWithGoalsNoResults, $actionWithResultsNoGoals]); + $content = $csv->toString(); + + // Assert CSV contains expected values + $this->assertStringContainsString('Action without goals or results', $content); + $this->assertStringContainsString('Action with goals and results', $content); + $this->assertStringContainsString('Action with goals and no results', $content); + $this->assertStringContainsString('Action with results and no goals', $content); + + self::assertEquals(<<<'CSV' + export.social_action_list.action_id,export.social_action_list.social_issue_id,export.social_action_list.problematique_label,export.social_action_list.social_issue_ordering,export.social_action_list.action_label,export.social_action_list.action_ordering,export.social_action_list.goal_label,export.social_action_list.goal_id,export.social_action_list.goal_result_label,export.social_action_list.goal_result_id,export.social_action_list.result_without_goal_label,export.social_action_list.result_without_goal_id,export.social_action_list.evaluation_title,export.social_action_list.evaluation_id,export.social_action_list.evaluation_url,export.social_action_list.evaluation_delay_month,export.social_action_list.evaluation_delay_week,export.social_action_list.evaluation_delay_day + ,,"Issue Title",0,"Action with goals and results",0,"not found",,"not found",,,,,,,,, + ,,"Issue Title",0,"Action without goals or results",0,,,,,,,,,,,, + ,,"Issue Title",0,"Action with goals and no results",0,"not found",,,,,,,,,,, + ,,"Issue Title",0,"Action with results and no goals",0,,,,,"Result without objectives",,,,,,, + + CSV, $content); + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index efa30c803..d8374433f 100644 --- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml @@ -71,6 +71,9 @@ services: Chill\PersonBundle\Controller\PersonSignatureController: tags: [ 'controller.service_arguments' ] - Chill\PersonBundle\Controller\SocialWorkExportController: + Chill\PersonBundle\Controller\SocialIssueCSVExportController: + tags: [ 'controller.service_arguments' ] + + Chill\PersonBundle\Controller\SocialActionCSVExportController: tags: [ 'controller.service_arguments' ] diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 96a2b15bd..204d5a982 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -755,6 +755,11 @@ socialAction: defaultNotificationDelay: Délai de notification par défaut socialIssue: Problématique sociale +socialIssue: + isParent?: Parent? + Parent id: Identifiant du parent + + household_id: Identifiant du ménage household: allowHolder: Peut être titulaire