From 56ec8fb516437ea84b01d8c4a56cc790f764ea21 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 6 Aug 2025 09:05:39 +0200 Subject: [PATCH 01/60] Remove 'to_validate' as default for task filter --- src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php index 152184570..debf0f1be 100644 --- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php @@ -624,8 +624,7 @@ final class SingleTaskController extends AbstractController ->addCheckbox('status', $statuses, $statuses, $statusTrans); $states = $this->singleTaskStateRepository->findAllExistingStates(); - $checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['in_progress', 'closed', 'canceled', 'validated'], true))); - + $checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['to_validate', 'in_progress', 'closed', 'canceled', 'validated'], true))); if ([] !== $states) { $filterBuilder ->addCheckbox('states', $states, $checked); From 2309636eaed4d19ea29db2a276cc15aebd887227 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 6 Aug 2025 13:47:29 +0200 Subject: [PATCH 02/60] - **fix:** adjust display logic for accompanying period dates, include closing date if period is closed. --- .changes/unreleased/Fixed-20250806-134609.yaml | 6 ++++++ .../views/Person/list_with_period.html.twig | 13 ++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 .changes/unreleased/Fixed-20250806-134609.yaml diff --git a/.changes/unreleased/Fixed-20250806-134609.yaml b/.changes/unreleased/Fixed-20250806-134609.yaml new file mode 100644 index 000000000..f3c8c07c3 --- /dev/null +++ b/.changes/unreleased/Fixed-20250806-134609.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: adjust display logic for accompanying period dates, include closing date if period is closed. +time: 2025-08-06T13:46:09.241584292+02:00 +custom: + Issue: "382" + SchemaChange: No schema change diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig index 41bc51864..8de08d617 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig @@ -32,9 +32,16 @@
{% if app != null %} -
- {{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }} -
+ {% if acp.closingDate != null %} + {{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({ + '%opening_date%': acp.openingDate|format_date('long'), + '%closing_date%': acp.closingDate|format_date('long')} + ) }} + {% else %} +
+ {{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }} +
+ {% endif %} {% endif %} {% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) %} From 2f6cef42383fc17f1c40c098f23d32fb7938e7f1 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 6 Aug 2025 14:20:09 +0200 Subject: [PATCH 03/60] - **fix:** move closing motive up to be coherent with display elsewhere --- .../views/Person/list_with_period.html.twig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig index 41bc51864..b8a2e426d 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig @@ -70,6 +70,20 @@
+ {% if acp.step == 'CLOSED' and acp.closingMotive is not null %} +
+
+

{{ 'Closing motive'|trans }}

+
+
+
+ {{ acp.closingMotive.name|localize_translatable_string }} +
+
+
+ {% endif %} + + {% if acp.user is not null %}
From aa085a1562dcc0d2b99cc1c0b3dc7923853cc6be Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 6 Aug 2025 17:34:53 +0200 Subject: [PATCH 04/60] **fix:** add min and step attributes to integer field in DateIntervalType --- .changes/unreleased/Fixed-20250806-173527.yaml | 6 ++++++ src/Bundle/ChillMainBundle/Form/Type/DateIntervalType.php | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 .changes/unreleased/Fixed-20250806-173527.yaml diff --git a/.changes/unreleased/Fixed-20250806-173527.yaml b/.changes/unreleased/Fixed-20250806-173527.yaml new file mode 100644 index 000000000..1b750c12d --- /dev/null +++ b/.changes/unreleased/Fixed-20250806-173527.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: add min and step attributes to integer field in DateIntervalType +time: 2025-08-06T17:35:27.413787704+02:00 +custom: + Issue: "384" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/Form/Type/DateIntervalType.php b/src/Bundle/ChillMainBundle/Form/Type/DateIntervalType.php index c9bc4dd82..b878ffb66 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/DateIntervalType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/DateIntervalType.php @@ -55,6 +55,10 @@ class DateIntervalType extends AbstractType { $builder ->add('n', IntegerType::class, [ + 'attr' => [ + 'min' => 0, + 'step' => 1, + ], 'constraints' => [ new GreaterThan([ 'value' => 0, From f5668592ca5f0deefc399e20c8d1c76748271f66 Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Mon, 18 Aug 2025 15:34:48 +0000 Subject: [PATCH 05/60] Resolve "Fusion des tiers" --- .../public/vuejs/_components/BadgeEntity.vue | 2 + .../views/CRUD/_view_content.html.twig | 17 ++- .../ThirdpartyDuplicateController.php | 125 ++++++++++++++++++ .../Form/ThirdpartyFindDuplicateType.php | 41 ++++++ .../views/Entity/thirdparty.html.twig | 8 +- .../Resources/views/ThirdParty/view.html.twig | 2 +- .../ThirdPartyDuplicate/_details.html.twig | 34 +++++ .../ThirdPartyDuplicate/confirm.html.twig | 97 ++++++++++++++ .../find_duplicate.html.twig | 38 ++++++ .../Service/ThirdpartyMergeService.php | 115 ++++++++++++++++ .../Templating/Entity/ThirdPartyRender.php | 1 + .../Service/ThirdpartyMergeServiceTest.php | 86 ++++++++++++ .../config/services.yaml | 10 +- .../translations/messages.fr.yml | 14 +- 14 files changed, 580 insertions(+), 10 deletions(-) create mode 100644 src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php create mode 100644 src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php create mode 100644 src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig create mode 100644 src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig create mode 100644 src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig create mode 100644 src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php create mode 100644 src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/BadgeEntity.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/BadgeEntity.vue index 1c5d2b6da..f8cd21c55 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/BadgeEntity.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/BadgeEntity.vue @@ -64,3 +64,5 @@ const props = defineProps({ entity: Object, }); + +thirdparty_duplicate: merge: Fussioner find: 'Désigner un tiers doublon' diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig index 203a24926..71755cc11 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_content.html.twig @@ -4,7 +4,7 @@ {% endblock crud_content_header %} {% block crud_content_view %} - + {% block crud_content_view_details %}
id
@@ -20,7 +20,7 @@ {{ 'Cancel'|trans }} - {% endblock %} + {% endblock %} {% block content_view_actions_before %}{% endblock %} {% block content_form_actions_delete %} {% if chill_crud_action_exists(crud_name, 'delete') %} @@ -32,7 +32,7 @@ {% endif %} {% endif %} - {% endblock content_form_actions_delete %} + {% endblock content_form_actions_delete %} {% block content_view_actions_duplicate_link %} {% if chill_crud_action_exists(crud_name, 'new') %} {% if is_granted(chill_crud_config('role', crud_name, 'new'), entity) %} @@ -44,6 +44,17 @@ {% endif %} {% endif %} {% endblock content_view_actions_duplicate_link %} + {% block content_view_actions_merge %} +
  • + + + {{ 'Merge'|trans }} + +
  • + {% endblock %} {% block content_view_actions_edit_link %} {% if chill_crud_action_exists(crud_name, 'edit') %} {% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %} diff --git a/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php new file mode 100644 index 000000000..ab42b5656 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Controller/ThirdpartyDuplicateController.php @@ -0,0 +1,125 @@ +getKind()) { + $suggested = $thirdparty->getParent()->getChildren(); + } + + $form = $this->createForm(ThirdpartyFindDuplicateType::class, null, ['suggested' => $suggested]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $thirdparty2 = $form->get('thirdparty')->getData(); + + $direction = $form->get('direction')->getData(); + + if ('starting' === $direction) { + $params = [ + 'thirdparty1_id' => $thirdparty->getId(), + 'thirdparty2_id' => $thirdparty2->getId(), + ]; + } else { + $params = [ + 'thirdparty1_id' => $thirdparty2->getId(), + 'thirdparty2_id' => $thirdparty->getId(), + ]; + } + + return $this->redirectToRoute('chill_thirdparty_duplicate_confirm', $params); + } + + return $this->render('@ChillThirdParty/ThirdPartyDuplicate/find_duplicate.html.twig', [ + 'thirdparty' => $thirdparty, + 'form' => $form->createView(), + ]); + } + + /** + * @ParamConverter("thirdparty1", options={"id": "thirdparty1_id"}) + * @ParamConverter("thirdparty2", options={"id": "thirdparty2_id"}) + */ + #[Route(path: '/{_locale}/3party/{thirdparty1_id}/duplicate/{thirdparty2_id}/confirm', name: 'chill_thirdparty_duplicate_confirm')] + public function confirmAction(ThirdParty $thirdparty1, ThirdParty $thirdparty2, Request $request) + { + try { + $this->validateThirdpartyMerge($thirdparty1, $thirdparty2); + $form = $this->createForm(PersonConfimDuplicateType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + + $this->thirdPartyMergeService->merge($thirdparty1, $thirdparty2); + + $session = $request->getSession(); + if ($session instanceof Session) { + $session->getFlashBag()->add('success', new TranslatableMessage('thirdparty_duplicate.Merge successful')); + } + + return $this->redirectToRoute('chill_crud_3party_3party_view', ['id' => $thirdparty1->getId()]); + } + + return $this->render('@ChillThirdParty/ThirdPartyDuplicate/confirm.html.twig', [ + 'thirdparty' => $thirdparty1, + 'thirdparty2' => $thirdparty2, + 'form' => $form->createView(), + ]); + } catch (\InvalidArgumentException $e) { + $this->addFlash('error', $this->translator->trans($e->getMessage())); + + return $this->redirectToRoute('chill_thirdparty_find_duplicate', [ + 'thirdparty_id' => $thirdparty1->getId(), + ]); + } + } + + private function validateThirdpartyMerge(ThirdParty $thirdparty1, ThirdParty $thirdparty2): void + { + $constraints = [ + [$thirdparty1 === $thirdparty2, 'thirdparty_duplicate.You cannot merge a thirdparty with itself. Please choose a different thirdparty'], + [$thirdparty1->getKind() !== $thirdparty2->getKind(), 'thirdparty_duplicate.A thirdparty can only be merged with a thirdparty of the same kind'], + [$thirdparty1->getParent() !== $thirdparty2->getParent(), 'thirdparty_duplicate.Two child thirdparties must have the same parent'], + ]; + + foreach ($constraints as [$condition, $message]) { + if ($condition) { + throw new \InvalidArgumentException($message); + } + } + } +} diff --git a/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php new file mode 100644 index 000000000..275b6d21c --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Form/ThirdpartyFindDuplicateType.php @@ -0,0 +1,41 @@ +add('thirdparty', PickThirdpartyDynamicType::class, [ + 'label' => 'Find duplicate', + 'mapped' => false, + 'suggested' => $options['suggested'], + ]) + ->add('direction', HiddenType::class, [ + 'data' => 'starting', + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'suggested' => [], + ]); + } +} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/Entity/thirdparty.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/Entity/thirdparty.html.twig index b3327e24d..8c500c6cc 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/Entity/thirdparty.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/Entity/thirdparty.html.twig @@ -171,7 +171,13 @@ - {% else %} + {% elseif is_granted('CHILL_3PARTY_3PARTY_UPDATE', thirdparty) %} +
  • + +
  • {% endif %} {% if options['customButtons']['after'] is defined %} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/view.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/view.html.twig index bc5fc3325..2bd6f426f 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/view.html.twig +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/view.html.twig @@ -128,7 +128,7 @@
    {% for tp in thirdParty.activeChildren %}
    - {{ tp|chill_entity_render_box({'render': 'bloc', 'addLink': false, 'isConfidential': tp.contactDataAnonymous ? true : false }) }} + {{ tp|chill_entity_render_box({'render': 'bloc', 'addLink': false, 'isConfidential': tp.contactDataAnonymous ? true : false, 'showFusion': true }) }}
    {% endfor %}
    diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig new file mode 100644 index 000000000..4d1f0cece --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/_details.html.twig @@ -0,0 +1,34 @@ +{%- macro details(thirdparty, options) -%} + +
      +
    • {{ 'name'|trans }}: + {{ thirdparty.name }}
    • +
    • {{ 'First name'|trans }}: + {% if thirdparty.firstname %}{{ thirdparty.firstname }}{% endif %}
    • +
    • {{ 'thirdparty.Civility'|trans }}: + {% if thirdparty.getCivility %}{{ thirdparty.getCivility.name|localize_translatable_string }}{% endif %}
    • +
    • {{ 'thirdparty.NameCompany'|trans }}: + {% if thirdparty.nameCompany is not empty %}{{ thirdparty.nameCompany }}{% endif %}
    • +
    • {{ 'thirdparty.Acronym'|trans }}: + {% if thirdparty.acronym %}{{ thirdparty.acronym }}{% endif %}
    • +
    • {{ 'thirdparty.Profession'|trans }}: + {% if thirdparty.profession %}{{ thirdparty.profession }}{% endif %}
    • +
    • {{ 'telephone'|trans }}: + {% if thirdparty.telephone %}{{ thirdparty.telephone|chill_format_phonenumber }}{% endif %}
    • +
    • {{ 'email'|trans }}: + {% if thirdparty.email is not null %}{{ thirdparty.email }}{% endif %}
    • +
    • {{ 'Address'|trans }}: + {%- if thirdparty.getAddress is not empty -%} + {{ thirdparty.getAddress|chill_entity_render_box }} + {% endif %}
    • +
    • {{ 'thirdparty.Contact data are confidential'|trans }}: + {{ thirdparty.contactDataAnonymous }}
    • +
    • {{ 'Contacts'|trans }}: +
        + {% for c in thirdparty.getChildren %} +
      • {{ c.name }} {{ c.firstName }}
      • + {% endfor %} +
      +
    • +
    +{% endmacro %} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig new file mode 100644 index 000000000..23d5507e7 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/confirm.html.twig @@ -0,0 +1,97 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% import '@ChillThirdParty/ThirdPartyDuplicate/_details.html.twig' as details %} + +{% block title %}{{ 'thirdparty_duplicate.Thirdparty duplicate title'|trans ~ ' ' ~ thirdparty.name }}{% endblock %} + +{% block content %} + +
    + +

    {{ 'thirdparty_duplicate.title'|trans }}

    + +
    +

    {{ 'thirdparty_duplicate.Thirdparty to delete'|trans }}: + {{ 'thirdparty_duplicate.Thirdparty to delete explanation'|trans }} +

    +
    + +

    + {{ thirdparty2 }} +

    + +

    {{ 'Deleted datas'|trans ~ ':' }}

    + {{ details.details(thirdparty2) }} + +{#

    {{ 'Moved links'|trans ~ ':' }}

    #} +{# {{ details.links(thirdparty2) }}#} +
    +
    + +
    +

    {{ 'thirdparty_duplicate.Thirdparty to keep'|trans }}: + {{ 'thirdparty_duplicate.Thirdparty to keep explanation'|trans }} +

    +
    + +

    + {{ thirdparty }} +

    + +

    {{ 'thirdparty_duplicate.Data to keep'|trans ~ ':' }}

    + {{ details.details(thirdparty) }} + +{#

    {{ 'thirdparty_duplicate.links to keep'|trans ~ ':' }}

    #} +{# {{ sidepane.links(thirdparty) }}#} +
    +
    + + {{ form_start(form) }} + +
    + +
    +
    + {{ form_widget(form.confirm) }} +
    +
    + {{ form_label(form.confirm) }} +
    +
    +
    + + + + {{ form_end(form) }} + +
    +{% endblock %} diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig new file mode 100644 index 000000000..f658fd728 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdPartyDuplicate/find_duplicate.html.twig @@ -0,0 +1,38 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% set activeRouteKey = 'chill_thirdparty_duplicate' %} + +{% block title %}{{ 'thirdparty_duplicate.find'|trans ~ ' ' ~ thirdparty.name|capitalize }}{% endblock %} + + +{% block content %} +
    + +

    {{ 'thirdparty_duplicate.find'|trans }}

    + + {{ form_start(form) }} + {{ form_rest(form) }} + + + + {{ form_end(form) }} + +
    +{% endblock %} + +{% block js %} + {{ encore_entry_script_tags('mod_pickentity_type') }} +{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('mod_pickentity_type') }} +{% endblock %} diff --git a/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php new file mode 100644 index 000000000..4eecb09b4 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Service/ThirdpartyMergeService.php @@ -0,0 +1,115 @@ +em->getConnection(); + $conn->beginTransaction(); + + try { + $queries = [ + ...$this->updateReferences($toKeep, $toDelete), + ...$this->removeThirdparty($toKeep, $toDelete), + ]; + + foreach ($queries as $query) { + $conn->executeStatement($query['sql'], $query['params']); + } + + $conn->commit(); + } catch (\Exception $e) { + $conn->rollBack(); + throw $e; + } + } + + /** + * @throws MappingException + */ + private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): array + { + $queries = []; + $allMeta = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($allMeta as $meta) { + if ($meta->isMappedSuperclass) { + continue; + } + + $tableName = $meta->getTableName(); + foreach ($meta->getAssociationMappings() as $assoc) { + if (ThirdParty::class !== $assoc['targetEntity']) { + continue; + } + + // phpstan wants boolean for if condition + if (($assoc['type'] & ClassMetadata::TO_ONE) !== 0) { + $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); + + $suffix = (ThirdParty::class === $assoc['sourceEntity']) ? 'chill_3party.' : ''; + + $queries[] = [ + 'sql' => "UPDATE {$suffix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ]; + } elseif (ClassMetadata::MANY_TO_MANY === $assoc['type'] && isset($assoc['joinTable'])) { + $joinTable = $assoc['joinTable']['name']; + $prefix = null !== ($assoc['joinTable']['schema'] ?? null) ? $assoc['joinTable']['schema'].'.' : ''; + $joinColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name']; + $queries[] = [ + 'sql' => "UPDATE {$prefix}{$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete AND NOT EXISTS (SELECT 1 FROM {$prefix}{$joinTable} WHERE {$joinColumn} = :toKeep)", + 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], + ]; + + $queries[] = [ + 'sql' => "DELETE FROM {$joinTable} WHERE {$joinColumn} = :toDelete", + 'params' => ['toDelete' => $toDelete->getId()], + ]; + } + } + } + + return $queries; + } + + public function removeThirdparty(ThirdParty $toKeep, ThirdParty $toDelete): array + { + return [ + [ + 'sql' => 'UPDATE chill_3party.third_party SET parent_id = :toKeep WHERE parent_id = :toDelete', + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ], + [ + 'sql' => 'UPDATE chill_3party.thirdparty_category SET thirdparty_id = :toKeep WHERE thirdparty_id = :toDelete AND NOT EXISTS (SELECT 1 FROM chill_3party.thirdparty_category WHERE thirdparty_id = :toKeep)', + 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], + ], + [ + 'sql' => 'DELETE FROM chill_3party.third_party WHERE id = :toDelete', + 'params' => ['toDelete' => $toDelete->getId()], + ], + ]; + } +} diff --git a/src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php b/src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php index a36ceda4e..427f2b9e3 100644 --- a/src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php +++ b/src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php @@ -39,6 +39,7 @@ class ThirdPartyRender implements ChillEntityRenderInterface 'showContacts' => $options['showContacts'] ?? false, 'showParent' => $options['showParent'] ?? true, 'isConfidential' => $options['isConfidential'] ?? false, + 'showFusion' => $options['showFusion'] ?? false, ]; return diff --git a/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php new file mode 100644 index 000000000..4b2819751 --- /dev/null +++ b/src/Bundle/ChillThirdPartyBundle/Tests/Service/ThirdpartyMergeServiceTest.php @@ -0,0 +1,86 @@ +em = self::getContainer()->get(EntityManagerInterface::class); + + $this->service = new ThirdpartyMergeService($this->em); + } + + public function testMergeUpdatesReferencesAndDeletesThirdparty(): void + { + // Create ThirdParty entities + $toKeep = new ThirdParty(); + $toKeep->setName('Thirdparty to keep'); + $this->em->persist($toKeep); + + $toDelete = new ThirdParty(); + $toDelete->setName('Thirdparty to delete'); + $this->em->persist($toDelete); + + // Create a related entity with TO_ONE relation (thirdparty parent) + $relatedToOneEntity = new ThirdParty(); + $relatedToOneEntity->setName('RelatedToOne thirdparty'); + $relatedToOneEntity->setParent($toDelete); + $this->em->persist($relatedToOneEntity); + + // Create a related entity with TO_MANY relation (thirdparty category) + $thirdpartyCategory = new ThirdPartyCategory(); + $thirdpartyCategory->setName(['fr' => 'Thirdparty category']); + $this->em->persist($thirdpartyCategory); + $toDelete->addCategory($thirdpartyCategory); + $this->em->persist($toDelete); + + $activity = new Activity(); + $activity->setDate(new \DateTime()); + $activity->addThirdParty($toDelete); + $this->em->persist($activity); + + $this->em->flush(); + + // Run merge + $this->service->merge($toKeep, $toDelete); + $this->em->refresh($toKeep); + $this->em->refresh($relatedToOneEntity); + + // Check that references were updated + $this->assertEquals($toKeep->getId(), $relatedToOneEntity->getParent()->getId(), 'The parent thirdparty was succesfully merged'); + + $updatedRelatedManyEntity = $this->em->find(ThirdPartyCategory::class, $thirdpartyCategory->getId()); + $this->assertContains($updatedRelatedManyEntity, $toKeep->getCategories(), 'The thirdparty category was found in the toKeep entity'); + + // Check that toDelete was removed + $this->em->clear(); + $deletedThirdParty = $this->em->find(ThirdParty::class, $toDelete->getId()); + $this->assertNull($deletedThirdParty); + } +} diff --git a/src/Bundle/ChillThirdPartyBundle/config/services.yaml b/src/Bundle/ChillThirdPartyBundle/config/services.yaml index 572026771..ffe3207d3 100644 --- a/src/Bundle/ChillThirdPartyBundle/config/services.yaml +++ b/src/Bundle/ChillThirdPartyBundle/config/services.yaml @@ -1,14 +1,14 @@ ---- services: - Chill\ThirdPartyBundle\Serializer\Normalizer\: + _defaults: autowire: true + autoconfigure: true + + Chill\ThirdPartyBundle\Serializer\Normalizer\: resource: '../Serializer/Normalizer/' tags: - { name: 'serializer.normalizer', priority: 64 } Chill\ThirdPartyBundle\Export\: - autowire: true - autoconfigure: true resource: '../Export/' Chill\ThirdPartyBundle\Validator\: @@ -16,3 +16,5 @@ services: autowire: true resource: '../Validator/' + Chill\ThirdPartyBundle\Service\ThirdpartyMergeService: ~ + diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index c5a5a110f..2c5e8f262 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -135,7 +135,6 @@ is thirdparty: Le demandeur est un tiers Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers "Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" - # admin admin: export_description: Liste des tiers (format CSV) @@ -155,3 +154,16 @@ Telephone2: Autre téléphone Contact email: Courrier électronique du contact Contact address: Adresse du contact Contact profession: Profession du contact + +thirdparty_duplicate: + title: Fusionner les tiers doublons + find: Désigner un tiers doublon + Thirdparty to keep: Tiers à conserver + Thirdparty to delete: Tiers à supprimer + Thirdparty to delete explanation: Ce tiers sera supprimé. Seuls les contacts de ce tiers, énumérés ci-dessous, seront transférés. + Thirdparty to keep explanation: Ce tiers sera conservé + Data to keep: Données conservées + You cannot merge a thirdparty with itself. Please choose a different thirdparty: Vous ne pouvez pas fusionner un tiers avec lui-même. Veuillez choisir un autre tiers. + A thirdparty can only be merged with a thirdparty of the same kind: Un tiers ne peut être fusionné qu'avec un tiers de même type. + Two child thirdparties must have the same parent: Deux tiers de type « contact » doivent avoir le même tiers parent. + Merge successful: La fusion a été effectuée avec succès From 904f4e5ed9edcd737a754be340a3856ee6b876d4 Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Mon, 18 Aug 2025 16:26:20 +0000 Subject: [PATCH 06/60] Add a filter to list for acpw where current user intervenes --- .changes/unreleased/Feature-20250717-110850.yaml | 6 ++++++ .../Controller/AccompanyingCourseWorkController.php | 8 ++++++++ .../AccompanyingPeriodWorkRepository.php | 12 +++++++++++- .../ChillPersonBundle/translations/messages.fr.yml | 2 +- 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/Feature-20250717-110850.yaml diff --git a/.changes/unreleased/Feature-20250717-110850.yaml b/.changes/unreleased/Feature-20250717-110850.yaml new file mode 100644 index 000000000..63a359e6e --- /dev/null +++ b/.changes/unreleased/Feature-20250717-110850.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Add filter to social actions list to filter out actions where current user intervenes +time: 2025-07-17T11:08:50.128269232+02:00 +custom: + Issue: "400" + SchemaChange: No schema change diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php index 523d5a875..29362ba83 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Templating\Listing\FilterOrderHelper; use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; @@ -130,6 +131,7 @@ final class AccompanyingCourseWorkController extends AbstractController $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period); $filter = $this->buildFilterOrder($period); + $currentUser = $this->getUser(); $filterData = [ 'types' => $filter->hasEntityChoice('typesFilter') ? $filter->getEntityChoiceData('typesFilter') : [], @@ -138,6 +140,10 @@ final class AccompanyingCourseWorkController extends AbstractController 'user' => $filter->getUserPickerData('userFilter'), ]; + if ($filter->getSingleCheckboxData('currentUserFilter') && $currentUser instanceof User) { + $filterData['currentUser'] = $currentUser; + } + $totalItems = $this->workRepository->countByAccompanyingPeriod($period); $paginator = $this->paginator->create($totalItems); @@ -201,6 +207,8 @@ final class AccompanyingCourseWorkController extends AbstractController ->addUserPicker('userFilter', 'accompanying_course_work.user_filter', ['required' => false]) ; + $filterBuilder->addSingleCheckbox('currentUserFilter', 'accompanying_course_work.my_actions_filter'); + return $filterBuilder->build(); } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 0d5bf5eee..df5ee6c66 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -90,7 +90,7 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository * * first, opened works * * then, closed works * - * @param array{types?: list, user?: list, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters + * @param array{types?: list, user?: list, currentUser?: User, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters * * @return AccompanyingPeriodWork[] */ @@ -101,6 +101,7 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository $sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id + AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE) WHERE accompanyingPeriod_id = :periodId"; // implement filters @@ -119,6 +120,10 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository .')'; } + if (isset($filters['currentUser'])) { + $sql .= ' AND rw.user_id = :currentUser'; + } + $sql .= " AND daterange(:after::date, :before::date) && daterange(w.startDate, w.endDate, '[]')"; // if the start and end date were inversed, we inverse the order to avoid an error @@ -152,6 +157,11 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository ->setParameter('limit', $limit, Types::INTEGER) ->setParameter('offset', $offset, Types::INTEGER); + if (isset($filters['currentUser'])) { + $nq->setParameter('currentUser', $filters['currentUser']->getId()); + } + + foreach ($filters['user'] as $key => $user) { $nq->setParameter('user_'.$key, $user); } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index c9f1af282..958ccb246 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -926,7 +926,7 @@ accompanying_course_work: types_filter: Filtrer par type d'action user_filter: Filtrer par intervenant On-going works over total: Actions en cours / Actions du parcours - + my_actions_filter: Mes actions (où j'interviens) # Person addresses: Adresses de résidence From e594b65d1e870306c5a942a81d63ea1719f7e5b1 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 28 Apr 2025 16:32:48 +0200 Subject: [PATCH 07/60] Create event theme admin entity --- .../Controller/EventThemeController.php | 44 +++++ .../ChillEventExtension.php | 44 ++++- .../ChillEventBundle/Entity/EventTheme.php | 183 ++++++++++++++++++ .../ChillEventBundle/Form/EventThemeType.php | 67 +++++++ .../Menu/AdminMenuBuilder.php | 8 +- .../views/Admin/EventTheme/edit.html.twig | 26 +++ .../views/Admin/EventTheme/index.html.twig | 45 +++++ .../views/Admin/EventTheme/new.html.twig | 11 ++ .../views/Entity/event_theme.html.twig | 13 ++ .../Templating/Entity/EventThemeRender.php | 109 +++++++++++ .../ChillEventBundle/config/services.yaml | 10 +- .../config/services/controller.yaml | 4 + .../config/services/forms.yaml | 4 + .../migrations/Version20250428092611.php | 40 ++++ .../translations/messages.fr.yml | 7 + 15 files changed, 608 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillEventBundle/Controller/EventThemeController.php create mode 100644 src/Bundle/ChillEventBundle/Entity/EventTheme.php create mode 100644 src/Bundle/ChillEventBundle/Form/EventThemeType.php create mode 100644 src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/edit.html.twig create mode 100644 src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/index.html.twig create mode 100644 src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/new.html.twig create mode 100644 src/Bundle/ChillEventBundle/Resources/views/Entity/event_theme.html.twig create mode 100644 src/Bundle/ChillEventBundle/Templating/Entity/EventThemeRender.php create mode 100644 src/Bundle/ChillEventBundle/migrations/Version20250428092611.php diff --git a/src/Bundle/ChillEventBundle/Controller/EventThemeController.php b/src/Bundle/ChillEventBundle/Controller/EventThemeController.php new file mode 100644 index 000000000..027b99241 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Controller/EventThemeController.php @@ -0,0 +1,44 @@ + 'create']); + } + + if ('edit' === $action) { + return parent::createFormFor($action, $entity, $formClass, ['step' => 'edit']); + } + + throw new \LogicException('action is not supported: '.$action); + } + + /** + * @param QueryBuilder|mixed $query + */ + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator): QueryBuilder + { + /* @var QueryBuilder $query */ + return $query->orderBy('e.ordering', 'ASC') + ->addOrderBy('e.id', 'ASC'); + } +} diff --git a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php index 0b30ca6c5..ecc072a58 100644 --- a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php +++ b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php @@ -11,6 +11,9 @@ declare(strict_types=1); namespace Chill\EventBundle\DependencyInjection; +use Chill\EventBundle\Controller\EventThemeController; +use Chill\EventBundle\Entity\EventTheme; +use Chill\EventBundle\Form\EventThemeType; use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\ParticipationVoter; use Symfony\Component\Config\FileLocator; @@ -26,7 +29,10 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension; */ class ChillEventExtension extends Extension implements PrependExtensionInterface { - public function load(array $configs, ContainerBuilder $container) + /** + * @throws \Exception + */ + public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); @@ -45,16 +51,17 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface /** (non-PHPdoc). * @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() */ - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { $this->prependAuthorization($container); + $this->prependCruds($container); $this->prependRoute($container); } /** * add authorization hierarchy. */ - protected function prependAuthorization(ContainerBuilder $container) + protected function prependAuthorization(ContainerBuilder $container): void { $container->prependExtensionConfig('security', [ 'role_hierarchy' => [ @@ -70,7 +77,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface /** * add route to route loader for chill. */ - protected function prependRoute(ContainerBuilder $container) + protected function prependRoute(ContainerBuilder $container): void { // add routes for custom bundle $container->prependExtensionConfig('chill_main', [ @@ -81,4 +88,33 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface ], ]); } + + protected function prependCruds(ContainerBuilder $container): void + { + $container->prependExtensionConfig('chill_main', [ + 'cruds' => [ + [ + 'class' => EventTheme::class, + 'name' => 'event_theme', + 'base_path' => '/admin/event/theme', + 'form_class' => EventThemeType::class, + 'controller' => EventThemeController::class, + 'actions' => [ + 'index' => [ + 'template' => '@ChillEvent/Admin/EventTheme/index.html.twig', + 'role' => 'ROLE_ADMIN', + ], + 'new' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillEvent/Admin/EventTheme/new.html.twig', + ], + 'edit' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillEvent/Admin/EventTheme/edit.html.twig', + ], + ], + ], + ], + ]); + } } diff --git a/src/Bundle/ChillEventBundle/Entity/EventTheme.php b/src/Bundle/ChillEventBundle/Entity/EventTheme.php new file mode 100644 index 000000000..902fc32fb --- /dev/null +++ b/src/Bundle/ChillEventBundle/Entity/EventTheme.php @@ -0,0 +1,183 @@ + + */ + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: EventTheme::class)] + private Collection $children; + + #[ORM\ManyToOne(targetEntity: EventTheme::class, inversedBy: 'children')] + private ?EventTheme $parent = null; + + #[ORM\Column(name: 'ordering', type: Types::FLOAT, options: ['default' => '0.0'])] + private float $ordering = 0.0; + + /** + * Constructor. + */ + public function __construct() + { + $this->children = new ArrayCollection(); + } + + /** + * Get active. + */ + public function getIsActive(): bool + { + return $this->isActive; + } + + /** + * Get id. + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * Get label. + */ + public function getName(): array + { + return $this->name; + } + + /** + * Set active. + * + * @param bool $active + * @return EventTheme + */ + public function setIsActive(bool $active): static + { + $this->isActive = $active; + + return $this; + } + + /** + * Set label. + * + * @param array $label + * @return EventTheme + */ + public function setName(array $label): static + { + $this->name = $label; + + return $this; + } + + public function addChild(self $child): self + { + if (!$this->children->contains($child)) { + $this->children[] = $child; + } + + return $this; + } + + public function removeChild(self $child): self + { + if ($this->children->removeElement($child)) { + // set the owning side to null (unless already changed) + if ($child->getParent() === $this) { + $child->setParent(null); + } + } + + return $this; + } + + public function getChildren(): Collection + { + return $this->children; + } + + public function getDescendants(): Collection + { + $descendants = new ArrayCollection(); + + foreach ($this->getChildren() as $child) { + if (!$descendants->contains($child)) { + $descendants->add($child); + + foreach ($child->getDescendants() as $descendantsOfChild) { + if (!$descendants->contains($descendantsOfChild)) { + $descendants->add($descendantsOfChild); + } + } + } + } + + return $descendants; + } + + public function hasParent(): bool + { + return null !== $this->parent; + } + + public function getOrdering(): float + { + return $this->ordering; + } + + public function setOrdering(float $ordering): EventTheme + { + $this->ordering = $ordering; + + return $this; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function setParent(?self $parent): self + { + $this->parent = $parent; + + $parent?->addChild($this); + + return $this; + } +} diff --git a/src/Bundle/ChillEventBundle/Form/EventThemeType.php b/src/Bundle/ChillEventBundle/Form/EventThemeType.php new file mode 100644 index 000000000..f48780641 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Form/EventThemeType.php @@ -0,0 +1,67 @@ +add('name', TranslatableStringFormType::class, [ + 'label' => 'Nom', + ]); + + if ('create' === $options['step']) { + $builder + ->add('parent', EntityType::class, [ + 'class' => EventTheme::class, + 'required' => false, + 'choice_label' => fn (EventTheme $theme): ?string => $this->translatableStringHelper->localize($theme->getName()), + 'mapped' => 'create' === $options['step'], + ]); + } + + $builder + ->add('ordering', NumberType::class, [ + 'required' => true, + 'scale' => 6, + ]) + ->add('isActive', ChoiceType::class, [ + 'choices' => [ + 'Yes' => true, + 'No' => false, + ], + 'expanded' => true, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => EventTheme::class, + ]); + $resolver->setRequired('step') + ->setAllowedValues('step', ['create', 'edit']); + } +} diff --git a/src/Bundle/ChillEventBundle/Menu/AdminMenuBuilder.php b/src/Bundle/ChillEventBundle/Menu/AdminMenuBuilder.php index 9db1713b6..8a6dec1ad 100644 --- a/src/Bundle/ChillEventBundle/Menu/AdminMenuBuilder.php +++ b/src/Bundle/ChillEventBundle/Menu/AdminMenuBuilder.php @@ -20,14 +20,14 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface /** * @var AuthorizationCheckerInterface */ - protected $authorizationChecker; + protected AuthorizationCheckerInterface $authorizationChecker; public function __construct(AuthorizationCheckerInterface $authorizationChecker) { $this->authorizationChecker = $authorizationChecker; } - public function buildMenu($menuId, MenuItem $menu, array $parameters) + public function buildMenu($menuId, MenuItem $menu, array $parameters): void { if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) { return; @@ -52,6 +52,10 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface $menu->addChild('Role', [ 'route' => 'chill_event_admin_role', ])->setExtras(['order' => 6530]); + + $menu->addChild('Theme', [ + 'route' => 'chill_crud_event_theme_index', + ])->setExtras(['order' => 6540]); } public static function getMenuIds(): array diff --git a/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/edit.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/edit.html.twig new file mode 100644 index 000000000..a63d81c99 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/edit.html.twig @@ -0,0 +1,26 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_edit_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_edit_content.html.twig' %} + + {% block crud_content_form_rows %} + {{ form_row(form.name) }} +
    + +
    + {{ entity.parent|chill_entity_render_box }} +
    +
    + {{ form_row(form.ordering) }} + {{ form_row(form.isActive) }} + {% endblock crud_content_form_rows %} + + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/index.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/index.html.twig new file mode 100644 index 000000000..d518ea625 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/index.html.twig @@ -0,0 +1,45 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_index.html.twig' %} + {% block table_entities_thead_tr %} + {{ 'Id'|trans }} + {{ 'Title'|trans }} + {{ 'Ordering'|trans }} + {{ 'active'|trans }} +   + {% endblock %} + + {% block table_entities_tbody %} + {% for entity in entities %} + + {{ entity.id }} + + {{ entity|chill_entity_render_box }} + + {{ entity.ordering }} + + {%- if entity.isActive -%} + + {%- else -%} + + {%- endif -%} + + +
      +
    • + +
    • +
    + + + {% endfor %} + {% endblock %} + + {% block actions_before %} +
  • + {{'Back to the admin'|trans}} +
  • + {% endblock %} + {% endembed %} +{% endblock %} diff --git a/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/new.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/new.html.twig new file mode 100644 index 000000000..7c204dddd --- /dev/null +++ b/src/Bundle/ChillEventBundle/Resources/views/Admin/EventTheme/new.html.twig @@ -0,0 +1,11 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_new_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_new_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillEventBundle/Resources/views/Entity/event_theme.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Entity/event_theme.html.twig new file mode 100644 index 000000000..08796d7b2 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Resources/views/Entity/event_theme.html.twig @@ -0,0 +1,13 @@ +{% set reversed_parents = parents|reverse %} + + + {%- for p in reversed_parents %} + + {{ p.name|localize_translatable_string }}{{ options['default.separator'] }} + + {%- endfor -%} + + {{ eventTheme.name|localize_translatable_string }} + + + diff --git a/src/Bundle/ChillEventBundle/Templating/Entity/EventThemeRender.php b/src/Bundle/ChillEventBundle/Templating/Entity/EventThemeRender.php new file mode 100644 index 000000000..ad765d714 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Templating/Entity/EventThemeRender.php @@ -0,0 +1,109 @@ + + */ +class EventThemeRender implements ChillEntityRenderInterface +{ + public const AND_CHILDREN_MENTION = 'show_and_children_mention'; + + public const DEFAULT_ARGS = [ + self::SEPARATOR_KEY => ' > ', + self::SHOW_AND_CHILDREN => false, + self::AND_CHILDREN_MENTION => 'event_theme.and children', + ]; + + public const SEPARATOR_KEY = 'default.separator'; + + /** + * Show a mention "and children" on each EventTheme, if the event theme + * has at least one child. + */ + public const SHOW_AND_CHILDREN = 'show_and_children'; + + public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {} + + /** + * @throws RuntimeError + * @throws SyntaxError + * @throws LoaderError + */ + public function renderBox($eventTheme, array $options): string + { + $options = array_merge(self::DEFAULT_ARGS, $options); + // give some help to twig: an array of parents + $parents = $this->buildParents($eventTheme); + + return $this + ->engine + ->render( + '@ChillEvent/Entity/event_theme.html.twig', + [ + 'eventTheme' => $eventTheme, + 'parents' => $parents, + 'options' => $options, + ] + ); + } + + public function renderString($entity, array $options): string + { + /** @var EventTheme $entity */ + $options = array_merge(self::DEFAULT_ARGS, $options); + + $titles = [$this->translatableStringHelper->localize($entity->getName())]; + + // loop to parent, until root + while ($entity->hasParent()) { + $entity = $entity->getParent(); + $titles[] = $this->translatableStringHelper->localize( + $entity->getTitle() + ); + } + + $titles = \array_reverse($titles); + + $title = \implode($options[self::SEPARATOR_KEY], $titles); + + if ($options[self::SHOW_AND_CHILDREN] && $entity->hasChildren()) { + $title .= ' ('.$this->translator->trans($options[self::AND_CHILDREN_MENTION]).')'; + } + + return $title; + } + + public function supports($entity, array $options): bool + { + return $entity instanceof EventTheme; + } + + private function buildParents(EventTheme $entity): array + { + $parents = []; + + while ($entity->hasParent()) { + $entity = $parents[] = $entity->getParent(); + } + + return $parents; + } +} diff --git a/src/Bundle/ChillEventBundle/config/services.yaml b/src/Bundle/ChillEventBundle/config/services.yaml index cee12a024..b117528a8 100644 --- a/src/Bundle/ChillEventBundle/config/services.yaml +++ b/src/Bundle/ChillEventBundle/config/services.yaml @@ -1,6 +1,7 @@ services: Chill\EventBundle\Controller\: autowire: true + autoconfigure: true resource: '../Controller' tags: ['controller.service_arguments'] @@ -8,4 +9,11 @@ services: autowire: true autoconfigure: true resource: '../Menu/' - tags: ['chill.menu_builder'] \ No newline at end of file + tags: ['chill.menu_builder'] + + Chill\EventBundle\Templating\Entity\: + autowire: true + autoconfigure: true + resource: '../Templating/Entity' + tags: + - 'chill.render_entity' diff --git a/src/Bundle/ChillEventBundle/config/services/controller.yaml b/src/Bundle/ChillEventBundle/config/services/controller.yaml index e69de29bb..2dda648c9 100644 --- a/src/Bundle/ChillEventBundle/config/services/controller.yaml +++ b/src/Bundle/ChillEventBundle/config/services/controller.yaml @@ -0,0 +1,4 @@ +services: + _defaults: + autowire: true + autoconfigure: true diff --git a/src/Bundle/ChillEventBundle/config/services/forms.yaml b/src/Bundle/ChillEventBundle/config/services/forms.yaml index 8c81c3e63..205307234 100644 --- a/src/Bundle/ChillEventBundle/config/services/forms.yaml +++ b/src/Bundle/ChillEventBundle/config/services/forms.yaml @@ -31,3 +31,7 @@ services: Chill\EventBundle\Form\Type\PickEventType: tags: - { name: form.type } + + Chill\EventBundle\Form\EventThemeType: + tags: + - { name: form.type } diff --git a/src/Bundle/ChillEventBundle/migrations/Version20250428092611.php b/src/Bundle/ChillEventBundle/migrations/Version20250428092611.php new file mode 100644 index 000000000..811a368ad --- /dev/null +++ b/src/Bundle/ChillEventBundle/migrations/Version20250428092611.php @@ -0,0 +1,40 @@ +addSql('CREATE SEQUENCE chill_event_event_theme_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_event_event_theme (id INT NOT NULL, parent_id INT DEFAULT NULL, isActive BOOLEAN NOT NULL, name JSON NOT NULL, ordering DOUBLE PRECISION DEFAULT \'0.0\' NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_80D7C6B0727ACA70 ON chill_event_event_theme (parent_id)'); + $this->addSql('ALTER TABLE chill_event_event_theme ADD CONSTRAINT FK_80D7C6B0727ACA70 FOREIGN KEY (parent_id) REFERENCES chill_event_event_theme (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_event_event_theme DROP CONSTRAINT FK_80D7C6B0727ACA70'); + $this->addSql('DROP TABLE chill_event_event_theme'); + } +} diff --git a/src/Bundle/ChillEventBundle/translations/messages.fr.yml b/src/Bundle/ChillEventBundle/translations/messages.fr.yml index 6319a0765..15987c463 100644 --- a/src/Bundle/ChillEventBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillEventBundle/translations/messages.fr.yml @@ -140,3 +140,10 @@ event: event_types: Par types d'événement event_dates: Par date d'événement +crud: + event_theme: + title_new: Créér une nouvelle thématique + title_edit: Modifier la thématique + index: + title: Liste des thématiques + add_new: Créér une nouvelle thématique From adc9c47d0a7301fb4babe911a7e2e5139b0063be Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 28 Apr 2025 16:46:17 +0200 Subject: [PATCH 08/60] Add event theme color for badge --- .../Resources/public/chill/scss/chill_variables.scss | 3 ++- .../Resources/public/chill/scss/render_box.scss | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/chill_variables.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/chill_variables.scss index 2f113f45b..dce2b4a34 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/chill_variables.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/chill_variables.scss @@ -10,8 +10,9 @@ $chill-household-context: #929d69; // Badges colors $social-issue-color: #4bafe8; $social-action-color: $orange; +$event-theme-color: #ecc546; $activity-color: yellowgreen; // budget colors $budget-resource-color: #6d9e63; -$budget-charge-color: #e03851; \ No newline at end of file +$budget-charge-color: #e03851; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/render_box.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/render_box.scss index 230640bbd..6ec463f77 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/render_box.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/render_box.scss @@ -10,7 +10,8 @@ /// SOCIAL-ISSUE AND SOCIAL-ACTION &.entity-social-issue, - &.entity-social-action { + &.entity-social-action, + &.entity-event-theme { margin-right: 0.3em; font-size: 120%; span.badge { @@ -32,4 +33,9 @@ @include badge_social($social-action-color); } } + &.entity-event-theme { + span.badge { + @include badge_social($event-theme-color); + } + } } From e7a1ff1ac8c1c834517ecbb124601894f82d5522 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 29 Apr 2025 10:02:52 +0200 Subject: [PATCH 09/60] Add event theme property to event entity --- src/Bundle/ChillEventBundle/Entity/Event.php | 38 +++++++++++++++---- .../migrations/Version20250429062911.php | 35 +++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 src/Bundle/ChillEventBundle/migrations/Version20250429062911.php diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index c065ed94f..28fcf6092 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -71,6 +71,10 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter #[ORM\ManyToOne(targetEntity: EventType::class)] private ?EventType $type = null; + #[ORM\ManyToMany(targetEntity: EventTheme::class)] + #[ORM\JoinTable('chill_event_eventtheme')] + private Collection $themes; + #[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_')] private CommentEmbeddable $comment; @@ -96,6 +100,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter $this->participations = new ArrayCollection(); $this->documents = new ArrayCollection(); $this->comment = new CommentEmbeddable(); + $this->themes = new ArrayCollection(); } /** @@ -126,10 +131,27 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter return $this; } + public function getThemes(): Collection + { + return $this->themes; + } + + public function addTheme(EventTheme $theme): self + { + $this->themes->add($theme); + + return $this; + } + + public function removeTheme(EventTheme $theme): void + { + $this->themes->removeElement($theme); + } + /** * @return Center */ - public function getCenter() + public function getCenter(): ?Center { return $this->center; } @@ -137,7 +159,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter /** * @return Scope */ - public function getCircle() + public function getCircle(): ?Scope { return $this->circle; } @@ -147,7 +169,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter * * @return \DateTime */ - public function getDate() + public function getDate(): ?\DateTime { return $this->date; } @@ -157,7 +179,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter * * @return int */ - public function getId() + public function getId(): ?int { return $this->id; } @@ -172,7 +194,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter * * @return string */ - public function getName() + public function getName(): ?string { return $this->name; } @@ -202,7 +224,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter * * @return Scope */ - public function getScope() + public function getScope(): ?Scope { return $this->getCircle(); } @@ -210,7 +232,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter /** * @return EventType */ - public function getType() + public function getType(): ?EventType { return $this->type; } @@ -218,7 +240,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter /** * Remove participation. */ - public function removeParticipation(Participation $participation) + public function removeParticipation(Participation $participation): void { $this->participations->removeElement($participation); } diff --git a/src/Bundle/ChillEventBundle/migrations/Version20250429062911.php b/src/Bundle/ChillEventBundle/migrations/Version20250429062911.php new file mode 100644 index 000000000..9f931eaa1 --- /dev/null +++ b/src/Bundle/ChillEventBundle/migrations/Version20250429062911.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE chill_event_eventtheme (event_id INT NOT NULL, eventtheme_id INT NOT NULL, PRIMARY KEY(event_id, eventtheme_id))'); + $this->addSql('CREATE INDEX IDX_8D75029771F7E88B ON chill_event_eventtheme (event_id)'); + $this->addSql('CREATE INDEX IDX_8D750297A81D3C55 ON chill_event_eventtheme (eventtheme_id)'); + $this->addSql('ALTER TABLE chill_event_eventtheme ADD CONSTRAINT FK_8D75029771F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_event_eventtheme ADD CONSTRAINT FK_8D750297A81D3C55 FOREIGN KEY (eventtheme_id) REFERENCES chill_event_event_theme (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_event_eventtheme DROP CONSTRAINT FK_8D75029771F7E88B'); + $this->addSql('ALTER TABLE chill_event_eventtheme DROP CONSTRAINT FK_8D750297A81D3C55'); + $this->addSql('DROP TABLE chill_event_eventtheme'); + } +} From 383f5887950bba90042193f35539808b453024a0 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 29 Apr 2025 10:03:42 +0200 Subject: [PATCH 10/60] Add field in event for themes --- .../ChillEventBundle/Entity/EventTheme.php | 5 +-- .../ChillEventBundle/Form/EventType.php | 11 +++++ .../Form/Type/PickEventThemeType.php | 45 +++++++++++++++++++ .../Form/Type/PickEventTypeType.php | 5 +-- .../Repository/EventThemeRepository.php | 38 ++++++++++++++++ .../Templating/Entity/EventThemeRender.php | 8 ++-- .../config/services/forms.yaml | 8 +++- 7 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 src/Bundle/ChillEventBundle/Form/Type/PickEventThemeType.php create mode 100644 src/Bundle/ChillEventBundle/Repository/EventThemeRepository.php diff --git a/src/Bundle/ChillEventBundle/Entity/EventTheme.php b/src/Bundle/ChillEventBundle/Entity/EventTheme.php index 902fc32fb..28bc87e04 100644 --- a/src/Bundle/ChillEventBundle/Entity/EventTheme.php +++ b/src/Bundle/ChillEventBundle/Entity/EventTheme.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\EventBundle\Entity; +use Chill\EventBundle\Repository\EventThemeRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -19,8 +20,8 @@ use Doctrine\ORM\Mapping as ORM; /** * Class EventTheme. */ -#[ORM\Entity] #[ORM\HasLifecycleCallbacks] +#[ORM\Entity(repositoryClass: EventThemeRepository::class)] #[ORM\Table(name: 'chill_event_event_theme')] class EventTheme { @@ -82,7 +83,6 @@ class EventTheme /** * Set active. * - * @param bool $active * @return EventTheme */ public function setIsActive(bool $active): static @@ -95,7 +95,6 @@ class EventTheme /** * Set label. * - * @param array $label * @return EventTheme */ public function setName(array $label): static diff --git a/src/Bundle/ChillEventBundle/Form/EventType.php b/src/Bundle/ChillEventBundle/Form/EventType.php index ebdf66010..77844aaba 100644 --- a/src/Bundle/ChillEventBundle/Form/EventType.php +++ b/src/Bundle/ChillEventBundle/Form/EventType.php @@ -14,7 +14,10 @@ namespace Chill\EventBundle\Form; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Form\StoredObjectType; use Chill\EventBundle\Entity\Event; +use Chill\EventBundle\Entity\EventTheme; +use Chill\EventBundle\Form\Type\PickEventThemeType; use Chill\EventBundle\Form\Type\PickEventTypeType; +use Chill\EventBundle\Repository\EventThemeRepository; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillDateTimeType; @@ -22,14 +25,19 @@ use Chill\MainBundle\Form\Type\CommentType; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Form\Type\PickUserLocationType; use Chill\MainBundle\Form\Type\ScopePickerType; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; class EventType extends AbstractType { + public function __construct(private readonly EventThemeRepository $eventThemeRepository, private readonly TranslatorInterface $translator, private readonly TranslatableStringHelperInterface $translatableStringHelper) {} + public function buildForm(FormBuilderInterface $builder, array $options) { $builder @@ -49,6 +57,9 @@ class EventType extends AbstractType 'class' => '', ], ]) + ->add('themes', PickEventThemeType::class, [ + 'multiple' => true, + ]) ->add('moderator', PickUserDynamicType::class, [ 'label' => 'Pick a moderator', ]) diff --git a/src/Bundle/ChillEventBundle/Form/Type/PickEventThemeType.php b/src/Bundle/ChillEventBundle/Form/Type/PickEventThemeType.php new file mode 100644 index 000000000..3010df271 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Form/Type/PickEventThemeType.php @@ -0,0 +1,45 @@ +setDefaults([ + 'class' => EventTheme::class, + 'choices' => $this->eventThemeRepository->findByActiveOrdered(), + 'choice_label' => fn (EventTheme $et) => $this->eventThemeRender->renderString($et, []), + 'placeholder' => 'event.form.Select one or more themes', + 'required' => true, + 'attr' => ['class' => 'select2'], + 'label' => 'event.theme.label', + 'multiple' => false, + ]) + ->setAllowedTypes('multiple', ['bool']); + } + + public function getParent(): string + { + return EntityType::class; + } +} diff --git a/src/Bundle/ChillEventBundle/Form/Type/PickEventTypeType.php b/src/Bundle/ChillEventBundle/Form/Type/PickEventTypeType.php index d027c05cf..472fdd463 100644 --- a/src/Bundle/ChillEventBundle/Form/Type/PickEventTypeType.php +++ b/src/Bundle/ChillEventBundle/Form/Type/PickEventTypeType.php @@ -23,10 +23,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; */ class PickEventTypeType extends AbstractType { - /** - * @var TranslatableStringHelper - */ - protected $translatableStringHelper; + protected TranslatableStringHelper $translatableStringHelper; public function __construct(TranslatableStringHelper $helper) { diff --git a/src/Bundle/ChillEventBundle/Repository/EventThemeRepository.php b/src/Bundle/ChillEventBundle/Repository/EventThemeRepository.php new file mode 100644 index 000000000..c8956356b --- /dev/null +++ b/src/Bundle/ChillEventBundle/Repository/EventThemeRepository.php @@ -0,0 +1,38 @@ + + */ +class EventThemeRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, EventTheme::class); + } + + public function findByActiveOrdered(): array + { + return $this->createQueryBuilder('t') + ->select('t') + ->where('t.isActive = True') + ->orderBy('t.ordering', 'ASC') + ->getQuery() + ->getResult(); + } + +} diff --git a/src/Bundle/ChillEventBundle/Templating/Entity/EventThemeRender.php b/src/Bundle/ChillEventBundle/Templating/Entity/EventThemeRender.php index ad765d714..6a10b61f7 100644 --- a/src/Bundle/ChillEventBundle/Templating/Entity/EventThemeRender.php +++ b/src/Bundle/ChillEventBundle/Templating/Entity/EventThemeRender.php @@ -47,18 +47,18 @@ class EventThemeRender implements ChillEntityRenderInterface * @throws SyntaxError * @throws LoaderError */ - public function renderBox($eventTheme, array $options): string + public function renderBox($entity, array $options): string { $options = array_merge(self::DEFAULT_ARGS, $options); // give some help to twig: an array of parents - $parents = $this->buildParents($eventTheme); + $parents = $this->buildParents($entity); return $this ->engine ->render( '@ChillEvent/Entity/event_theme.html.twig', [ - 'eventTheme' => $eventTheme, + 'eventTheme' => $entity, 'parents' => $parents, 'options' => $options, ] @@ -76,7 +76,7 @@ class EventThemeRender implements ChillEntityRenderInterface while ($entity->hasParent()) { $entity = $entity->getParent(); $titles[] = $this->translatableStringHelper->localize( - $entity->getTitle() + $entity->getName() ); } diff --git a/src/Bundle/ChillEventBundle/config/services/forms.yaml b/src/Bundle/ChillEventBundle/config/services/forms.yaml index 205307234..9264a09ee 100644 --- a/src/Bundle/ChillEventBundle/config/services/forms.yaml +++ b/src/Bundle/ChillEventBundle/config/services/forms.yaml @@ -32,6 +32,12 @@ services: tags: - { name: form.type } - Chill\EventBundle\Form\EventThemeType: + Chill\EventBundle\Form\Type\PickEventThemeType: tags: - { name: form.type } + + Chill\EventBundle\Form\EventType: + tags: + - { name: form.type } + + From 27f0bf28e9324eb924a127c7ea3606f21821567b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 29 Apr 2025 10:03:57 +0200 Subject: [PATCH 11/60] Adjust templates and translations --- .../Menu/AdminMenuBuilder.php | 5 +- .../Resources/views/Event/edit.html.twig | 1 + .../Resources/views/Event/new.html.twig | 4 +- .../Resources/views/Event/page_list.html.twig | 5 + .../Resources/views/Event/show.html.twig | 191 +++++++++--------- .../translations/messages.fr.yml | 3 + 6 files changed, 112 insertions(+), 97 deletions(-) diff --git a/src/Bundle/ChillEventBundle/Menu/AdminMenuBuilder.php b/src/Bundle/ChillEventBundle/Menu/AdminMenuBuilder.php index 8a6dec1ad..07fd81734 100644 --- a/src/Bundle/ChillEventBundle/Menu/AdminMenuBuilder.php +++ b/src/Bundle/ChillEventBundle/Menu/AdminMenuBuilder.php @@ -17,9 +17,6 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; class AdminMenuBuilder implements LocalMenuBuilderInterface { - /** - * @var AuthorizationCheckerInterface - */ protected AuthorizationCheckerInterface $authorizationChecker; public function __construct(AuthorizationCheckerInterface $authorizationChecker) @@ -53,7 +50,7 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface 'route' => 'chill_event_admin_role', ])->setExtras(['order' => 6530]); - $menu->addChild('Theme', [ + $menu->addChild('event.theme.label', [ 'route' => 'chill_crud_event_theme_index', ])->setExtras(['order' => 6540]); } diff --git a/src/Bundle/ChillEventBundle/Resources/views/Event/edit.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Event/edit.html.twig index b6b11878b..052a13e26 100644 --- a/src/Bundle/ChillEventBundle/Resources/views/Event/edit.html.twig +++ b/src/Bundle/ChillEventBundle/Resources/views/Event/edit.html.twig @@ -17,6 +17,7 @@ {{ form_row(edit_form.date) }} {{ form_row(edit_form.type, { label: "Event type" }) }} + {{ form_row(edit_form.themes) }} {{ form_row(edit_form.moderator) }} {{ form_row(edit_form.location) }} {{ form_row(edit_form.organizationCost) }} diff --git a/src/Bundle/ChillEventBundle/Resources/views/Event/new.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Event/new.html.twig index 0fb69a4ea..898af74c1 100644 --- a/src/Bundle/ChillEventBundle/Resources/views/Event/new.html.twig +++ b/src/Bundle/ChillEventBundle/Resources/views/Event/new.html.twig @@ -13,10 +13,12 @@ {{ form_start(form) }} {{ form_errors(form) }} - {{ form_row(form.circle) }} +{# {{ form_row(form.circle) }}#} {{ form_row(form.name) }} + {{ form_row(form.circle) }} {{ form_row(form.date) }} {{ form_row(form.type, { label: "Event type" }) }} + {{ form_row(form.themes) }} {{ form_row(form.moderator) }} {{ form_row(form.location) }} {{ form_row(form.organizationCost) }} diff --git a/src/Bundle/ChillEventBundle/Resources/views/Event/page_list.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Event/page_list.html.twig index 4d887d3c5..bf6ab48f0 100644 --- a/src/Bundle/ChillEventBundle/Resources/views/Event/page_list.html.twig +++ b/src/Bundle/ChillEventBundle/Resources/views/Event/page_list.html.twig @@ -41,6 +41,11 @@ block js %} {{ e.moderator | chill_entity_render_box }}

    {% endif %} +
    + {% for t in e.themes %} + {{ t|chill_entity_render_box }} + {% endfor %} +
    diff --git a/src/Bundle/ChillEventBundle/Resources/views/Event/show.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Event/show.html.twig index 7d8bf1fc0..b124c528b 100644 --- a/src/Bundle/ChillEventBundle/Resources/views/Event/show.html.twig +++ b/src/Bundle/ChillEventBundle/Resources/views/Event/show.html.twig @@ -38,6 +38,14 @@ {{ 'Event type'|trans }} {{ event.type.name|localize_translatable_string }} + + {{ 'event.theme.label'|trans }} + + {% for t in event.themes %} + {{ t|chill_entity_render_box }} + {% endfor %} + + {{ 'Moderator'|trans }} {{ event.moderator|trans|default('-') }} @@ -80,6 +88,97 @@
    {% endif %} +
    +

    {{ 'Participations'|trans }}

    + {% set count = event.participations|length %} +

    {{ 'count participations to this event'|trans({'count': count}) }}

    + + {% if count > 0 %} + + + + + + + + + + + + {% for participation in event.participations %} + + + + + + + + {% endfor %} + +
    {{ 'Person'|trans }}{{ 'Role'|trans }}{{ 'Status'|trans }}{{ 'Last update'|trans }} 
    + {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { + targetEntity: { name: 'person', id: participation.person.id }, + action: 'show', + displayBadge: true, + buttonText: participation.person|chill_entity_render_string, + isDead: participation.person.deathdate is not null + } %} + {{ participation.role.name|localize_translatable_string }}{{ participation.status.name|localize_translatable_string }}{{ participation.lastUpdate|ago }} {# sf4 check: filter 'time_diff' is abandoned, + alternative: knplabs/knp-time-bundle provide filter 'ago' #} + + +
      + {% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %} +
    • + +
    • +
    • + +
    • + {% endif %} +
    +
    + + + {% endif %} + +
    +
    + {{ form_start(form_add_participation_by_person) }} +
    + {{ form_widget(form_add_participation_by_person.person_id, { 'attr' : { + 'class' : 'custom-select', + 'style': 'min-width: 15em; max-width: 18em; display: inline-block;' + }} ) }} +
    + + {{ form_end(form_add_participation_by_person) }} +
    +
    + +
      + {% if count > 0 %} +
    • + {{ form_start(form_export, {'attr': {'id': 'export_tableur'}}) }} +
      + {{ form_widget(form_export.format, { 'attr' : { 'class': 'custom-select' } }) }} +
      + {{ form_widget(form_export.submit, { 'attr' : { 'class': 'btn btn-save' } }) }} +
      + +
      + {{ form_rest(form_export) }} + {{ form_end(form_export) }} +
    • +
    • {{ 'Edit all the participations'|trans }}
    • + {% endif %} +
    +
    +
    + {{ chill_delegated_block('block_footer_show', { 'event': event }) }} +
      @@ -100,97 +199,5 @@
    - -

    {{ 'Participations'|trans }}

    - {% set count = event.participations|length %} -

    {{ 'count participations to this event'|trans({'count': count}) }}

    - - {% if count > 0 %} - - - - - - - - - - - - {% for participation in event.participations %} - - - - - - - - {% endfor %} - -
    {{ 'Person'|trans }}{{ 'Role'|trans }}{{ 'Status'|trans }}{{ 'Last update'|trans }} 
    - {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { - targetEntity: { name: 'person', id: participation.person.id }, - action: 'show', - displayBadge: true, - buttonText: participation.person|chill_entity_render_string, - isDead: participation.person.deathdate is not null - } %} - {{ participation.role.name|localize_translatable_string }}{{ participation.status.name|localize_translatable_string }}{{ participation.lastUpdate|ago }} {# sf4 check: filter 'time_diff' is abandoned, - alternative: knplabs/knp-time-bundle provide filter 'ago' #} - - -
      - {% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %} -
    • - -
    • -
    • - -
    • - {% endif %} -
    -
    - - - {% endif %} - - - -
    -
    - {{ form_start(form_add_participation_by_person) }} -
    - {{ form_widget(form_add_participation_by_person.person_id, { 'attr' : { - 'class' : 'custom-select', - 'style': 'min-width: 15em; max-width: 18em; display: inline-block;' - }} ) }} -
    - - {{ form_end(form_add_participation_by_person) }} -
    - -
    - {{ form_start(form_export, {'attr': {'id': 'export_tableur'}}) }} -
    - {{ form_widget(form_export.format, { 'attr' : { 'class': 'custom-select' } }) }} -
    - {{ form_widget(form_export.submit, { 'attr' : { 'class': 'btn btn-save' } }) }} -
    - -
    - {{ form_rest(form_export) }} - {{ form_end(form_export) }} -
    -
    - -
    - {{ chill_delegated_block('block_footer_show', { 'event': event }) }} -
    {% endblock %} diff --git a/src/Bundle/ChillEventBundle/translations/messages.fr.yml b/src/Bundle/ChillEventBundle/translations/messages.fr.yml index 15987c463..73f56f27f 100644 --- a/src/Bundle/ChillEventBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillEventBundle/translations/messages.fr.yml @@ -128,6 +128,8 @@ Create a new type: Créer un nouveau type Create a new status: Créer un nouveau statut event: + theme: + label: Thématiques fields: organizationCost: Coût d'organisation location: Localisation @@ -136,6 +138,7 @@ event: organisationCost_help: Coût d'organisation pour la structure. Utile pour les statistiques. add_document: Ajouter un document remove_document: Supprimer le document + Select one or more themes: Selectionnez une ou plusieurs thématiques filter: event_types: Par types d'événement event_dates: Par date d'événement From bb71e084b8d7271744d06759470b95735bc777c9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 29 Apr 2025 14:31:39 +0200 Subject: [PATCH 12/60] Create address on the fly field in event form --- .../Controller/EventController.php | 6 ++ src/Bundle/ChillEventBundle/Entity/Event.php | 9 +++ .../ChillEventBundle/Form/EventType.php | 22 +++--- .../Resources/public/vuejs/App.vue | 14 ++++ .../Resources/public/vuejs/index.js | 6 ++ .../Resources/public/vuejs/store.js | 79 +++++++++++++++++++ .../Resources/views/Event/new.html.twig | 28 +++++-- .../ChillEventBundle/chill.webpack.config.js | 7 ++ .../vuejs/Address/components/AddAddress.vue | 68 ++++++++++------ .../components/AddAddress/AddressMore.vue | 58 ++++++++++---- .../AddAddress/AddressSelection.vue | 42 +++++++--- .../components/AddAddress/CitySelection.vue | 42 +++++++--- .../AddAddress/CountrySelection.vue | 28 +++++-- .../vuejs/Address/components/EditPane.vue | 21 ++++- .../vuejs/Address/components/ShowPane.vue | 56 +++++++++---- 15 files changed, 386 insertions(+), 100 deletions(-) create mode 100644 src/Bundle/ChillEventBundle/Resources/public/vuejs/App.vue create mode 100644 src/Bundle/ChillEventBundle/Resources/public/vuejs/index.js create mode 100644 src/Bundle/ChillEventBundle/Resources/public/vuejs/store.js diff --git a/src/Bundle/ChillEventBundle/Controller/EventController.php b/src/Bundle/ChillEventBundle/Controller/EventController.php index b4f099769..09cb86861 100644 --- a/src/Bundle/ChillEventBundle/Controller/EventController.php +++ b/src/Bundle/ChillEventBundle/Controller/EventController.php @@ -41,6 +41,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -59,6 +60,8 @@ final class EventController extends AbstractController private readonly PaginatorFactory $paginator, private readonly Security $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry, + private readonly SerializerInterface $serializer, + ) {} #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])] @@ -202,9 +205,12 @@ final class EventController extends AbstractController return $this->redirectToRoute('chill_event__event_show', ['event_id' => $entity->getId()]); } + $entity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']); + return $this->render('@ChillEvent/Event/new.html.twig', [ 'entity' => $entity, 'form' => $form->createView(), + 'entity_json' => $entity_array, ]); } diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index 28fcf6092..c45c68159 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -27,6 +27,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Serializer\Annotation as Serializer; /** * Class Event. @@ -58,6 +59,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter private ?User $moderator = null; #[Assert\NotBlank] + #[Serializer\Groups(['read'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 150)] private ?string $name = null; @@ -65,13 +67,19 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter * @var Collection */ #[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)] + #[Serializer\Groups(['read'])] private Collection $participations; #[Assert\NotNull] + #[Serializer\Groups(['read'])] #[ORM\ManyToOne(targetEntity: EventType::class)] private ?EventType $type = null; + /** + * @var Collection + */ #[ORM\ManyToMany(targetEntity: EventTheme::class)] + #[Serializer\Groups(['read'])] #[ORM\JoinTable('chill_event_eventtheme')] private Collection $themes; @@ -79,6 +87,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter private CommentEmbeddable $comment; #[ORM\ManyToOne(targetEntity: Location::class)] + #[Serializer\Groups(['read'])] #[ORM\JoinColumn(nullable: true)] private ?Location $location = null; diff --git a/src/Bundle/ChillEventBundle/Form/EventType.php b/src/Bundle/ChillEventBundle/Form/EventType.php index 77844aaba..6bceed93f 100644 --- a/src/Bundle/ChillEventBundle/Form/EventType.php +++ b/src/Bundle/ChillEventBundle/Form/EventType.php @@ -14,29 +14,27 @@ namespace Chill\EventBundle\Form; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Form\StoredObjectType; use Chill\EventBundle\Entity\Event; -use Chill\EventBundle\Entity\EventTheme; use Chill\EventBundle\Form\Type\PickEventThemeType; use Chill\EventBundle\Form\Type\PickEventTypeType; -use Chill\EventBundle\Repository\EventThemeRepository; use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer; use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillDateTimeType; use Chill\MainBundle\Form\Type\CommentType; use Chill\MainBundle\Form\Type\PickUserDynamicType; -use Chill\MainBundle\Form\Type\PickUserLocationType; use Chill\MainBundle\Form\Type\ScopePickerType; -use Chill\MainBundle\Templating\TranslatableStringHelperInterface; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Contracts\Translation\TranslatorInterface; class EventType extends AbstractType { - public function __construct(private readonly EventThemeRepository $eventThemeRepository, private readonly TranslatorInterface $translator, private readonly TranslatableStringHelperInterface $translatableStringHelper) {} + public function __construct( + private readonly IdToLocationDataTransformer $idToLocationDataTransformer, + ) {} public function buildForm(FormBuilderInterface $builder, array $options) { @@ -63,9 +61,6 @@ class EventType extends AbstractType ->add('moderator', PickUserDynamicType::class, [ 'label' => 'Pick a moderator', ]) - ->add('location', PickUserLocationType::class, [ - 'label' => 'event.fields.location', - ]) ->add('comment', CommentType::class, [ 'label' => 'Comment', 'required' => false, @@ -85,6 +80,10 @@ class EventType extends AbstractType 'label' => 'event.fields.organizationCost', 'help' => 'event.form.organisationCost_help', ]); + + $builder->add('location', HiddenType::class) + ->get('location') + ->addModelTransformer($this->idToLocationDataTransformer); } public function configureOptions(OptionsResolver $resolver) @@ -103,6 +102,7 @@ class EventType extends AbstractType */ public function getBlockPrefix() { - return 'chill_eventbundle_event'; + // as the js shares some hardcoded items from the activity bundle, we have to rewrite block prefix + return 'chill_activitybundle_activity'; } } diff --git a/src/Bundle/ChillEventBundle/Resources/public/vuejs/App.vue b/src/Bundle/ChillEventBundle/Resources/public/vuejs/App.vue new file mode 100644 index 000000000..89158d742 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Resources/public/vuejs/App.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/Bundle/ChillEventBundle/Resources/public/vuejs/index.js b/src/Bundle/ChillEventBundle/Resources/public/vuejs/index.js new file mode 100644 index 000000000..e0be75aaf --- /dev/null +++ b/src/Bundle/ChillEventBundle/Resources/public/vuejs/index.js @@ -0,0 +1,6 @@ +import { createApp } from "vue"; + +import App from "./App.vue"; +import store from "./store"; + +createApp(App).use(store).mount("#event"); diff --git a/src/Bundle/ChillEventBundle/Resources/public/vuejs/store.js b/src/Bundle/ChillEventBundle/Resources/public/vuejs/store.js new file mode 100644 index 000000000..ddfa3d512 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Resources/public/vuejs/store.js @@ -0,0 +1,79 @@ +import "es6-promise/auto"; +import { createStore } from "vuex"; + +import prepareLocations from "ChillActivityAssets/vuejs/Activity/store.locations"; +import {whoami} from "ChillMainAssets/lib/api/user"; +import {mapEntity} from "ChillCalendarAssets/vuejs/Calendar/store/utils"; +import {postLocation} from "ChillActivityAssets/vuejs/Activity/api"; + +const debug = process.env.NODE_ENV !== "production"; + +const store = createStore({ + strict: debug, + state: { + activity: window.entity, // activity is the event entity in this case (re-using component from activity bundle) + currentEvent: null, + availableLocations: [], + me: null, + }, + getters: { + + }, + actions: { + addAvailableLocationGroup({ commit }, payload) { + commit("addAvailableLocationGroup", payload); + }, + updateLocation({ commit }, value) { + // console.log("### action: updateLocation", value); + let hiddenLocation = document.getElementById( + "chill_activitybundle_activity_location", + ); + if (value.onthefly) { + const body = { + type: "location", + name: + value.name === "__AccompanyingCourseLocation__" ? null : value.name, + locationType: { + id: value.locationType.id, + type: "location-type", + }, + }; + if (value.address.id) { + Object.assign(body, { + address: { + id: value.address.id, + }, + }); + } + postLocation(body) + .then((location) => (hiddenLocation.value = location.id)) + .catch((err) => { + console.log(err.message); + }); + } else { + hiddenLocation.value = value.id; + } + commit("updateLocation", value); + }, + }, + mutations: { + setWhoAmiI(state, me) { + state.me = me; + }, + addAvailableLocationGroup(state, group) { + state.availableLocations.push(group); + }, + updateLocation(state, value) { + // console.log("### mutation: updateLocation", value); + state.activity.location = value; + }, + } +}); + +whoami().then((me) => { + store.commit("setWhoAmiI", me); +}); + +prepareLocations(store); + +export default store; diff --git a/src/Bundle/ChillEventBundle/Resources/views/Event/new.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Event/new.html.twig index 898af74c1..279bf4588 100644 --- a/src/Bundle/ChillEventBundle/Resources/views/Event/new.html.twig +++ b/src/Bundle/ChillEventBundle/Resources/views/Event/new.html.twig @@ -1,19 +1,19 @@ -{% extends '@ChillEvent/layout.html.twig' %} {% block js %} -{{ encore_entry_script_tags("mod_async_upload") }} -{{ encore_entry_script_tags("mod_pickentity_type") }} +{% extends '@ChillEvent/layout.html.twig' %} -{% endblock %} {% block css %} +{% block css %} {{ encore_entry_link_tags("mod_async_upload") }} {{ encore_entry_link_tags("mod_pickentity_type") }} +{{ encore_entry_link_tags('vue_event') }} +{% endblock %} -{% endblock %} {% block title 'Event creation'|trans %} {% block event_content --%} +{% block title 'Event creation'|trans %} + +{% block event_content -%}

    {{ "Event creation" | trans }}

    {{ form_start(form) }} {{ form_errors(form) }} -{# {{ form_row(form.circle) }}#} {{ form_row(form.name) }} {{ form_row(form.circle) }} {{ form_row(form.date) }} @@ -21,6 +21,7 @@ {{ form_row(form.themes) }} {{ form_row(form.moderator) }} {{ form_row(form.location) }} +
    {{ form_row(form.organizationCost) }} {{ form_row(form.comment) }} {{ form_row(form.documents) }} @@ -42,5 +43,18 @@ {{ form_end(form) }} + +
    +
    {% endblock %} + +{% block js %} + {{ encore_entry_script_tags("mod_async_upload") }} + {{ encore_entry_script_tags("mod_pickentity_type") }} + {{ encore_entry_script_tags('vue_event') }} + +{% endblock %} diff --git a/src/Bundle/ChillEventBundle/chill.webpack.config.js b/src/Bundle/ChillEventBundle/chill.webpack.config.js index e2c1e14bc..3f13a7773 100644 --- a/src/Bundle/ChillEventBundle/chill.webpack.config.js +++ b/src/Bundle/ChillEventBundle/chill.webpack.config.js @@ -1,3 +1,10 @@ module.exports = function (encore, entries) { entries.push(__dirname + "/Resources/public/chill/index.js"); + + encore.addEntry( + "vue_event", + __dirname + "/Resources/public/vuejs/index.js", + ); }; + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue index 103849e3a..923defdeb 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue @@ -21,10 +21,10 @@ > @@ -43,7 +43,7 @@ @@ -62,13 +62,13 @@ > @@ -85,10 +85,10 @@ > @@ -108,17 +108,17 @@ @@ -139,7 +139,7 @@ > @@ -171,10 +171,10 @@ > @@ -193,10 +193,10 @@ @@ -216,13 +216,13 @@ @@ -244,9 +244,16 @@ import { postPostalCode, } from "../api"; import { - postAddressToPerson, - postAddressToHousehold, -} from "ChillPersonAssets/vuejs/_api/AddAddress.js"; + CREATE_A_NEW_ADDRESS, + ADDRESS_LOADING, + ACTIVITY_CREATE_ADDRESS, + ACTIVITY_EDIT_ADDRESS, + CANCEL, + SAVE, + PREVIOUS, + NEXT, + trans, +} from "translator"; import ShowPane from "./ShowPane.vue"; import SuggestPane from "./SuggestPane.vue"; import EditPane from "./EditPane.vue"; @@ -254,6 +261,17 @@ import DatePane from "./DatePane.vue"; export default { name: "AddAddress", + setup() { + return { + trans, + CREATE_A_NEW_ADDRESS, + ADDRESS_LOADING, + CANCEL, + SAVE, + PREVIOUS, + NEXT + }; + }, props: ["context", "options", "addressChangedCallback"], components: { Modal, @@ -373,9 +391,11 @@ export default { (this.options.title.edit !== null || this.options.title.create !== null) ) { + console.log('this.options.title', this.options.title) + return this.context.edit - ? this.options.title.edit - : this.options.title.create; + ? ACTIVITY_EDIT_ADDRESS + : ACTIVITY_CREATE_ADDRESS; } return this.context.edit ? this.defaultz.title.edit diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue index 4b59efbc6..0be4b7809 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue @@ -1,6 +1,6 @@ From 76433e251252927923581a5d41960c865e0b6e2c Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 28 Aug 2025 13:49:45 +0200 Subject: [PATCH 47/60] Fix incorrect parameter name in event details link --- .changes/unreleased/Fixed-20250828-134939.yaml | 6 ++++++ .../Resources/views/Event/listByPerson.html.twig | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/Fixed-20250828-134939.yaml diff --git a/.changes/unreleased/Fixed-20250828-134939.yaml b/.changes/unreleased/Fixed-20250828-134939.yaml new file mode 100644 index 000000000..8a6f92514 --- /dev/null +++ b/.changes/unreleased/Fixed-20250828-134939.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: Fix incorrect parameter name in event details link +time: 2025-08-28T13:49:39.087943549+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/src/Bundle/ChillEventBundle/Resources/views/Event/listByPerson.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Event/listByPerson.html.twig index 9096279b9..bfa089c0a 100644 --- a/src/Bundle/ChillEventBundle/Resources/views/Event/listByPerson.html.twig +++ b/src/Bundle/ChillEventBundle/Resources/views/Event/listByPerson.html.twig @@ -53,7 +53,7 @@ {% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %} {% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %} - From ea06a96f919c183397f5a584ecc6db3434fcbf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 1 Sep 2025 08:05:11 +0000 Subject: [PATCH 48/60] Add external identifiers for person, editable in edit form, with minimal features associated --- .../unreleased/Feature-20250901-094055.yaml | 6 + .gitignore | 3 + .../CustomFieldsDefaultGroupRepository.php | 29 ++++ .../config/services.yaml | 4 + .../Resources/public/chill/chillmain.scss | 9 +- .../public/chill/scss/render_box.scss | 2 - .../ChillPersonBundle/ChillPersonBundle.php | 3 + .../Controller/PersonController.php | 51 ------ .../Controller/PersonEditController.php | 79 ++++++++++ .../DependencyInjection/Configuration.php | 18 +++ .../Entity/Identifier/PersonIdentifier.php | 83 ++++++++++ .../Identifier/PersonIdentifierDefinition.php | 107 +++++++++++++ .../ChillPersonBundle/Entity/Person.php | 47 ++++++ .../PersonIdentifiersDataMapper.php | 73 +++++++++ .../Form/PersonIdentifiersType.php | 48 ++++++ .../ChillPersonBundle/Form/PersonType.php | 33 ++-- .../Exception/EngineNotFoundException.php | 20 +++ ...nIdentifierDefinitionNotFoundException.php | 20 +++ .../Exception/UnexpectedTypeException.php | 20 +++ .../Identifier/StringIdentifier.php | 41 +++++ .../PersonIdentifierEngineInterface.php | 27 ++++ .../PersonIdentifierManager.php | 65 ++++++++ .../PersonIdentifierManagerInterface.php | 26 ++++ .../PersonIdentifierWorker.php | 49 ++++++ .../Rendering/PersonIdRendering.php | 65 ++++++++ .../Rendering/PersonIdRenderingInterface.php | 19 +++ .../PersonIdRenderingTwigExtension.php | 31 ++++ .../PersonIdentifierEntityRender.php | 41 +++++ .../PersonIdentifierDefinitionRepository.php | 32 ++++ .../Resources/public/chill/chillperson.scss | 5 - .../views/AccompanyingCourse/banner.html.twig | 2 +- .../Resources/views/Entity/person.html.twig | 17 +- .../Resources/views/Person/edit.html.twig | 28 +++- .../Resources/views/Person/view.html.twig | 61 +++----- .../Templating/Entity/PersonRender.php | 6 +- .../Rendering/PersonIdRenderingTest.php | 145 ++++++++++++++++++ .../ChillPersonBundle/config/services.yaml | 13 ++ .../migrations/Version20250822123819.php | 68 ++++++++ .../translations/messages+intl-icu.fr.yaml | 3 + .../translations/messages.fr.yml | 3 + 40 files changed, 1274 insertions(+), 128 deletions(-) create mode 100644 .changes/unreleased/Feature-20250901-094055.yaml create mode 100644 src/Bundle/ChillCustomFieldsBundle/EntityRepository/CustomFieldsDefaultGroupRepository.php create mode 100644 src/Bundle/ChillPersonBundle/Controller/PersonEditController.php create mode 100644 src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifier.php create mode 100644 src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifierDefinition.php create mode 100644 src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php create mode 100644 src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/Exception/EngineNotFoundException.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/Exception/PersonIdentifierDefinitionNotFoundException.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/Exception/UnexpectedTypeException.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManager.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManagerInterface.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRendering.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRenderingInterface.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRenderingTwigExtension.php create mode 100644 src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdentifierEntityRender.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Identifier/PersonIdentifierDefinitionRepository.php create mode 100644 src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Rendering/PersonIdRenderingTest.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20250822123819.php diff --git a/.changes/unreleased/Feature-20250901-094055.yaml b/.changes/unreleased/Feature-20250901-094055.yaml new file mode 100644 index 000000000..b240a1bfc --- /dev/null +++ b/.changes/unreleased/Feature-20250901-094055.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Add external identifier for a Person +time: 2025-09-01T09:40:55.990365093+02:00 +custom: + Issue: "64" + SchemaChange: Add columns or tables diff --git a/.gitignore b/.gitignore index 88458b64e..be72f296f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ migrations/* templates/* translations/* +# we allow developers to add customization on their installation, without commiting it +config/packages/dev/* + ###> symfony/framework-bundle ### /.env.local /.env.local.php diff --git a/src/Bundle/ChillCustomFieldsBundle/EntityRepository/CustomFieldsDefaultGroupRepository.php b/src/Bundle/ChillCustomFieldsBundle/EntityRepository/CustomFieldsDefaultGroupRepository.php new file mode 100644 index 000000000..a336970dc --- /dev/null +++ b/src/Bundle/ChillCustomFieldsBundle/EntityRepository/CustomFieldsDefaultGroupRepository.php @@ -0,0 +1,29 @@ +findOneBy(['entity' => $className]); + } +} diff --git a/src/Bundle/ChillCustomFieldsBundle/config/services.yaml b/src/Bundle/ChillCustomFieldsBundle/config/services.yaml index d27323a0a..bdc447996 100644 --- a/src/Bundle/ChillCustomFieldsBundle/config/services.yaml +++ b/src/Bundle/ChillCustomFieldsBundle/config/services.yaml @@ -127,3 +127,7 @@ services: factory: ["@doctrine", getRepository] arguments: - "Chill\\CustomFieldsBundle\\Entity\\CustomFieldLongChoice\\Option" + + Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository: + autowire: true + autoconfigure: true diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss index 1dc56dded..bb570f378 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss @@ -170,13 +170,14 @@ div.banner { font-weight: lighter; font-size: 50%; margin-left: 0.5em; - &:before { content: '(n°'; } - &:after { content: ')'; } + + &.same-size { + font-size: unset; + font-weight: unset; + } } span.age { margin-left: 0.5em; - &:before { content: '('; } - &:after { content: ')'; } } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss index 57fa17648..7e854d1ca 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss @@ -44,8 +44,6 @@ section.chill-entity { margin-left: 0.5em; } span.id-number { - &:before { content: '(n°'; } - &:after { content: ')'; } } } p.moreinfo {} diff --git a/src/Bundle/ChillPersonBundle/ChillPersonBundle.php b/src/Bundle/ChillPersonBundle/ChillPersonBundle.php index e97ccecdf..2d567aaa2 100644 --- a/src/Bundle/ChillPersonBundle/ChillPersonBundle.php +++ b/src/Bundle/ChillPersonBundle/ChillPersonBundle.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterfac use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface; use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass; use Chill\PersonBundle\Export\Helper\CustomizeListPersonHelperInterface; +use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface; use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface; use Chill\PersonBundle\Widget\PersonListWidgetFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -38,5 +39,7 @@ class ChillPersonBundle extends Bundle ->addTag('chill_person.list_person_customizer'); $container->registerForAutoconfiguration(NotificationFlagProviderInterface::class) ->addTag('chill_main.notification_flag_provider'); + $container->registerForAutoconfiguration(PersonIdentifierEngineInterface::class) + ->addTag('chill_person.person_identifier_engine'); } } diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonController.php b/src/Bundle/ChillPersonBundle/Controller/PersonController.php index 9f35e1b5c..4698373f1 100644 --- a/src/Bundle/ChillPersonBundle/Controller/PersonController.php +++ b/src/Bundle/ChillPersonBundle/Controller/PersonController.php @@ -17,7 +17,6 @@ use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Form\CreationPersonType; -use Chill\PersonBundle\Form\PersonType; use Chill\PersonBundle\Privacy\PrivacyEvent; use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Search\SimilarPersonMatcher; @@ -49,56 +48,6 @@ final class PersonController extends AbstractController private readonly EntityManagerInterface $em, ) {} - #[Route(path: '/{_locale}/person/{person_id}/general/edit', name: 'chill_person_general_edit')] - public function editAction(int $person_id, Request $request) - { - $person = $this->_getPerson($person_id); - - if (null === $person) { - throw $this->createNotFoundException(); - } - - $this->denyAccessUnlessGranted( - 'CHILL_PERSON_UPDATE', - $person, - 'You are not allowed to edit this person' - ); - - $form = $this->createForm( - PersonType::class, - $person, - [ - 'cFGroup' => $this->getCFGroup(), - ] - ); - - $form->handleRequest($request); - - if ($form->isSubmitted() && !$form->isValid()) { - $this->get('session') - ->getFlashBag()->add('error', $this->translator - ->trans('This form contains errors')); - } elseif ($form->isSubmitted() && $form->isValid()) { - $this->em->flush(); - - $this->get('session')->getFlashBag() - ->add( - 'success', - $this->translator - ->trans('The person data has been updated') - ); - - return $this->redirectToRoute('chill_person_view', [ - 'person_id' => $person->getId(), - ]); - } - - return $this->render( - '@ChillPerson/Person/edit.html.twig', - ['person' => $person, 'form' => $form->createView()] - ); - } - public function getCFGroup() { $cFGroup = null; diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonEditController.php b/src/Bundle/ChillPersonBundle/Controller/PersonEditController.php new file mode 100644 index 000000000..be5b87bc3 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/PersonEditController.php @@ -0,0 +1,79 @@ +security->isGranted(PersonVoter::UPDATE, $person)) { + throw new AccessDeniedHttpException('You are not allowed to edit this person.'); + } + + $form = $this->formFactory->create( + PersonType::class, + $person, + ['cFGroup' => $this->customFieldsDefaultGroupRepository->findOneByEntity(Person::class)?->getCustomFieldsGroup()] + ); + + $form->handleRequest($request); + + if ($form->isSubmitted() && !$form->isValid()) { + $session + ->getFlashBag()->add('error', new TranslatableMessage('This form contains errors')); + } elseif ($form->isSubmitted() && $form->isValid()) { + $this->entityManager->flush(); + + $session->getFlashBag()->add('success', new TranslatableMessage('The person data has been updated')); + + return new RedirectResponse( + $this->urlGenerator->generate('chill_person_view', ['person_id' => $person->getId()]) + ); + } + + return new Response($this->twig->render('@ChillPerson/Person/edit.html.twig', [ + 'form' => $form->createView(), + 'person' => $person, + ])); + } +} diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php index 12c8b4c5b..da87ae050 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php @@ -110,6 +110,24 @@ class Configuration implements ConfigurationInterface ->end() ->end() // children for 'person_fields', parent = array 'person_fields' ->end() // person_fields, parent = children of root + ->arrayNode('person_render') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('id_content_text') + ->defaultValue('n°[[ person_id ]]') + ->info( + <<<'EOF' + The way we display the person's id. Variables availables: "[[ person_id ]]", or, for person's + identifier: "[[ identifier_xx ]]" where xx is the identifier's definition's id. + + There are also conditions available: "[[ if:identifier_yy ]] [[ identifier_yy ]] [[ endif:identifier_yy ]]" + + Take care of keeping exactly one space between "[[" and the placeholder's content, and exactly one space before "]]" + EOF + ) + ->end() + ->end() // end of person_render's children + ->end() // end of person_render ->arrayNode('household_fields') ->canBeDisabled() ->children() diff --git a/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifier.php b/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifier.php new file mode 100644 index 000000000..f0dea00b4 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifier.php @@ -0,0 +1,83 @@ + '[]', 'jsonb' => true])] + private array $value = []; + + #[ORM\Column(name: 'canonical', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '[]'])] + private string $canonical = ''; + + public function __construct( + #[ORM\ManyToOne(targetEntity: PersonIdentifierDefinition::class)] + #[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + private PersonIdentifierDefinition $definition, + ) {} + + public function getId(): ?int + { + return $this->id; + } + + public function setPerson(?Person $person): self + { + $this->person = $person; + + return $this; + } + + public function getPerson(): Person + { + return $this->person; + } + + public function getValue(): array + { + return $this->value; + } + + public function setValue(array $value): void + { + $this->value = $value; + } + + public function getCanonical(): string + { + return $this->canonical; + } + + public function setCanonical(string $canonical): void + { + $this->canonical = $canonical; + } + + public function getDefinition(): PersonIdentifierDefinition + { + return $this->definition; + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifierDefinition.php b/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifierDefinition.php new file mode 100644 index 000000000..6d6112569 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Identifier/PersonIdentifierDefinition.php @@ -0,0 +1,107 @@ + true])] + private bool $active = true; + + public function __construct( + #[ORM\Column(name: 'label', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] + private array $label, + #[ORM\Column(name: 'engine', type: \Doctrine\DBAL\Types\Types::STRING, length: 100)] + private string $engine, + #[ORM\Column(name: 'is_searchable', type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])] + private bool $isSearchable = false, + #[ORM\Column(name: 'is_editable_by_users', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => false])] + private bool $isEditableByUsers = false, + #[ORM\Column(name: 'data', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])] + private array $data = [], + ) {} + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): array + { + return $this->label; + } + + public function setLabel(array $label): void + { + $this->label = $label; + } + + public function getEngine(): string + { + return $this->engine; + } + + public function setEngine(string $engine): void + { + $this->engine = $engine; + } + + public function isSearchable(): bool + { + return $this->isSearchable; + } + + public function setIsSearchable(bool $isSearchable): void + { + $this->isSearchable = $isSearchable; + } + + public function isEditableByUsers(): bool + { + return $this->isEditableByUsers; + } + + public function setIsEditableByUsers(bool $isEditableByUsers): void + { + $this->isEditableByUsers = $isEditableByUsers; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function getData(): array + { + return $this->data; + } + + public function setData(array $data): void + { + $this->data = $data; + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index 5d57f11af..aa78774b6 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -31,6 +31,7 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; +use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; use Chill\PersonBundle\Entity\Person\PersonCenterCurrent; use Chill\PersonBundle\Entity\Person\PersonCenterHistory; use Chill\PersonBundle\Entity\Person\PersonCurrentAddress; @@ -271,6 +272,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI #[ORM\GeneratedValue(strategy: 'AUTO')] private ?int $id = null; + #[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonIdentifier::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + private Collection $identifiers; + /** * The person's last name. */ @@ -418,6 +422,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI $this->resources = new ArrayCollection(); $this->centerHistory = new ArrayCollection(); $this->signatures = new ArrayCollection(); + $this->identifiers = new ArrayCollection(); } public function __toString(): string @@ -498,6 +503,24 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $this; } + public function addIdentifier(PersonIdentifier $identifier): self + { + if (!$this->identifiers->contains($identifier)) { + $this->identifiers[] = $identifier; + $identifier->setPerson($this); + } + + return $this; + } + + public function removeIdentifier(PersonIdentifier $identifier): self + { + $this->identifiers->removeElement($identifier); + $identifier->setPerson(null); + + return $this; + } + public function removeSignature(EntityWorkflowStepSignature $signature): self { $this->signatures->removeElement($signature); @@ -1129,6 +1152,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $this->id; } + /** + * @return ReadableCollection + */ + public function getIdentifiers(): ReadableCollection + { + return $this->identifiers; + } + /** * @return string */ @@ -1262,6 +1293,22 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $this->spokenLanguages; } + public function addSpokenLanguage(Language $language): self + { + if (!$this->spokenLanguages->contains($language)) { + $this->spokenLanguages->add($language); + } + + return $this; + } + + public function removeSpokenLanguage(Language $language): self + { + $this->spokenLanguages->removeElement($language); + + return $this; + } + public function getUpdatedAt(): ?\DateTimeInterface { return $this->updatedAt; diff --git a/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php b/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php new file mode 100644 index 000000000..eea151865 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Form/DataMapper/PersonIdentifiersDataMapper.php @@ -0,0 +1,73 @@ + $formsByKey */ + $formsByKey = iterator_to_array($forms); + + foreach ($this->identifierManager->getWorkers() as $worker) { + if (!$worker->getDefinition()->isEditableByUsers()) { + continue; + } + $form = $formsByKey['identifier_'.$worker->getDefinition()->getId()]; + $identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition()->getId() === $identifier->getId()); + if (null === $identifier) { + $identifier = new PersonIdentifier($worker->getDefinition()); + } + $form->setData($identifier->getValue()); + } + } + + public function mapFormsToData(\Traversable $forms, &$viewData): void + { + if (!$viewData instanceof Collection) { + throw new UnexpectedTypeException($viewData, Collection::class); + } + + foreach ($forms as $name => $form) { + $identifierId = (int) substr((string) $name, 11); + $identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $identifier->getId() === $identifierId); + $definition = $this->identifierDefinitionRepository->find($identifierId); + if (null === $identifier) { + $identifier = new PersonIdentifier($definition); + $viewData->add($identifier); + } + if (!$identifier->getDefinition()->isEditableByUsers()) { + continue; + } + + $worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($definition); + $identifier->setValue($form->getData()); + $identifier->setCanonical($worker->canonicalizeValue($identifier->getValue())); + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php b/src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php new file mode 100644 index 000000000..ea077f626 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Form/PersonIdentifiersType.php @@ -0,0 +1,48 @@ +identifierManager->getWorkers() as $worker) { + if (!$worker->getDefinition()->isEditableByUsers()) { + continue; + } + + $subBuilder = $builder->create( + 'identifier_'.$worker->getDefinition()->getId(), + options: [ + 'compound' => true, + 'label' => $this->translatableStringHelper->localize($worker->getDefinition()->getLabel()), + ] + ); + $worker->buildForm($subBuilder); + $builder->add($subBuilder); + } + + $builder->setDataMapper($this->identifiersDataMapper); + } +} diff --git a/src/Bundle/ChillPersonBundle/Form/PersonType.php b/src/Bundle/ChillPersonBundle/Form/PersonType.php index 21d56dde7..09ab04b01 100644 --- a/src/Bundle/ChillPersonBundle/Form/PersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/PersonType.php @@ -72,8 +72,8 @@ class PersonType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('firstName') - ->add('lastName') + ->add('firstName', TextType::class, ['empty_data' => '']) + ->add('lastName', TextType::class, ['empty_data' => '']) ->add('birthdate', ChillDateType::class, [ 'required' => false, ]) @@ -101,7 +101,7 @@ class PersonType extends AbstractType if ('visible' === $this->config['memo']) { $builder - ->add('memo', ChillTextareaType::class, ['required' => false]); + ->add('memo', ChillTextareaType::class, ['required' => false, 'empty_data' => '']); } if ('visible' === $this->config['employment_status']) { @@ -118,6 +118,7 @@ class PersonType extends AbstractType $builder->add('placeOfBirth', TextType::class, [ 'required' => false, 'attr' => ['style' => 'text-transform: uppercase;'], + 'empty_data' => '', ]); $builder->get('placeOfBirth')->addModelTransformer(new CallbackTransformer( @@ -127,7 +128,9 @@ class PersonType extends AbstractType } if ('visible' === $this->config['contact_info']) { - $builder->add('contactInfo', ChillTextareaType::class, ['required' => false]); + $builder->add('contactInfo', ChillTextareaType::class, [ + 'required' => false, 'empty_data' => '', 'label' => 'Notes on contact information', + ]); } if ('visible' === $this->config['phonenumber']) { @@ -152,12 +155,12 @@ class PersonType extends AbstractType 'required' => false, ] ) - ->add('acceptSMS', CheckboxType::class, [ + ->add('acceptSms', CheckboxType::class, [ 'required' => false, ]); } - $builder->add('otherPhoneNumbers', ChillCollectionType::class, [ + $builder->add('otherPhonenumbers', ChillCollectionType::class, [ 'entry_type' => PersonPhoneType::class, 'button_add_label' => 'Add new phone', 'button_remove_label' => 'Remove phone', @@ -173,12 +176,12 @@ class PersonType extends AbstractType if ('visible' === $this->config['email']) { $builder - ->add('email', EmailType::class, ['required' => false]); + ->add('email', EmailType::class, ['required' => false, 'empty_data' => '']); } if ('visible' === $this->config['acceptEmail']) { $builder - ->add('acceptEmail', CheckboxType::class, ['required' => false]); + ->add('acceptEmail', CheckboxType::class, ['required' => false, 'empty_data' => '']); } if ('visible' === $this->config['country_of_birth']) { @@ -222,6 +225,10 @@ class PersonType extends AbstractType ]); } + $builder->add('identifiers', PersonIdentifiersType::class, [ + 'by_reference' => false, + ]); + if ($options['cFGroup']) { $builder ->add( @@ -232,10 +239,7 @@ class PersonType extends AbstractType } } - /** - * @param OptionsResolverInterface $resolver - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Person::class, @@ -251,10 +255,7 @@ class PersonType extends AbstractType ); } - /** - * @return string - */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'chill_personbundle_person'; } diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Exception/EngineNotFoundException.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Exception/EngineNotFoundException.php new file mode 100644 index 000000000..a3c7cf593 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Exception/EngineNotFoundException.php @@ -0,0 +1,20 @@ +add('content', TextType::class, ['label' => false]); + } + + public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string + { + return $identifier?->getValue()['content'] ?? ''; + } +} diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php new file mode 100644 index 000000000..6c75f263e --- /dev/null +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierEngineInterface.php @@ -0,0 +1,27 @@ + + */ + public function getWorkers(): array + { + $workers = []; + foreach ($this->personIdentifierDefinitionRepository->findByActive() as $definition) { + try { + $worker = $this->getEngine($definition->getEngine()); + } catch (EngineNotFoundException) { + continue; + } + + $workers[] = new PersonIdentifierWorker($worker, $definition); + + } + + return $workers; + } + + public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker + { + return new PersonIdentifierWorker($this->getEngine($personIdentifierDefinition->getEngine()), $personIdentifierDefinition); + } + + /** + * @throw EngineNotFoundException + */ + private function getEngine(string $name): PersonIdentifierEngineInterface + { + foreach ($this->engines as $engine) { + if ($engine->getName() === $name) { + return $engine; + } + } + + throw new EngineNotFoundException($name); + } +} diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManagerInterface.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManagerInterface.php new file mode 100644 index 000000000..9bec7d1fd --- /dev/null +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierManagerInterface.php @@ -0,0 +1,26 @@ + + */ + public function getWorkers(): array; + + public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker; +} diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php new file mode 100644 index 000000000..94702b8eb --- /dev/null +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/PersonIdentifierWorker.php @@ -0,0 +1,49 @@ +identifierEngine; + } + + public function getDefinition(): PersonIdentifierDefinition + { + return $this->definition; + } + + public function buildForm(FormBuilderInterface $builder): void + { + $this->identifierEngine->buildForm($builder, $this->definition); + } + + public function canonicalizeValue(array $value): ?string + { + return $this->identifierEngine->canonicalizeValue($value, $this->definition); + } + + public function renderAsString(?PersonIdentifier $identifier): string + { + return $this->identifierEngine->renderAsString($identifier, $this->definition); + } +} diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRendering.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRendering.php new file mode 100644 index 000000000..c529b3aba --- /dev/null +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRendering.php @@ -0,0 +1,65 @@ +idContentText = $parameterBag->get('chill_person')['person_render']['id_content_text']; + } + + public function renderPersonId(Person $person): string + { + $args = [ + '[[ person_id ]]' => $person->getId(), + ]; + + foreach ($person->getIdentifiers() as $identifier) { + if (!$identifier->getDefinition()->isActive()) { + continue; + } + + $key = 'identifier_'.$identifier->getDefinition()->getId(); + + $args + += [ + "[[ {$key} ]]" => $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition()) + ->renderAsString($identifier), + "[[ if:{$key} ]]" => '', + "[[ endif:{$key} ]]" => '', + ]; + // we remove the eventual conditions + + + } + + $rendered = strtr($this->idContentText, $args); + + // Delete the conditions which are not met, for instance: + // [[ if:identifier_99 ]] ... [[ endif:identifier_99 ]] + // this match the same dumber for opening and closing of the condition + return preg_replace( + '/\[\[\s*if:identifier_(\d+)\s*\]\].*?\[\[\s*endif:identifier_\1\s*\]\]/s', + '', + $rendered + ); + } +} diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRenderingInterface.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRenderingInterface.php new file mode 100644 index 000000000..831dcc57d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdRenderingInterface.php @@ -0,0 +1,19 @@ + $this->personIdRendering->renderPersonId($person) + ), + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdentifierEntityRender.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdentifierEntityRender.php new file mode 100644 index 000000000..e84941ffd --- /dev/null +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Rendering/PersonIdentifierEntityRender.php @@ -0,0 +1,41 @@ + + */ +final readonly class PersonIdentifierEntityRender implements ChillEntityRenderInterface +{ + public function __construct(private PersonIdentifierManagerInterface $identifierManager) {} + + public function renderBox(mixed $entity, array $options): string + { + return $this->renderString($entity, $options); + } + + public function renderString(mixed $entity, array $options): string + { + $worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($entity->getDefinition()); + + return $worker->renderAsString($entity); + } + + public function supports(object $entity, array $options): bool + { + return $entity instanceof PersonIdentifier; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/Identifier/PersonIdentifierDefinitionRepository.php b/src/Bundle/ChillPersonBundle/Repository/Identifier/PersonIdentifierDefinitionRepository.php new file mode 100644 index 000000000..33b531e5a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Identifier/PersonIdentifierDefinitionRepository.php @@ -0,0 +1,32 @@ + + */ +class PersonIdentifierDefinitionRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $managerRegistry) + { + parent::__construct($managerRegistry, PersonIdentifierDefinition::class); + } + + public function findByActive(): array + { + return $this->findBy(['active' => true]); + } +} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/chillperson.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/chillperson.scss index afa163cf2..846ba008e 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/chillperson.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/chillperson.scss @@ -281,11 +281,6 @@ abbr.referrer { // still used ? font-style: italic; } -.created-updated { - border: 1px solid black; - padding: 10px; -} - /// Masonry blocs on AccompanyingCourse resume page div#dashboards { div.mbloc { diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/banner.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/banner.html.twig index bb039b5a0..fb1a81dfe 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/banner.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/banner.html.twig @@ -8,7 +8,7 @@

    {{ 'Accompanying Course'|trans }} - {{ accompanyingCourse.id }} + ({{ 'accompanying_period.number'|trans({ 'id': accompanyingCourse.id}) }})

    diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig index 380a17fa2..d8a930b40 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Entity/person.html.twig @@ -78,11 +78,6 @@ {%- if options['addEntity'] -%} {{ 'Person'|trans }} {%- endif -%} - {%- if options['addId'] -%} - - {{ person.id|upper -}} - - {%- endif -%} {%- if options['addInfo'] -%}

    @@ -99,6 +94,12 @@ {%- if options['addAge'] -%}  {{ 'years_old'|trans({ 'age': person.age }) }} {%- endif -%} + {%- if options['addId'] -%} + {%- set personId = person|chill_person_id_render_text %} + + ({{ personId }}) + + {%- endif -%} {%- elseif person.birthdate is not null -%}

    {%- endif -%} {#- tricks to remove easily whitespace after template -#} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/edit.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/edit.html.twig index cb2d867c4..fe5dde242 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/edit.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/edit.html.twig @@ -31,7 +31,7 @@ {% if form.memo is defined %}

    {{ 'Memo'|trans }}

    - {{ form_row(form.memo, {'label' : 'Memo'} ) }} + {{ form_widget(form.memo, {'label' : 'Memo'} ) }}
    {% endif %} @@ -85,15 +85,17 @@ {{ form_row(form.mobilenumber, {'label': 'Mobilenumber'}) }}
    - {{ form_row(form.acceptSMS, {'label' : 'Accept short text message ?'}) }} + {{ form_row(form.acceptSms, {'label' : 'Accept short text message ?'}) }}
    {%- endif -%} - {%- if form.otherPhoneNumbers is defined -%} - {{ form_widget(form.otherPhoneNumbers) }} - {{ form_errors(form.otherPhoneNumbers) }} + {%- if form.otherPhonenumbers is defined -%} + {{ form_widget(form.otherPhonenumbers) }} + {{ form_errors(form.otherPhonenumbers) }} {%- endif -%} {%- if form.contactInfo is defined -%} - {{ form_row(form.contactInfo, {'label': 'Notes on contact information'}) }} + {{ form_label(form.contactInfo) }} + {{ form_widget(form.contactInfo) }} + {{ form_errors(form.contactInfo) }} {%- endif -%} {%- endif -%} @@ -134,6 +136,20 @@ {%- endif -%} + {% if form.identifiers|length > 0 %} +
    +

    {{ 'person.Identifiers'|trans }}

    +
    + {% for f in form.identifiers %} + {{ form_row(f) }} + {% endfor %} +
    +
    + {% else %} + {{ form_widget(form.identifiers) }} + {% endif %} + + {{ form_rest(form) }}