diff --git a/src/Bundle/ChillMainBundle/Entity/Embeddable/PrivateCommentEmbeddable.php b/src/Bundle/ChillMainBundle/Entity/Embeddable/PrivateCommentEmbeddable.php index 0c2af22f7..5ada822c4 100644 --- a/src/Bundle/ChillMainBundle/Entity/Embeddable/PrivateCommentEmbeddable.php +++ b/src/Bundle/ChillMainBundle/Entity/Embeddable/PrivateCommentEmbeddable.php @@ -63,4 +63,28 @@ class PrivateCommentEmbeddable return $this; } + + /** + * Merges comments from the provided object into the current object. + * + * Identifies common user IDs between the current object's comments and the + * newComment's comments. If a user ID exists in both, their comments are + * concatenated with the provided separator. If a user ID exists only in the + * newComment, their comment is added to the current object directly. + * + * @param self $commentsToAppend the object containing the new comments to be merged + * @param string $separator the string used to separate concatenated comments + */ + public function concatenateComments(self $commentsToAppend, string $separator = "\n\n-----------\n\n"): void + { + $commonUserIds = array_intersect(array_keys($this->comments), array_keys($commentsToAppend->getComments())); + + foreach ($commentsToAppend->getComments() as $userId => $comment) { + if (in_array($userId, $commonUserIds, true)) { + $this->comments[$userId] = $this->comments[$userId].$separator.$commentsToAppend->getComments()[$userId]; + } else { + $this->comments[$userId] = $commentsToAppend->getComments()[$userId]; + } + } + } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts index b50bb5534..e8256b348 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts @@ -61,6 +61,9 @@ export interface ConflictHttpExceptionInterface /** * Generic api method that can be adapted to any fetch request + * + * This method is suitable make a single fetch. When performing a GET to fetch a list of elements, always consider pagination + * and use of the @link{fetchResults} method. */ export const makeFetch = ( method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE", diff --git a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig index 9f07b3aa0..033fdb342 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig @@ -281,10 +281,10 @@ {% endblock %} {% block pick_linked_entities_widget %} - -
+
+ {% endblock %} {% block pick_postal_code_widget %} diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/PrivateCommentEmbeddableTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/PrivateCommentEmbeddableTest.php new file mode 100644 index 000000000..359b194a8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/PrivateCommentEmbeddableTest.php @@ -0,0 +1,56 @@ +prophesize(User::class); + $userA->getId()->willReturn(1); + $userB = $this->prophesize(User::class); + $userB->getId()->willReturn(2); + $userC = $this->prophesize(User::class); + $userC->getId()->willReturn(3); + + $toKeep = new PrivateCommentEmbeddable(); + $toKeep->setCommentForUser($userA->reveal(), 'My comment for A'); + $toKeep->setCommentForUser($userB->reveal(), 'My comment for B'); + + $toDelete = new PrivateCommentEmbeddable(); + $toDelete->setCommentForUser($userC->reveal(), 'My comment for C'); + $toDelete->setCommentForUser($userB->reveal(), 'Another comment for B'); + + $toKeep->concatenateComments($toDelete, '----'); + + self::assertTrue($toKeep->hasCommentForUser($userA->reveal())); + self::assertEquals('My comment for A', $toKeep->getCommentForUser($userA->reveal())); + + self::assertTrue($toKeep->hasCommentForUser($userB->reveal())); + self::assertEquals('My comment for B----Another comment for B', $toKeep->getCommentForUser($userB->reveal())); + + self::assertTrue($toKeep->hasCommentForUser($userC->reveal())); + self::assertEquals('My comment for C', $toKeep->getCommentForUser($userC->reveal())); + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index ac7c7dec4..9212a96dc 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -314,7 +314,6 @@ final class AccompanyingCourseApiController extends ApiController $this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingPeriod); $works = $this->accompanyingPeriodWorkRepository->findBy(['accompanyingPeriod' => $accompanyingPeriod]); - dump($works); return $this->json($works, Response::HTTP_OK, [], ['groups' => ['read']]); } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluation.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluation.php index 748174e19..44713c476 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluation.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluation.php @@ -218,14 +218,6 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU public function setAccompanyingPeriodWork(?AccompanyingPeriodWork $accompanyingPeriodWork): AccompanyingPeriodWorkEvaluation { - if ( - $accompanyingPeriodWork instanceof AccompanyingPeriodWork - && $this->accompanyingPeriodWork instanceof AccompanyingPeriodWork - && $this->accompanyingPeriodWork->getId() !== $accompanyingPeriodWork->getId() - ) { - throw new \RuntimeException('Changing the accompanyingPeriodWork is not allowed'); - } - $this->accompanyingPeriodWork = $accompanyingPeriodWork; return $this; diff --git a/src/Bundle/ChillPersonBundle/Form/FindAccompanyingPeriodWorkType.php b/src/Bundle/ChillPersonBundle/Form/FindAccompanyingPeriodWorkType.php index 1f862353e..ab78cb541 100644 --- a/src/Bundle/ChillPersonBundle/Form/FindAccompanyingPeriodWorkType.php +++ b/src/Bundle/ChillPersonBundle/Form/FindAccompanyingPeriodWorkType.php @@ -24,8 +24,9 @@ class FindAccompanyingPeriodWorkType extends AbstractType { $builder ->add('acpw', PickLinkedAccompanyingPeriodWorkType::class, [ - 'label' => 'Accompanying period work', + 'label' => 'Social action', 'multiple' => false, + 'accompanyingPeriod' => $options['accompanyingPeriod'], ]) ->add('direction', HiddenType::class, [ 'data' => 'starting', diff --git a/src/Bundle/ChillPersonBundle/Form/Type/PickLinkedAccompanyingPeriodWorkType.php b/src/Bundle/ChillPersonBundle/Form/Type/PickLinkedAccompanyingPeriodWorkType.php index ad199f882..a2a282d19 100644 --- a/src/Bundle/ChillPersonBundle/Form/Type/PickLinkedAccompanyingPeriodWorkType.php +++ b/src/Bundle/ChillPersonBundle/Form/Type/PickLinkedAccompanyingPeriodWorkType.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Form\Type; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; @@ -19,29 +20,25 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class PickLinkedAccompanyingPeriodWorkType extends AbstractType { - public function __construct(private readonly NormalizerInterface $normalizer) {} - public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['multiple'] = $options['multiple']; $view->vars['types'] = ['acpw']; $view->vars['uniqid'] = uniqid('pick_acpw_dyn'); - $view->vars['suggested'] = []; $view->vars['as_id'] = true === $options['as_id'] ? '1' : '0'; $view->vars['submit_on_adding_new_entity'] = false; - - foreach ($options['suggested'] as $suggestion) { - $view->vars['suggested'][] = $this->normalizer->normalize($suggestion, 'json', ['groups' => 'read']); - } + $view->vars['pick-entities-type'] = 'acpw'; + $view->vars['attr']['data-accompanying-period-id'] = $options['accompanyingPeriod']->getId(); } public function configureOptions(OptionsResolver $resolver) { $resolver + ->setRequired('accompanyingPeriod') + ->setAllowedTypes('accompanyingPeriod', [AccompanyingPeriod::class]) ->setDefault('multiple', false) ->setAllowedTypes('multiple', ['bool']) ->setDefault('compound', false) - ->setDefault('suggested', []) ->setDefault('as_id', false) ->setAllowedTypes('as_id', ['bool']) ->setDefault('submit_on_adding_new_entity', false) diff --git a/src/Bundle/ChillPersonBundle/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.js b/src/Bundle/ChillPersonBundle/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.js deleted file mode 100644 index 1c7a5023b..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.js +++ /dev/null @@ -1,12 +0,0 @@ -import { createApp } from "vue"; -import AccompanyingPeriodWorkSelectorModal from "../../vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue"; - -document.addEventListener("DOMContentLoaded", () => { - const el = document.getElementById("linked-acpw-selector"); - if (el) { - const accompanyingPeriodId = el.dataset.accompanyingPeriod; - createApp(AccompanyingPeriodWorkSelectorModal, { - accompanyingPeriodId, - }).mount(el); - } -}); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.ts b/src/Bundle/ChillPersonBundle/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.ts new file mode 100644 index 000000000..0ac3b4d6f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.ts @@ -0,0 +1,47 @@ +import { createApp } from "vue"; +import AccompanyingPeriodWorkSelectorModal from "../../vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue"; +import {AccompanyingPeriodWork} from "../../types"; + +document.addEventListener("DOMContentLoaded", () => { + const elements = document.querySelectorAll('div[data-pick-entities-type="acpw"]'); + elements.forEach(el => { + const uniqid = el.dataset.inputUniqid; + + if (undefined === uniqid) { + throw 'Uniqid not found on this element'; + } + + const input = document.querySelector(`input[data-input-uniqid="${uniqid}"]`); + + if (null === input) { + throw 'Element with uniqid not found: ' + uniqid; + } + + const accompanyingPeriodIdAsString = input.dataset.accompanyingPeriodId; + + if (undefined === accompanyingPeriodIdAsString) { + throw 'accompanying period id not found'; + } + + const accompanyingPeriodId = Number.parseInt(accompanyingPeriodIdAsString); + + const app = createApp( + { + template: + '', + components: {AccompanyingPeriodWorkSelectorModal}, + data() { + return { accompanyingPeriodId }; + }, + methods: { + pickWork: function(payload: {work: AccompanyingPeriodWork}) { + console.log("payload", payload); + input.value = payload.work.id.toString(); + } + } + }); + + + app.mount(el); + }) +}); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/types.ts b/src/Bundle/ChillPersonBundle/Resources/public/types.ts index 5d1c40568..d5c896453 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/types.ts +++ b/src/Bundle/ChillPersonBundle/Resources/public/types.ts @@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument { } export interface AccompanyingPeriodWork { - id?: number; + id: number; accompanyingPeriod?: AccompanyingPeriod; accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[]; createdAt?: string; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue index 3a8e20016..3a289da4b 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue @@ -59,16 +59,22 @@ import { } from "translator"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; +type AccompanyingPeriodWorkSelectorModalProps = { + accompanyingPeriodId: number; +}; + const selectedAcpw = ref(null); const showModal = ref(false); const accompanyingPeriodWorks = ref([]); -const props = defineProps({ - accompanyingPeriodId: String, -}); +const props = defineProps(); + +const emit = defineEmits<{ + pickWork: [payload: {work: AccompanyingPeriodWork|null}], +}>(); onMounted(() => { if (props.accompanyingPeriodId) { - getAccompanyingPeriodWorks(parseInt(props.accompanyingPeriodId)); + getAccompanyingPeriodWorks(props.accompanyingPeriodId); } else { console.error("No accompanyingperiod id was given"); } @@ -85,19 +91,10 @@ const getAccompanyingPeriodWorks = (periodId: number) => { }); }; -watch(selectedAcpw, (newValue) => { - const inputField = document.getElementById( - "find_accompanying_period_work_acpw", - ) as HTMLInputElement; - if (inputField) { - inputField.value = String(newValue?.id); - } -}); - const openModal = () => (showModal.value = true); const closeModal = () => (showModal.value = false); const confirmSelection = () => { - selectedAcpw.value = selectedAcpw.value; + emit('pickWork', {work: selectedAcpw.value}); closeModal(); }; diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/_details.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/_details.html.twig index ed1a27cad..94007ca9c 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/_details.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/_details.html.twig @@ -1,9 +1,9 @@ {%- macro details(w, accompanyingCourse, options) -%} {% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { - 'displayAction': true, + 'displayAction': false, 'displayContent': 'short', 'displayFontSmall': true, 'itemBlocClass': '', - 'displayNotification': true + 'displayNotification': false } %} {% endmacro %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/assign_acpw_duplicate.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/assign_acpw_duplicate.html.twig index a08c46454..a8d2f7f07 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/assign_acpw_duplicate.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/assign_acpw_duplicate.html.twig @@ -4,10 +4,21 @@ {% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %} +{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %} + {% block content %}
-

{{ 'acpw_duplicate.Assign duplicate'|trans }}

+
+

{{ 'acpw_duplicate.to keep'|trans ~ ':' }}

+
+
+ {{ details.details(acpw, accompanyingCourse) }} +
+
+
+ +

{{ 'acpw_duplicate.Assign duplicate'|trans }}

{{ form_start(form) }} {%- if form.acpw is defined -%} {{ form_row(form.acpw) }} @@ -22,7 +33,7 @@
  • - +
  • @@ -32,9 +43,11 @@ {% endblock %} {% block js %} + {{ parent() }} {{ encore_entry_script_tags('mod_duplicate_selector') }} {% endblock %} {% block css %} + {{ parent() }} {{ encore_entry_link_tags('mod_duplicate_selector') }} {% endblock %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/confirm.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/confirm.html.twig index eb0833657..92d65b7c3 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/confirm.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriodWorkDuplicate/confirm.html.twig @@ -50,7 +50,7 @@ diff --git a/src/Bundle/ChillPersonBundle/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeService.php b/src/Bundle/ChillPersonBundle/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeService.php index 9627c86b9..6ad799c76 100644 --- a/src/Bundle/ChillPersonBundle/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeService.php +++ b/src/Bundle/ChillPersonBundle/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeService.php @@ -12,202 +12,93 @@ declare(strict_types=1); namespace Chill\PersonBundle\Service\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; -use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory; -use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; +/** + * Service for merging two AccompanyingPeriodWork entities into a single entity. + */ class AccompanyingPeriodWorkMergeService { public function __construct(private readonly EntityManagerInterface $em) {} /** - * @throws Exception + * Merges two AccompanyingPeriodWork entities into one by transferring relevant data and removing the obsolete entity. + * + * @param AccompanyingPeriodWork $toKeep the entity to retain after the merge + * @param AccompanyingPeriodWork $toDelete the entity to be removed after transferring data + * + * @return AccompanyingPeriodWork the kept accompanying period work */ - public function merge(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void + public function merge(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): AccompanyingPeriodWork { - $conn = $this->em->getConnection(); - $conn->beginTransaction(); + $this->em->wrapInTransaction(function (EntityManagerInterface $entityManager) use ($toKeep, $toDelete) { + $this->alterStartDate($toKeep, $toDelete); + $this->alterEndDate($toKeep, $toDelete); + $this->concatenateComments($toKeep, $toDelete); + $this->transferWorkflowsSQL($toKeep, $toDelete); + $this->updateReferencesSQL($toKeep, $toDelete); + $entityManager->remove($toDelete); + }); - try { - $queries = array_merge( - $this->updateReferencesSQL($toKeep, $toDelete), - $this->transferWorkflowsSQL($toKeep, $toDelete), - $this->generateStartDateSQL($toDelete, $toKeep), - $this->generateEndDateSQL($toDelete, $toKeep), - $this->generateCommentSQL($toDelete, $toKeep), - $this->removeAccompanyingPeriodWork($toDelete) - ); - - foreach ($queries as $query) { - dump($query); - $conn->executeStatement($query['sql'], $query['params']); - } - - $conn->commit(); - } catch (\Exception $e) { - dump($e->getMessage()); - $conn->rollBack(); - throw $e; - } + return $toKeep; } - private function transferWorkflowsSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): array + private function transferWorkflowsSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void { - $queries = []; - $queries[] = [ - 'sql' => "UPDATE chill_main_workflow_entity w + $this->em->getConnection()->executeQuery( + "UPDATE chill_main_workflow_entity w SET relatedentityid = :toKeepId WHERE w.relatedentityid = :toDeleteId AND w.relatedentityclass = 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork'", - 'params' => ['toKeepId' => $toKeep->getId(), 'toDeleteId' => $toDelete->getId()], - ]; - - return $queries; + ['toKeepId' => $toKeep->getId(), 'toDeleteId' => $toDelete->getId()] + ); } - private function generateStartDateSQL(AccompanyingPeriodWork $toDelete, AccompanyingPeriodWork $toKeep): array + private function alterStartDate(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void { - $queries = []; - $queries[] = [ - 'sql' => 'UPDATE chill_person_accompanying_period_work - SET startdate = LEAST( - COALESCE((SELECT startdate FROM chill_person_accompanying_period_work WHERE id = :toDelete), startdate), - startdate - ) - WHERE id = :toKeep', - 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], - ]; - - return $queries; + $startDate = min($toKeep->getStartDate(), $toDelete->getStartDate()); + $toKeep->setStartDate($startDate); } - private function generateEndDateSQL(AccompanyingPeriodWork $toDelete, AccompanyingPeriodWork $toKeep): array + private function alterEndDate(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void { - $queries = []; - $queries[] = [ - 'sql' => ' - UPDATE chill_person_accompanying_period_work - SET enddate = - CASE - WHEN (SELECT enddate FROM chill_person_accompanying_period_work WHERE id = :toDelete) IS NULL - OR enddate IS NULL - THEN NULL - ELSE GREATEST( - COALESCE((SELECT enddate FROM chill_person_accompanying_period_work WHERE id = :toDelete), enddate), - enddate - ) - END - WHERE id = :toKeep', - 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], + if (null === $toKeep->getEndDate() || null === $toDelete->getEndDate()) { + $toKeep->setEndDate(null); - ]; - - return $queries; - } - - private function generateCommentSQL(AccompanyingPeriodWork $toDelete, AccompanyingPeriodWork $toKeep): array - { - $queries = []; - $queries[] = [ - 'sql' => "WITH updated_values AS ( - SELECT - acpw1.id AS to_update, - acpw1.note || ' ' || acpw2.note AS new_note, - jsonb_set( - acpw1.privatecomment_comments::jsonb, - '{1}', - to_jsonb((acpw1.privatecomment_comments::jsonb->>'1') || ' ' || (acpw2.privatecomment_comments::jsonb->>'1')) - ) AS new_privatecomment_comments - FROM - chill_person_accompanying_period_work acpw1, - chill_person_accompanying_period_work acpw2 - WHERE - acpw1.id = :toKeep AND - acpw2.id = :toDelete - ) - UPDATE chill_person_accompanying_period_work - SET - note = updated_values.new_note, - privatecomment_comments = updated_values.new_privatecomment_comments - FROM - updated_values - WHERE - chill_person_accompanying_period_work.id = updated_values.to_update", - 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], - ]; - - return $queries; - } - - private function updateReferencesSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $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 (AccompanyingPeriodWork::class !== $assoc['targetEntity'] && AccompanyingPeriodWork::class !== $assoc['sourceEntity']) { - continue; - } - - if (($assoc['type'] & ClassMetadata::TO_ONE) !== 0) { - $joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']); - - if (AccompanyingPeriodWorkReferrerHistory::class === $assoc['sourceEntity']) { - $queries[] = [ - 'sql' => "DELETE FROM {$tableName} WHERE {$joinColumn} = :toDelete", - 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], - ]; - } - - $queries[] = [ - 'sql' => "UPDATE {$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete", - 'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()], - ]; - } elseif (8 === $assoc['type'] && isset($assoc['joinTable'])) { - if ($assoc['isOwningSide']) { - dump($assoc); - $joinTable = $assoc['joinTable']['name']; - $joinColumn = $assoc['joinTable']['joinColumns'][0]['name']; - $relationColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name']; - $queries[] = [ - 'sql' => " - UPDATE {$joinTable} SET {$joinColumn} = :toKeep - WHERE {$joinColumn} = :toDelete - AND NOT EXISTS ( - SELECT 1 - FROM {$joinTable} AS t2 - WHERE t2.{$joinColumn} = :toKeep - AND t2.{$relationColumn} = {$joinTable}.{$relationColumn} - ) - ", - 'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()], - ]; - - $queries[] = [ - 'sql' => "DELETE FROM {$joinTable} WHERE {$joinColumn} = :toDelete", - 'params' => ['toDelete' => $toDelete->getId()], - ]; - } - } - } + return; } - return $queries; + $endDate = max($toKeep->getEndDate(), $toDelete->getEndDate()); + $toKeep->setEndDate($endDate); } - public function removeAccompanyingPeriodWork(AccompanyingPeriodWork $toDelete): array + private function concatenateComments(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void { - return [[ - 'sql' => 'DELETE FROM chill_person_accompanying_period_work WHERE id = :toDelete', - 'params' => ['toDelete' => $toDelete->getId()], - ]]; + $toKeep->setNote($toKeep->getNote()."\n\n-----------------\n\n".$toDelete->getNote()); + $toKeep->getPrivateComment()->concatenateComments($toDelete->getPrivateComment()); + } + + private function updateReferencesSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void + { + foreach ($toDelete->getAccompanyingPeriodWorkEvaluations() as $evaluation) { + $toKeep->addAccompanyingPeriodWorkEvaluation($evaluation); + } + + foreach ($toDelete->getReferrers() as $referrer) { + // we only keep the current referrer + $toKeep->addReferrer($referrer); + } + + foreach ($toDelete->getPersons() as $person) { + $toKeep->addPerson($person); + } + + if (null === $toKeep->getHandlingThierParty()) { + $toKeep->setHandlingThierParty($toDelete->getHandlingThierParty()); + } + + foreach ($toDelete->getThirdParties() as $thirdParty) { + $toKeep->addThirdParty($thirdParty); + } } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeServiceTest.php b/src/Bundle/ChillPersonBundle/Tests/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeServiceTest.php new file mode 100644 index 000000000..442842112 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Service/AccompanyingPeriodWork/AccompanyingPeriodWorkMergeServiceTest.php @@ -0,0 +1,198 @@ +prophesize(EntityManagerInterface::class); + $entityManager->wrapInTransaction(Argument::type('callable'))->will(function ($args) use ($entityManager) { + call_user_func_array($args[0], [$entityManager->reveal()]); + })->shouldBeCalled(); + $entityManager->remove($toRemove)->shouldBeCalled(); + + $connection = $this->prophesize(Connection::class); + $connection->executeQuery(Argument::type('string'), Argument::type('array'))->shouldBeCalled(); + + $entityManager->getConnection()->willReturn($connection->reveal()); + + return new AccompanyingPeriodWorkMergeService($entityManager->reveal()); + } + + /** + * @dataProvider provideStartDateMoveData + */ + public function testStartDateMove(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete, ?\DateTime $expected): void + { + $service = $this->buildMergeService($toDelete); + $return = $service->merge($toKeep, $toDelete); + + self::assertEquals($expected, $return->getStartDate()); + } + + public static function provideStartDateMoveData(): array + { + return [ + 'Earliest date kept when toKeep is earlier' => [ + (new AccompanyingPeriodWork())->setStartDate(new \DateTime('2023-01-01')), + (new AccompanyingPeriodWork())->setStartDate(new \DateTime('2023-06-01')), + new \DateTime('2023-01-01'), + ], + 'Earliest date kept when toDelete is earlier' => [ + (new AccompanyingPeriodWork())->setStartDate(new \DateTime('2023-06-01')), + (new AccompanyingPeriodWork())->setStartDate(new \DateTime('2023-01-01')), + new \DateTime('2023-01-01'), + ], + 'Same start dates remain unchanged' => [ + (new AccompanyingPeriodWork())->setStartDate(new \DateTime('2023-01-01')), + (new AccompanyingPeriodWork())->setStartDate(new \DateTime('2023-01-01')), + new \DateTime('2023-01-01'), + ], + ]; + } + + /** + * @dataProvider provideEndDateMoveData + */ + public function testEndDateMove(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete, ?\DateTimeImmutable $expected): void + { + $service = $this->buildMergeService($toDelete); + $return = $service->merge($toKeep, $toDelete); + + self::assertEquals($expected, $return->getEndDate()); + } + + public static function provideEndDateMoveData(): array + { + return [ + 'Oldest date kept when toKeep is older' => [ + (new AccompanyingPeriodWork())->setEndDate(new \DateTimeImmutable('2022-01-01'))->setStartDate(new \DateTime('2021-01-01')), + (new AccompanyingPeriodWork())->setEndDate(new \DateTimeImmutable('2023-06-01'))->setStartDate(new \DateTime('2021-01-01')), + new \DateTimeImmutable('2023-06-01'), + ], + 'Oldest date kept when toDelete is older' => [ + (new AccompanyingPeriodWork())->setEndDate(new \DateTimeImmutable('2023-06-01'))->setStartDate(new \DateTime('2021-01-01')), + (new AccompanyingPeriodWork())->setEndDate(new \DateTimeImmutable('2022-01-01'))->setStartDate(new \DateTime('2021-01-01')), + new \DateTimeImmutable('2023-06-01'), + ], + 'Same end dates remain unchanged' => [ + (new AccompanyingPeriodWork())->setEndDate(new \DateTimeImmutable('2023-01-01'))->setStartDate(new \DateTime('2021-01-01')), + (new AccompanyingPeriodWork())->setEndDate(new \DateTimeImmutable('2023-01-01'))->setStartDate(new \DateTime('2021-01-01')), + new \DateTimeImmutable('2023-01-01'), + ], + 'End date is null if toKeep is null' => [ + (new AccompanyingPeriodWork())->setEndDate(null)->setStartDate(new \DateTime('2021-01-01')), + (new AccompanyingPeriodWork())->setEndDate(new \DateTimeImmutable('2023-01-01'))->setStartDate(new \DateTime('2021-01-01')), + null, + ], + 'End date is null if toDelete is null' => [ + (new AccompanyingPeriodWork())->setEndDate(new \DateTimeImmutable('2023-01-01'))->setStartDate(new \DateTime('2021-01-01')), + (new AccompanyingPeriodWork())->setEndDate(null)->setStartDate(new \DateTime('2021-01-01')), + null, + ], + 'End date is null if both are null' => [ + (new AccompanyingPeriodWork())->setEndDate(null)->setStartDate(new \DateTime('2021-01-01')), + (new AccompanyingPeriodWork())->setEndDate(null)->setStartDate(new \DateTime('2021-01-01')), + null, + ], + ]; + } + + /** + * @dataProvider provideMoveHandlingThirdPartyData + */ + public function testMoveHandlingThirdParty(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete, ?ThirdParty $expected): void + { + $service = $this->buildMergeService($toDelete); + $return = $service->merge($toKeep, $toDelete); + + self::assertSame($expected, $return->getHandlingThierParty()); + } + + public static function provideMoveHandlingThirdPartyData(): iterable + { + yield 'Third party not change when existing in kept' => [ + (new AccompanyingPeriodWork())->setStartDate(new \DateTimeImmutable('2022-01-01'))->setHandlingThierParty($tpA = new ThirdParty()), + (new AccompanyingPeriodWork())->setStartDate(new \DateTimeImmutable('2022-01-01'))->setHandlingThierParty(new ThirdParty()), + $tpA, + ]; + + yield 'Third party will change when not existing in kept' => [ + (new AccompanyingPeriodWork())->setStartDate(new \DateTimeImmutable('2022-01-01')), + (new AccompanyingPeriodWork())->setStartDate(new \DateTimeImmutable('2022-01-01'))->setHandlingThierParty($tpB = new ThirdParty()), + $tpB, + ]; + + yield 'Third party do not change when not existing in removed' => [ + (new AccompanyingPeriodWork())->setStartDate(new \DateTimeImmutable('2022-01-01'))->setHandlingThierParty($tpC = new ThirdParty()), + (new AccompanyingPeriodWork())->setStartDate(new \DateTimeImmutable('2022-01-01')), + $tpC, + ]; + } + + public function testMerge(): void + { + $accompanyingPeriodWork = new AccompanyingPeriodWork(); + $accompanyingPeriodWork->setStartDate(new \DateTime('2022-01-01')); + $accompanyingPeriodWork->addReferrer($userA = new User()); + $accompanyingPeriodWork->addReferrer($userC = new User()); + $accompanyingPeriodWork->addAccompanyingPeriodWorkEvaluation($evaluationA = new AccompanyingPeriodWorkEvaluation()); + $accompanyingPeriodWork->setNote('blabla'); + $accompanyingPeriodWork->addThirdParty($thirdPartyA = new ThirdParty()); + + $toDelete = new AccompanyingPeriodWork(); + $toDelete->setStartDate(new \DateTime('2022-01-01')); + $toDelete->addReferrer($userB = new User()); + $toDelete->addReferrer($userC); + $toDelete->addAccompanyingPeriodWorkEvaluation($evaluationB = new AccompanyingPeriodWorkEvaluation()); + $toDelete->setNote('boum'); + $toDelete->addThirdParty($thirdPartyB = new ThirdParty()); + + $service = $this->buildMergeService($toDelete); + $service->merge($accompanyingPeriodWork, $toDelete); + + self::assertTrue($accompanyingPeriodWork->getReferrers()->contains($userA)); + self::assertTrue($accompanyingPeriodWork->getReferrers()->contains($userB)); + self::assertTrue($accompanyingPeriodWork->getReferrers()->contains($userC)); + + self::assertTrue($accompanyingPeriodWork->getAccompanyingPeriodWorkEvaluations()->contains($evaluationA)); + self::assertTrue($accompanyingPeriodWork->getAccompanyingPeriodWorkEvaluations()->contains($evaluationB)); + foreach ($accompanyingPeriodWork->getAccompanyingPeriodWorkEvaluations() as $evaluation) { + self::assertSame($accompanyingPeriodWork, $evaluation->getAccompanyingPeriodWork()); + } + + self::assertStringContainsString('blabla', $accompanyingPeriodWork->getNote()); + self::assertStringContainsString('boum', $toDelete->getNote()); + + self::assertTrue($accompanyingPeriodWork->getThirdParties()->contains($thirdPartyA)); + self::assertTrue($accompanyingPeriodWork->getThirdParties()->contains($thirdPartyB)); + } +} diff --git a/src/Bundle/ChillPersonBundle/chill.webpack.config.js b/src/Bundle/ChillPersonBundle/chill.webpack.config.js index 4c8f106c4..effbec70f 100644 --- a/src/Bundle/ChillPersonBundle/chill.webpack.config.js +++ b/src/Bundle/ChillPersonBundle/chill.webpack.config.js @@ -66,6 +66,6 @@ module.exports = function (encore, entries) { ); encore.addEntry( "mod_duplicate_selector", - __dirname + "/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.js", + __dirname + "/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.ts", ); };