This commit is contained in:
Julien Fastré 2025-04-15 14:02:25 +02:00
parent 0581b59dbd
commit 7e2bf91e09
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
18 changed files with 430 additions and 224 deletions

View File

@ -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];
}
}
}
}

View File

@ -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 = <Input, Output>(
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",

View File

@ -281,10 +281,10 @@
{% endblock %}
{% block pick_linked_entities_widget %}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-linked-entities"
data-suggested="{{ form.vars['suggested']|json_encode|escape('html_attr') }}"
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}" />
<div data-input-uniqid="{{ form.vars['uniqid'] }}" data-module="pick-linked-entities" data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
></div>
{% endblock %}
{% block pick_postal_code_widget %}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Entity\Workflow;
use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable;
use Chill\MainBundle\Entity\User;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @coversNothing
*/
class PrivateCommentEmbeddableTest extends TestCase
{
use ProphecyTrait;
public function testConcatenateComment(): void
{
$userA = $this->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()));
}
}

View File

@ -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']]);
}

View File

@ -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;

View File

@ -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',

View File

@ -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)

View File

@ -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);
}
});

View File

@ -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<HTMLDivElement>('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<HTMLInputElement>(`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:
'<accompanying-period-work-selector-modal :accompanying-period-id="accompanyingPeriodId" @pickWork="pickWork"></accompanying-period-work-selector-modal>',
components: {AccompanyingPeriodWorkSelectorModal},
data() {
return { accompanyingPeriodId };
},
methods: {
pickWork: function(payload: {work: AccompanyingPeriodWork}) {
console.log("payload", payload);
input.value = payload.work.id.toString();
}
}
});
app.mount(el);
})
});

View File

@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
}
export interface AccompanyingPeriodWork {
id?: number;
id: number;
accompanyingPeriod?: AccompanyingPeriod;
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
createdAt?: string;

View File

@ -59,16 +59,22 @@ import {
} from "translator";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
type AccompanyingPeriodWorkSelectorModalProps = {
accompanyingPeriodId: number;
};
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
const showModal = ref(false);
const accompanyingPeriodWorks = ref<AccompanyingPeriodWork[]>([]);
const props = defineProps({
accompanyingPeriodId: String,
});
const props = defineProps<AccompanyingPeriodWorkSelectorModalProps>();
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();
};
</script>

View File

@ -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 %}

View File

@ -4,10 +4,21 @@
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
{% block content %}
<div class="person-duplicate">
<h1>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h1>
<div class="col">
<h4>{{ 'acpw_duplicate.to keep'|trans ~ ':' }}</h4>
<div class="accompanying-course-work">
<div class="flex-table">
{{ details.details(acpw, accompanyingCourse) }}
</div>
</div>
</div>
<h3>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h3>
{{ form_start(form) }}
{%- if form.acpw is defined -%}
{{ form_row(form.acpw) }}
@ -22,7 +33,7 @@
</a>
</li>
<li>
<button class="btn btn-save" type="submit">{{ 'Next'|trans }}</button>
<button class="btn btn-action" type="submit">{{ 'Next'|trans }}</button>
</li>
</ul>
@ -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 %}

View File

@ -50,7 +50,7 @@
<ul class="col-12 record_actions">
<li class="cancel">
<a href="{{ path('chill_person_accompanying_period_work_assign_duplicate', {id : acpw.id}) }}" class="btn btn-chill-gray center margin-5">
<a href="{{ path('chill_person_accompanying_period_work_assign_duplicate', {id : acpw.id}) }}" class="btn btn-cancel">
{{ 'Return'|trans }}
</a>
</li>
@ -62,7 +62,7 @@
</a>
</li>
<li>
<button class="btn btn-submit" type="submit"><i class="fa fa-cog fa-fw"></i>{{ 'Merge'|trans }}</button>
<button class="btn btn-submit" type="submit"><i class="bi bi-chevron-contract"></i>{{ 'Merge'|trans }}</button>
</li>
</ul>

View File

@ -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']);
return $toKeep;
}
$conn->commit();
} catch (\Exception $e) {
dump($e->getMessage());
$conn->rollBack();
throw $e;
}
}
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;
return;
}
private function generateCommentSQL(AccompanyingPeriodWork $toDelete, AccompanyingPeriodWork $toKeep): array
$endDate = max($toKeep->getEndDate(), $toDelete->getEndDate());
$toKeep->setEndDate($endDate);
}
private function concatenateComments(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
$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;
$toKeep->setNote($toKeep->getNote()."\n\n-----------------\n\n".$toDelete->getNote());
$toKeep->getPrivateComment()->concatenateComments($toDelete->getPrivateComment());
}
private function updateReferencesSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): array
private function updateReferencesSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
$queries = [];
$allMeta = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($allMeta as $meta) {
if ($meta->isMappedSuperclass) {
continue;
foreach ($toDelete->getAccompanyingPeriodWorkEvaluations() as $evaluation) {
$toKeep->addAccompanyingPeriodWorkEvaluation($evaluation);
}
$tableName = $meta->getTableName();
foreach ($meta->getAssociationMappings() as $assoc) {
if (AccompanyingPeriodWork::class !== $assoc['targetEntity'] && AccompanyingPeriodWork::class !== $assoc['sourceEntity']) {
continue;
foreach ($toDelete->getReferrers() as $referrer) {
// we only keep the current referrer
$toKeep->addReferrer($referrer);
}
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()],
];
foreach ($toDelete->getPersons() as $person) {
$toKeep->addPerson($person);
}
$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()],
];
}
}
}
if (null === $toKeep->getHandlingThierParty()) {
$toKeep->setHandlingThierParty($toDelete->getHandlingThierParty());
}
return $queries;
foreach ($toDelete->getThirdParties() as $thirdParty) {
$toKeep->addThirdParty($thirdParty);
}
public function removeAccompanyingPeriodWork(AccompanyingPeriodWork $toDelete): array
{
return [[
'sql' => 'DELETE FROM chill_person_accompanying_period_work WHERE id = :toDelete',
'params' => ['toDelete' => $toDelete->getId()],
]];
}
}

View File

@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Service\AccompanyingPeriodWork;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Service\AccompanyingPeriodWork\AccompanyingPeriodWorkMergeService;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Monolog\Test\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @coversNothing
*/
class AccompanyingPeriodWorkMergeServiceTest extends TestCase
{
use ProphecyTrait;
private function buildMergeService(AccompanyingPeriodWork $toRemove): AccompanyingPeriodWorkMergeService
{
$entityManager = $this->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));
}
}

View File

@ -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",
);
};