Merge branch 'refs/heads/master' into ticket-app-master

# Conflicts:
#	src/Bundle/ChillPersonBundle/Resources/public/types.ts
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AccompanyingPeriod/SetReferrer.vue
This commit is contained in:
Julien Fastré 2025-07-02 17:28:59 +02:00
commit 06e8264dde
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
84 changed files with 1689 additions and 502 deletions

View File

@ -1,6 +0,0 @@
kind: DX
body: Remove dead code for wopi-link module
time: 2025-04-30T14:45:50.406111606+02:00
custom:
Issue: "352"
SchemaChange: No schema change

View File

@ -1,6 +0,0 @@
kind: DX
body: Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
time: 2025-05-28T16:58:13.226870341+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@ -0,0 +1,6 @@
kind: Feature
body: Allow the merge of two accompanying period works
time: 2025-02-11T14:22:43.134106669+01:00
custom:
Issue: "359"
SchemaChange: No schema change

View File

@ -1,7 +0,0 @@
kind: Feature
body: Add the document file name to the document title when a user upload a document,
unless there is already a document title.
time: 2025-04-24T14:22:11.800975422+02:00
custom:
Issue: "377"
SchemaChange: No schema change

View File

@ -1,6 +0,0 @@
kind: Feature
body: Add desactivation date for social action and issue csv export
time: 2025-05-20T09:56:28.108941934+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@ -1,6 +0,0 @@
kind: Feature
body: Add Emoji and Fullscreen feature to ckeditor configuration
time: 2025-05-23T13:33:41.645095128+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@ -1,6 +0,0 @@
kind: Feature
body: Create editor which allow us to toggle between rich and simple text editor
time: 2025-05-23T13:34:34.56795603+02:00
custom:
Issue: "321"
SchemaChange: No schema change

View File

@ -1,7 +0,0 @@
kind: Fixed
body: trying to prevent bug of typeerror in doc-history + improved display of document
history
time: 2025-04-24T13:39:43.878468232+02:00
custom:
Issue: "376"
SchemaChange: No schema change

View File

@ -1,7 +0,0 @@
kind: Fixed
body: Display previous participation in acc course work even if the person has left
the acc course
time: 2025-04-24T16:37:46.970203594+02:00
custom:
Issue: "381"
SchemaChange: No schema change

View File

@ -1,6 +0,0 @@
kind: Fixed
body: Fix display of text in calendar events
time: 2025-05-05T10:27:15.461493066+02:00
custom:
Issue: "372"
SchemaChange: No schema change

View File

@ -1,6 +0,0 @@
kind: Fixed
body: Add missing translation for user_group.no_user_groups
time: 2025-05-14T14:53:39.53927329+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@ -0,0 +1,6 @@
kind: Fixed
body: Display the list of participant in the results, even if there is only one participant and that the search result display the requestor
time: 2025-06-04T16:55:50.107852336+02:00
custom:
Issue: "390"
SchemaChange: No schema change

View File

@ -0,0 +1,6 @@
kind: Fixed
body: Fix admin entity edit actions for event admin entities and activity reason (category) entities
time: 2025-06-11T16:46:23.113506434+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@ -1,7 +0,0 @@
kind: Fixed
body: |
Allow null and cast as string to setContent method for NewsItem
time: 2025-06-19T17:01:42.125730402+02:00
custom:
Issue: "392"
SchemaChange: No schema change

View File

@ -0,0 +1,6 @@
kind: Fixed
body: 'Fix translations for social action fields in admin form: results, goals, evaluations'
time: 2025-07-02T13:55:34.599050626+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@ -1,6 +0,0 @@
kind: UX
body: Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
time: 2025-04-23T17:26:24.45777387+02:00
custom:
Issue: "374"
SchemaChange: No schema change

View File

@ -0,0 +1,6 @@
kind: UX
body: Improve labeling of fields in person resource creation form
time: 2025-06-17T19:26:50.599703116+02:00
custom:
Issue: ""
SchemaChange: No schema change

22
.changes/v3.12.0.md Normal file
View File

@ -0,0 +1,22 @@
## v3.12.0 - 2025-06-30
### Feature
* ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.
* Add desactivation date for social action and issue csv export
* Add Emoji and Fullscreen feature to ckeditor configuration
* ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor
* Do not remove workflow which are automatically canceled after staling for more than 30 days
### Fixed
* ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history
* ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course
* ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events
* Add missing translation for user_group.no_user_groups
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem
* ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.
### DX
* ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module
* Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
### UX
* ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
* Improve labeling of fields in person resource creation form

3
.changes/v3.12.1.md Normal file
View File

@ -0,0 +1,3 @@
## v3.12.1 - 2025-06-30
### Fixed
* Fix loading of the list of documents

View File

@ -6,6 +6,53 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.12.1 - 2025-06-30
### Fixed
* Fix loading of the list of documents
## v3.12.0 - 2025-06-30
### Feature
* ([#377](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/377)) Add the document file name to the document title when a user upload a document, unless there is already a document title.
* Add desactivation date for social action and issue csv export
* Add Emoji and Fullscreen feature to ckeditor configuration
* ([#321](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/321)) Create editor which allow us to toggle between rich and simple text editor
* Do not remove workflow which are automatically canceled after staling for more than 30 days
### Fixed
* ([#376](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/376)) trying to prevent bug of typeerror in doc-history + improved display of document history
* ([#381](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/381)) Display previous participation in acc course work even if the person has left the acc course
* ([#372](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/372)) Fix display of text in calendar events
* Add missing translation for user_group.no_user_groups
* Fix admin entity edit actions for event admin entities and activity reason (category) entities
* ([#392](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/392)) Allow null and cast as string to setContent method for NewsItem
* ([#393](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/393)) Doc Generation: the "dump only" method send the document as an email attachment.
### DX
* ([#352](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/352)) Remove dead code for wopi-link module
* Replace library node-sass by sass, and upgrade bootstrap to version 5.3 (yarn upgrade / install is required)
### UX
* ([#374](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/374)) Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
* Improve labeling of fields in person resource creation form
## v3.11.0 - 2025-04-17
### Feature
* ([#365](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/365)) Add counters of actions and activities, with 2 boxes to (1) show the number of active actions on total actions and (2) show the number of activities in a accompanying period, and pills in menus for showing the number of active actions and the number of activities.
* ([#364](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/364)) Added a second phone number "telephone2" to the thirdParty entity. Adapted twig templates and vuejs apps to handle this phone number
**Schema Change**: Add columns or tables
* Signature: add a button to go directly to the signature zone, even if there is only one
### Fixed
* Fixed wrong translations in the on-the-fly for creation of thirdParty
* Fixed update of phone number in on-the-fly edition of thirdParty
* Fixed closing of modal when editing thirdParty in accompanying course works
* Shorten the delay between two execution of AccompanyingPeriodStepChangeCronjob, to ensure at least one execution in a day
* ([#102](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/102)) Fix display of title in document list
* When cleaning the old stored object versions, do not throw an error if the stored object is not found on disk
* Add consistent log prefix and key to logs when stale workflows are automatically canceled
* ([#380](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/380)) Remove the "not null" validation constraint on recently added properties on HouseholdComposition
### DX
* Add new chill-col style for displaying title and aside in a flex table
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes

View File

@ -11,6 +11,7 @@
"@hotwired/stimulus": "^3.0.0",
"@luminateone/eslint-baseline": "^1.0.9",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5",

View File

@ -48,28 +48,6 @@ class ActivityReasonCategoryController extends AbstractController
]);
}
/**
* Displays a form to edit an existing ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/edit', name: 'chill_activity_activityreasoncategory_edit')]
public function editAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReasonCategory::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReasonCategory entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('@ChillActivity/ActivityReasonCategory/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
]);
}
/**
* Lists all ActivityReasonCategory entities.
*/
@ -100,29 +78,10 @@ class ActivityReasonCategoryController extends AbstractController
]);
}
/**
* Finds and displays a ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/show', name: 'chill_activity_activityreasoncategory_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReasonCategory::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReasonCategory entity.');
}
return $this->render('@ChillActivity/ActivityReasonCategory/show.html.twig', [
'entity' => $entity,
]);
}
/**
* Edits an existing ActivityReasonCategory entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/update', name: 'chill_activity_activityreasoncategory_update', methods: ['POST', 'PUT'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreasoncategory/{id}/update', name: 'chill_activity_activityreasoncategory_update')]
public function updateAction(Request $request, mixed $id)
{
$em = $this->managerRegistry->getManager();
@ -139,7 +98,7 @@ class ActivityReasonCategoryController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em->flush();
return $this->redirectToRoute('chill_activity_activityreasoncategory_edit', ['id' => $id]);
return $this->redirectToRoute('chill_activity_activityreasoncategory', ['id' => $id]);
}
return $this->render('@ChillActivity/ActivityReasonCategory/edit.html.twig', [
@ -178,7 +137,7 @@ class ActivityReasonCategoryController extends AbstractController
{
$form = $this->createForm(ActivityReasonCategoryType::class, $entity, [
'action' => $this->generateUrl('chill_activity_activityreasoncategory_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@ -17,7 +17,6 @@ use Chill\ActivityBundle\Repository\ActivityReasonRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* ActivityReason controller.
@ -50,28 +49,6 @@ class ActivityReasonController extends AbstractController
]);
}
/**
* Displays a form to edit an existing ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/edit', name: 'chill_activity_activityreason_edit')]
public function editAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReason::class)->find($id);
if (null === $entity) {
throw new NotFoundHttpException('Unable to find ActivityReason entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('@ChillActivity/ActivityReason/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
]);
}
/**
* Lists all ActivityReason entities.
*/
@ -102,29 +79,10 @@ class ActivityReasonController extends AbstractController
]);
}
/**
* Finds and displays a ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/show', name: 'chill_activity_activityreason_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(ActivityReason::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReason entity.');
}
return $this->render('@ChillActivity/ActivityReason/show.html.twig', [
'entity' => $entity,
]);
}
/**
* Edits an existing ActivityReason entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update', methods: ['POST', 'PUT'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/activityreason/{id}/update', name: 'chill_activity_activityreason_update')]
public function updateAction(Request $request, mixed $id)
{
$em = $this->managerRegistry->getManager();
@ -180,7 +138,7 @@ class ActivityReasonController extends AbstractController
{
$form = $this->createForm(ActivityReasonType::class, $entity, [
'action' => $this->generateUrl('chill_activity_activityreason_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@ -2,7 +2,7 @@ import "es6-promise/auto";
import { createStore } from "vuex";
import { postLocation } from "./api";
import prepareLocations from "./store.locations.js";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import {fetchResults, makeFetch} from "ChillMainAssets/lib/api/apiMethods";
const debug = process.env.NODE_ENV !== "production";
//console.log('window.activity', window.activity);
@ -365,11 +365,11 @@ const store = createStore({
const accompanyingPeriodId = state.activity.accompanyingPeriod.id;
const url = `/api/1.0/person/accompanying-course/${accompanyingPeriodId}/works.json`;
try {
const works = await makeFetch("GET", url);
// console.log("works", works);
const works = await fetchResults(url);
// console.log('works', works);
commit("setAccompanyingPeriodWorks", works);
} catch (error) {
console.error("Failed to fetch accompanying period works:", error);
console.error('Failed to fetch works:', error);
}
},
getWhoAmI({ commit }) {

View File

@ -3,7 +3,7 @@
{% block admin_content %}
<h1>{{ 'ActivityReason list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
@ -29,10 +29,7 @@
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activityreason_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_activity_activityreason_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
<a href="{{ path('chill_activity_activityreason_update', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>

View File

@ -3,7 +3,7 @@
{% block admin_content %}
<h1>{{ 'ActivityReasonCategory list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
@ -23,10 +23,7 @@
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activityreasoncategory_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_activity_activityreasoncategory_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
<a href="{{ path('chill_activity_activityreasoncategory_update', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>

View File

@ -58,6 +58,7 @@
<script>
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
export default {
name: "PickTemplate",
@ -113,6 +114,9 @@ export default {
},
},
methods: {
localizeString(str) {
return localizeString(str);
},
clickGenerate(event, link) {
if (!this.preventDefaultMoveToGenerate) {
window.location.assign(link);

View File

@ -1,7 +1,5 @@
{{ 'docgen.data_dump_email.Dear'|trans }}
{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }}
{{ 'docgen.data_dump_email.data_dump_ready_and_attached'|trans }}
{{ link }}
{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }}
{{ 'docgen.data_dump_email.filename'|trans({filename: filename}) }}

View File

@ -11,13 +11,13 @@ declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\Generator;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@ -37,15 +37,15 @@ class RequestGenerationHandler implements MessageHandlerInterface
private const LOG_PREFIX = '[docgen message handler] ';
public function __construct(
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
private readonly DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Generator $generator,
private readonly GeneratorInterface $generator,
private readonly LoggerInterface $logger,
private readonly StoredObjectRepository $storedObjectRepository,
private readonly StoredObjectRepositoryInterface $storedObjectRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly MailerInterface $mailer,
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
private readonly TranslatorInterface $translator,
private readonly StoredObjectManagerInterface $storedObjectManager,
) {}
public function __invoke(RequestGenerationMessage $message)
@ -90,7 +90,7 @@ class RequestGenerationHandler implements MessageHandlerInterface
$this->sendDataDump($destinationStoredObject, $message);
} else {
$destinationStoredObject = $this->generator->generateDocFromTemplate(
$this->generator->generateDocFromTemplate(
$template,
$message->getEntityId(),
$message->getContextGenerationData(),
@ -122,19 +122,20 @@ class RequestGenerationHandler implements MessageHandlerInterface
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
{
$url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600);
$parts = [];
parse_str(parse_url($url->url)['query'], $parts);
$validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']);
// Get the content of the document
$content = $this->storedObjectManager->read($destinationStoredObject);
$filename = $destinationStoredObject->getFilename();
$contentType = $destinationStoredObject->getType();
// Create the email with the document as an attachment
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
->context([
'link' => $url->url,
'validity' => $validity,
'filename' => $filename,
])
->subject($this->translator->trans('docgen.data_dump_email.subject'));
->subject($this->translator->trans('docgen.data_dump_email.subject'))
->attach($content, $filename, $contentType);
$this->mailer->send($email);
}

View File

@ -0,0 +1,132 @@
<?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\DocGeneratorBundle\Tests\Service\Messenger;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationHandler;
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*
* @coversNothing
*/
class RequestGenerationHandlerTest extends TestCase
{
use ProphecyTrait;
public function testGenerationHappyScenario(): void
{
// Create entities
$template = new DocGeneratorTemplate();
$this->setPrivateProperty($template, 'id', 1);
$storedObject = new StoredObject();
$this->setPrivateProperty($storedObject, 'id', 2);
$creator = new User();
$creator->setEmail('test@example.com');
$this->setPrivateProperty($creator, 'id', 3);
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
$docGeneratorTemplateRepository->find(1)->willReturn($template);
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
$storedObjectRepository->find(2)->willReturn($storedObject);
$userRepository = $this->prophesize(UserRepositoryInterface::class);
$userRepository->find(3)->willReturn($creator);
// Create a mock for the Query object
$query = $this->prophesize(Query::class);
$query->setParameter('id', 2)->willReturn($query->reveal());
$query->execute()->shouldBeCalled();
// Create a mock for the EntityManager
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->createQuery(Argument::containingString('UPDATE'))->willReturn($query->reveal());
$entityManager->flush()->shouldBeCalled();
$generator = $this->prophesize(GeneratorInterface::class);
$generator->generateDocFromTemplate(
$template,
123, // entityId
['key' => 'value'], // contextGenerationData
$storedObject,
$creator
)
->willReturn($storedObject)->shouldBeCalled();
$logger = new NullLogger();
$mailer = $this->prophesize(MailerInterface::class);
$translator = $this->prophesize(TranslatorInterface::class);
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
// Create handler
$handler = new RequestGenerationHandler(
$docGeneratorTemplateRepository->reveal(),
$entityManager->reveal(),
$generator->reveal(),
$logger,
$storedObjectRepository->reveal(),
$userRepository->reveal(),
$mailer->reveal(),
$translator->reveal(),
$storedObjectManager->reveal()
);
// Create message
$message = new RequestGenerationMessage(
$creator,
$template,
123, // entityId
$storedObject,
['key' => 'value'], // contextGenerationData
false, // isTest
null, // sendResultToEmail
false // dumpOnly
);
// Invoke handler
$handler->__invoke($message);
// Assertions
// The assertions are handled by the shouldBeCalled() expectations on the mocks
$this->assertTrue(true); // Just to have an assertion in the test
}
private function setPrivateProperty(object $object, string $propertyName, $value): void
{
$reflection = new \ReflectionClass($object);
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$property->setValue($object, $value);
}
}

View File

@ -1,4 +1,2 @@
docgen:
data_dump_email:
link_valid_until: >-
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}
# No ICU messages needed for data_dump_email anymore

View File

@ -34,8 +34,10 @@ docgen:
data_dump_email:
subject: Contenu des données de génération de document disponible
Dear: Cher
data_dump_ready_and_link: >-
Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant:
data_dump_ready_and_attached: >-
Le contenu des données est disponible. Vous le trouverez en pièce jointe à cet email.
filename: >-
Nom du fichier: %filename%

View File

@ -48,30 +48,6 @@ class EventTypeController extends AbstractController
]);
}
/**
* Deletes a EventType entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/event_type/{id}/delete', name: 'chill_eventtype_admin_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(EventType::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_eventtype_admin');
}
/**
* Displays a form to edit an existing EventType entity.
*/
@ -87,12 +63,10 @@ class EventTypeController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/EventType/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@ -126,28 +100,6 @@ class EventTypeController extends AbstractController
]);
}
/**
* Finds and displays a EventType entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/event_type/{id}/show', name: 'chill_eventtype_admin_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(EventType::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/EventType/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing EventType entity.
*/
@ -162,7 +114,6 @@ class EventTypeController extends AbstractController
throw $this->createNotFoundException('Unable to find EventType entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@ -175,7 +126,6 @@ class EventTypeController extends AbstractController
return $this->render('@ChillEvent/EventType/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@ -198,22 +148,6 @@ class EventTypeController extends AbstractController
return $form;
}
/**
* Creates a form to delete a EventType entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl(
'chill_eventtype_admin_delete',
['id' => $id]
))
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a EventType entity.
*
@ -228,7 +162,7 @@ class EventTypeController extends AbstractController
'chill_eventtype_admin_update',
['id' => $entity->getId()]
),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@ -48,30 +48,6 @@ class RoleController extends AbstractController
]);
}
/**
* Deletes a Role entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/role/{id}/delete', name: 'chill_event_admin_role_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Role::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Role entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_event_admin_role');
}
/**
* Displays a form to edit an existing Role entity.
*/
@ -87,12 +63,10 @@ class RoleController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Role/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@ -126,28 +100,6 @@ class RoleController extends AbstractController
]);
}
/**
* Finds and displays a Role entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/role/{id}/show', name: 'chill_event_admin_role_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Role::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Role entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Role/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing Role entity.
*/
@ -162,7 +114,6 @@ class RoleController extends AbstractController
throw $this->createNotFoundException('Unable to find Role entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@ -175,7 +126,6 @@ class RoleController extends AbstractController
return $this->render('@ChillEvent/Role/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@ -198,20 +148,6 @@ class RoleController extends AbstractController
return $form;
}
/**
* Creates a form to delete a Role entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('chill_event_admin_role_delete', ['id' => $id]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a Role entity.
*
@ -226,7 +162,7 @@ class RoleController extends AbstractController
'chill_event_admin_role_update',
['id' => $entity->getId()]
),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@ -48,30 +48,6 @@ class StatusController extends AbstractController
]);
}
/**
* Deletes a Status entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/status/{id}/delete', name: 'chill_event_admin_status_delete', methods: ['POST', 'DELETE'])]
public function deleteAction(Request $request, mixed $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Status::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Status entity.');
}
$em->remove($entity);
$em->flush();
}
return $this->redirectToRoute('chill_event_admin_status');
}
/**
* Displays a form to edit an existing Status entity.
*/
@ -87,12 +63,10 @@ class StatusController extends AbstractController
}
$editForm = $this->createEditForm($entity);
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Status/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@ -126,28 +100,6 @@ class StatusController extends AbstractController
]);
}
/**
* Finds and displays a Status entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/admin/event/status/{id}/show', name: 'chill_event_admin_status_show')]
public function showAction(mixed $id)
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Status::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Status entity.');
}
$deleteForm = $this->createDeleteForm($id);
return $this->render('@ChillEvent/Status/show.html.twig', [
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Edits an existing Status entity.
*/
@ -162,7 +114,6 @@ class StatusController extends AbstractController
throw $this->createNotFoundException('Unable to find Status entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
@ -175,7 +126,6 @@ class StatusController extends AbstractController
return $this->render('@ChillEvent/Status/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
@ -198,19 +148,6 @@ class StatusController extends AbstractController
return $form;
}
/**
* Creates a form to delete a Status entity by id.
*
* @return \Symfony\Component\Form\FormInterface The form
*/
private function createDeleteForm(mixed $id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('chill_event_admin_status_delete', ['id' => $id]))
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
}
/**
* Creates a form to edit a Status entity.
*
@ -222,7 +159,7 @@ class StatusController extends AbstractController
{
$form = $this->createForm(StatusType::class, $entity, [
'action' => $this->generateUrl('chill_event_admin_status_update', ['id' => $entity->getId()]),
'method' => 'PUT',
'method' => 'POST',
]);
$form->add('submit', SubmitType::class, ['label' => 'Update']);

View File

@ -8,7 +8,7 @@
{{ form_row(edit_form.name) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@ -16,14 +16,11 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_eventtype_admin_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_eventtype_admin_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_eventtype_admin_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@ -8,7 +8,7 @@
{{ form_row(form.name) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@ -21,17 +21,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_eventtype_admin') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_eventtype_admin_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

View File

@ -8,12 +8,12 @@
{{ form_row(edit_form.type) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
{{ form_row(edit_form.submit, { 'attr': { 'class' : 'btn btn-edit' }}) }}
{{ form_row(edit_form.submit, { 'attr': { 'class' : 'btn btn-update' }}) }}
</li>
</ul>

View File

@ -17,15 +17,12 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_event_admin_role_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.type.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_event_admin_role_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_event_admin_role_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@ -9,7 +9,7 @@
{{ form_row(form.type) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@ -25,17 +25,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_role') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_event_admin_role_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

View File

@ -9,7 +9,7 @@
{{ form_row(edit_form.type) }}
{{ form_row(edit_form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@ -17,15 +17,12 @@
<tbody>
{% for entity in entities %}
<tr>
<td><a href="{{ path('chill_event_admin_status_show', { 'id': entity.id }) }}">{{ entity.id }}</a></td>
<td>{{ entity.id }}</a></td>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.type.name|localize_translatable_string }}</td>
<td>{{ entity.active }}</td>
<td><i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_event_admin_status_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_event_admin_status_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>

View File

@ -9,7 +9,7 @@
{{ form_row(form.type) }}
{{ form_row(form.active) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>

View File

@ -25,17 +25,12 @@
</tbody>
</table>
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event_admin_status') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li>
<li>
<a href="{{ path('chill_event_admin_status_edit', { 'id': entity.id }) }}" class="btn btn-edit">{{ 'Edit'|trans }}</a>
</li>
<li>
{{ form_start(delete_form) }}
{{ form_row(delete_form.submit, { 'attr': { 'class' : 'btn btn-delete' }}) }}
{{ form_end(delete_form) }}
</li>
</ul>
{% endblock %}

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

@ -224,3 +224,7 @@ export interface WorkflowAttachment {
updatedBy: User | null;
genericDoc: null | GenericDoc;
}
export interface PrivateCommentEmbeddable {
comments: Record<number, string>;
}

View File

@ -11,10 +11,12 @@ const appMessages = {
user: "Utilisateurs",
person: "Usagers",
thirdparty: "Tiers",
acpw: "Action d'accompagnements",
modal_title_one: "Indiquer un ",
user_one: "Utilisateur",
thirdparty_one: "Tiers",
person_one: "Usager",
acpw_one: "Action d'accompagnement",
},
},
};

View File

@ -1,11 +1,11 @@
<template>
<span class="chill-entity entity-user">
{{ user.label }}
<span class="user-job" v-if="user.user_job !== null"
> ({{ localizeString(user.user_job.label) }})</span
<span class="user-job" v-if="user.user_job !== null">
({{ localizeString(user.user_job.label) }})</span
>
<span class="main-scope" v-if="user.main_scope !== null"
> ({{ localizeString(user.main_scope.name) }})</span
<span class="main-scope" v-if="user.main_scope !== null">
({{ localizeString(user.main_scope.name) }})</span
>
<span
v-if="user.isAbsent"

View File

@ -266,6 +266,27 @@
data-label="{{ form.vars['label']|trans|escape('html_attr') }}"></div>
{% endblock %}
{% block pick_linked_entities_row %}
<div class="row">
<div class="col-md-12">
{{ form_label(form) }}
{{ form_help(form) }}
</div>
</div>
<div class="row justify-content-end">
<div class="col-md-7 col-sm-12">
{{ form_widget(form) }}
</div>
</div>
{% 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-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 %}
{{ form_help(form)}}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>

View File

@ -58,7 +58,6 @@ final readonly class CancelStaleWorkflowHandler
$transitions = $workflowComponent->getEnabledTransitions($workflow);
$transitionApplied = false;
$wasInInitialPosition = 'initial' === $workflow->getStep();
foreach ($transitions as $transition) {
if ($this->willTransitionLeadToFinalNegative($transition, $metadataStore)) {
@ -80,10 +79,6 @@ final readonly class CancelStaleWorkflowHandler
throw new UnrecoverableMessageHandlingException(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId));
}
if ($wasInInitialPosition) {
$this->em->remove($workflow);
}
$this->em->flush();
}

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

@ -313,10 +313,24 @@ final class AccompanyingCourseApiController extends ApiController
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingPeriod);
$works = $this->accompanyingPeriodWorkRepository->findBy(['accompanyingPeriod' => $accompanyingPeriod]);
dump($works);
$total = $this->accompanyingPeriodWorkRepository->countByAccompanyingPeriod($accompanyingPeriod);
$paginator = $this->getPaginatorFactory()->create($total);
return $this->json($works, Response::HTTP_OK, [], ['groups' => ['read']]);
$works = $this->accompanyingPeriodWorkRepository->findByAccompanyingPeriodOpenFirst(
$accompanyingPeriod,
[
'types' => [],
'before' => null,
'after' => null,
'user' => [],
],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
$collection = new Collection($works, $paginator);
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['read']]);
}
public function workApi($id, Request $request, string $_format): Response

View File

@ -0,0 +1,131 @@
<?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\Controller;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Form\FindAccompanyingPeriodWorkType;
use Chill\PersonBundle\Form\PersonConfimDuplicateType;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Service\AccompanyingPeriodWork\AccompanyingPeriodWorkMergeService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatorInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class AccompanyingPeriodWorkDuplicateController extends AbstractController
{
public function __construct(private readonly AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository, private readonly TranslatorInterface $translator, private readonly AccompanyingPeriodWorkMergeService $accompanyingPeriodWorkMergeService) {}
/**
* @ParamConverter("accompanyingPeriodWork", options={"id": "acpw_id"})
*/
#[Route(path: '{_locale}/person/accompanying-period/work/{id}/assign-duplicate', name: 'chill_person_accompanying_period_work_assign_duplicate')]
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request)
{
$accompanyingPeriod = $acpw->getAccompanyingPeriod();
$this->denyAccessUnlessGranted(
'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE',
$acpw,
'You are not allowed to merge this accompanying period work'
);
$form = $this->createForm(FindAccompanyingPeriodWorkType::class, null, ['accompanyingPeriod' => $accompanyingPeriod]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$acpw2 = $this->accompanyingPeriodWorkRepository->find($form->get('acpw')->getData());
$direction = $form->get('direction')->getData();
if ('starting' === $direction) {
$params = [
'acpw1_id' => $acpw->getId(),
'acpw2_id' => $acpw2->getId(),
];
} else {
$params = [
'acpw1_id' => $acpw2->getId(),
'acpw2_id' => $acpw->getId(),
];
}
return $this->redirectToRoute('chill_person_acpw_duplicate_confirm', $params);
}
return $this->render('@ChillPerson/AccompanyingPeriodWorkDuplicate/assign_acpw_duplicate.html.twig', [
'accompanyingCourse' => $accompanyingPeriod,
// 'acpwArray' => $acpwArray,
'acpw' => $acpw,
'form' => $form->createView(),
]);
}
/**
* @ParamConverter("acpw1", options={"id": "acpw1_id"})
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
*/
#[Route(path: '/{_locale}/person/{acpw1_id}/duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
{
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
try {
$this->validateMerge($acpw1, $acpw2);
$form = $this->createForm(PersonConfimDuplicateType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->accompanyingPeriodWorkMergeService->merge($acpw1, $acpw2);
$session = $request->getSession();
if ($session instanceof Session) {
$session->getFlashBag()->add('success', new TranslatableMessage('acpw_duplicate.Successfully merged'));
}
return $this->redirectToRoute('chill_person_accompanying_period_work_show', ['id' => $acpw1->getId()]);
}
return $this->render('@ChillPerson/AccompanyingPeriodWorkDuplicate/confirm.html.twig', [
'acpw' => $acpw1,
'acpw2' => $acpw2,
'accompanyingCourse' => $accompanyingPeriod,
'form' => $form->createView(),
]);
} catch (\InvalidArgumentException $e) {
$this->addFlash('error', $this->translator->trans($e->getMessage()));
return $this->redirectToRoute('chill_person_accompanying_period_work_assign_duplicate', [
'id' => $acpw1->getId(),
]);
}
}
private function validateMerge(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2): void
{
$constraints = [
[$acpw1 === $acpw2, 'acpw_duplicate.You cannot merge a accompanying period work with itself. Please choose a different one'],
];
foreach ($constraints as [$condition, $message]) {
if ($condition) {
throw new \InvalidArgumentException($message);
}
}
}
}

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

@ -99,14 +99,6 @@ class AccompanyingPeriodWorkGoal
public function setAccompanyingPeriodWork(?AccompanyingPeriodWork $accompanyingPeriodWork): self
{
if (
$this->accompanyingPeriodWork instanceof AccompanyingPeriodWork
&& $accompanyingPeriodWork !== $this->accompanyingPeriodWork
&& null !== $accompanyingPeriodWork
) {
throw new \LogicException('Change accompanying period work is not allowed');
}
$this->accompanyingPeriodWork = $accompanyingPeriodWork;
return $this;

View File

@ -0,0 +1,42 @@
<?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\Form;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Form\Type\PickLinkedAccompanyingPeriodWorkType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FindAccompanyingPeriodWorkType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('acpw', PickLinkedAccompanyingPeriodWorkType::class, [
'label' => 'Social action',
'multiple' => false,
'accompanyingPeriod' => $options['accompanyingPeriod'],
])
->add('direction', HiddenType::class, [
'data' => 'starting',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setRequired('accompanyingPeriod')
->setAllowedTypes('accompanyingPeriod', AccompanyingPeriod::class);
}
}

View File

@ -16,10 +16,8 @@ use Chill\MainBundle\Form\Type\CommentType;
use Chill\PersonBundle\Entity\Person\PersonResource;
use Chill\PersonBundle\Entity\Person\PersonResourceKind;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Chill\PersonBundle\Templating\Entity\ResourceKindRender;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
@ -29,7 +27,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final class PersonResourceType extends AbstractType
{
public function __construct(private readonly ResourceKindRender $resourceKindRender, private readonly PersonRenderInterface $personRender, private readonly ThirdPartyRender $thirdPartyRender, private readonly TranslatorInterface $translator) {}
public function __construct(private readonly ResourceKindRender $resourceKindRender, private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@ -52,13 +50,13 @@ final class PersonResourceType extends AbstractType
},
])
->add('person', PickPersonDynamicType::class, [
'label' => 'Usager',
'label' => $this->translator->trans('person_resource.person_non_prof'),
])
->add('thirdparty', PickThirdpartyDynamicType::class, [
'label' => 'Tiers',
'label' => $this->translator->trans('person_resource.thirdparty_prof'),
])
->add('freetext', ChillTextareaType::class, [
'label' => 'Description libre',
'label' => $this->translator->trans('person_resource.freetext'),
'required' => false,
])
->add('comment', CommentType::class, [

View File

@ -25,21 +25,14 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Class SocialActionType.
*/
class SocialActionType extends AbstractType
{
/**
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
public function __construct(TranslatableStringHelper $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@ -64,6 +57,7 @@ class SocialActionType extends AbstractType
->add('results', EntityType::class, [
'class' => Result::class,
'required' => false,
'label' => $this->translator->trans('person_admin.social_result'),
'multiple' => true,
'attr' => ['class' => 'select2'],
'choice_label' => fn (Result $r) => $this->translatableStringHelper->localize($r->getTitle()),
@ -74,6 +68,7 @@ class SocialActionType extends AbstractType
'required' => false,
'multiple' => true,
'attr' => ['class' => 'select2'],
'label' => $this->translator->trans('person_admin.social_goal'),
'choice_label' => fn (Goal $g) => $this->translatableStringHelper->localize($g->getTitle()),
])
@ -82,6 +77,7 @@ class SocialActionType extends AbstractType
'required' => false,
'multiple' => true,
'attr' => ['class' => 'select2'],
'label' => $this->translator->trans('person_admin.social_evaluation'),
'choice_label' => fn (Evaluation $e) => $this->translatableStringHelper->localize($e->getTitle()),
])

View File

@ -0,0 +1,51 @@
<?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\Form\Type;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PickLinkedAccompanyingPeriodWorkType extends AbstractType
{
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['as_id'] = true === $options['as_id'] ? '1' : '0';
$view->vars['submit_on_adding_new_entity'] = false;
$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('as_id', false)
->setAllowedTypes('as_id', ['bool'])
->setDefault('submit_on_adding_new_entity', false)
->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
}
public function getBlockPrefix()
{
return 'pick_linked_entities';
}
}

View File

@ -0,0 +1,49 @@
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

@ -1,15 +1,23 @@
import { StoredObject } from "ChillDocStoreAssets/types";
import {
Address,
Address,
Scope,
Center,
Civility,
DateTime,
Household,
ThirdParty,
User,
UserGroup,
WorkflowAvailable,
} from "../../../ChillMainBundle/Resources/public/types";
Job,
Household,
User,
UserGroup,
WorkflowAvailable,
ThirdParty,
PrivateCommentEmbeddable,
} from "ChillMainAssets/types";
import { StoredObject } from "ChillDocStoreAssets/types";
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
export interface Person {
id: number;
@ -32,6 +40,42 @@ export interface Person {
current_residential_addresses: Address[];
}
export interface AccompanyingPeriod {
id: number;
addressLocation?: Address | null;
administrativeLocation?: Location | null;
calendars: Calendar[];
closingDate?: Date | null;
closingMotive?: ClosingMotive | null;
comments: Comment[];
confidential: boolean;
createdAt?: Date | null;
createdBy?: User | null;
emergency: boolean;
intensity?: "occasional" | "regular";
job?: Job | null;
locationHistories: AccompanyingPeriodLocationHistory[];
openingDate?: Date | null;
origin?: Origin | null;
participations: AccompanyingPeriodParticipation[];
personLocation?: Person | null;
pinnedComment?: Comment | null;
preventUserIsChangedNotification: boolean;
remark: string;
requestorAnonymous: boolean;
requestorPerson?: Person | null;
requestorThirdParty?: Thirdparty | null;
resources: AccompanyingPeriodResource[];
scopes: Scope[];
socialIssues: SocialIssue[];
step?:
| "CLOSED"
| "CONFIRMED"
| "CONFIRMED_INACTIVE_SHORT"
| "CONFIRMED_INACTIVE_LONG"
| "DRAFT";
}
export interface AccompanyingPeriodWorkEvaluationDocument {
id: number;
type: "accompanying_period_work_evaluation_document";
@ -45,6 +89,175 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
workflows: object[];
}
export interface AccompanyingPeriodWork {
id: number;
accompanyingPeriod?: AccompanyingPeriod;
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
createdAt?: string;
createdAutomatically: boolean;
createdAutomaticallyReason: string;
createdBy: User;
endDate?: string;
goals: AccompanyingPeriodWorkGoal[];
handlingThierParty?: Thirdparty;
note: string;
persons: Person[];
privateComment: PrivateCommentEmbeddable;
referrersHistory: AccompanyingPeriodWorkReferrerHistory[];
results: Result[];
socialAction?: SocialAction;
startDate?: string;
thirdParties: Thirdparty[];
updatedAt?: string;
updatedBy: User;
version: number;
}
interface SocialAction {
id: number;
parent?: SocialAction | null;
children: SocialAction[];
issue?: SocialIssue | null;
ordering: number;
title: {
fr: string;
};
defaultNotificationDelay?: string | null;
desactivationDate?: string | null;
evaluations: Evaluation[];
goals: Goal[];
results: Result[];
}
export interface AccompanyingPeriodResource {
id: number;
accompanyingPeriod: AccompanyingPeriod;
comment?: string | null;
person?: Person | null;
thirdParty?: Thirdparty | null;
}
export interface Origin {
id: number;
label: {
fr: string;
};
noActiveAfter: DateTime;
}
export interface ClosingMotive {
id: number;
active: boolean;
name: {
fr: string;
};
ordering: number;
isCanceledAccompanyingPeriod: boolean;
parent?: ClosingMotive | null;
children: ClosingMotive[];
}
export interface AccompanyingPeriodParticipation {
id: number;
startDate: DateTime;
endDate?: DateTime | null;
accompanyingPeriod: AccompanyingPeriod;
person: Person;
}
export interface AccompanyingPeriodLocationHistory {
id: number;
startDate: DateTime;
endDate?: DateTime | null;
addressLocation?: Address | null;
period: AccompanyingPeriod;
personLocation?: Person | null;
}
export interface SocialIssue {
id: number;
parent?: SocialIssue | null;
children: SocialIssue[];
socialActions?: SocialAction[] | null;
ordering: number;
title: {
fr: string;
};
desactivationDate?: string | null;
}
export interface Goal {
id: number;
results: Result[];
socialActions?: SocialAction[] | null;
title: {
fr: string;
};
}
export interface Result {
id: number;
accompanyingPeriodWorks: AccompanyingPeriodWork[];
accompanyingPeriodWorkGoals: AccompanyingPeriodWorkGoal[];
goals: Goal[];
socialActions: SocialAction[];
title: {
fr: string;
};
desactivationDate?: string | null;
}
export interface AccompanyingPeriodWorkGoal {
id: number;
accompanyingPeriodWork: AccompanyingPeriodWork;
goal: Goal;
note: string;
results: Result[];
}
export interface AccompanyingPeriodWorkEvaluation {
accompanyingPeriodWork: AccompanyingPeriodWork | null;
comment: string;
createdAt: DateTime | null;
createdBy: User | null;
documents: AccompanyingPeriodWorkEvaluationDocument[];
endDate: DateTime | null;
evaluation: Evaluation | null;
id: number | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
key: any;
maxDate: DateTime | null;
startDate: DateTime | null;
updatedAt: DateTime | null;
updatedBy: User | null;
warningInterval: string | null;
timeSpent: number | null;
}
export interface Evaluation {
id: number;
url: string;
socialActions: SocialAction[];
title: {
fr: string;
};
active: boolean;
delay: string;
notificationDelay: string;
}
export interface AccompanyingPeriodWorkReferrerHistory {
id: number;
accompanyingPeriodWork: AccompanyingPeriodWork;
user: User;
startDate: DateTime;
endDate: DateTime | null;
createdAt: DateTime;
updatedAt: DateTime | null;
createdBy: User;
updatedBy: User | null;
}
export type Entities = (UserGroup | User | Person | ThirdParty | Household) & {
address?: Address | null;
kind?: string;

View File

@ -120,7 +120,7 @@ export default {
return pos;
},
handleScroll(event) {
handleScroll() {
let pos = this.findPos(this.stickyNav);
let top = this.heightSum + this.top - window.scrollY;
//console.log(window.scrollY);

View File

@ -79,8 +79,9 @@
{{ $t("select_time_spent") }}
</option>
<option
v-for="time in timeSpentChoices"
v-for="(time, i) in timeSpentChoices"
:value="time.value"
:key="i"
>
{{ time.text }}
</option>

View File

@ -0,0 +1,60 @@
<template>
<div class="container">
<div class="item-bloc">
<div class="item-row">
<h2 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
<span class="chill-entity entity-social-action">
<span class="badge bg-light text-dark">
{{ acpw?.socialAction?.title.fr }}
</span>
</span>
<ul class="small_in_title columns mt-1">
<li>
<span class="item-key">
{{ trans(ACCOMPANYING_COURSE_WORK_START_DATE) }} :
</span>
<b>{{ formatDate(acpw.startDate) }}</b>
</li>
<li v-if="acpw.endDate">
<span class="item-key">
{{ trans(ACCOMPANYING_COURSE_WORK_END_DATE) }} :
</span>
<b>{{ formatDate(acpw.endDate) }}</b>
</li>
</ul>
</span>
</h2>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ACCOMPANYING_COURSE_WORK_END_DATE,
ACCOMPANYING_COURSE_WORK_START_DATE,
trans,
} from "translator";
import { ISOToDate } from "ChillMainAssets/chill/js/date";
import { DateTime } from "ChillMainAssets/types";
import { AccompanyingPeriodWork } from "../../../types";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{ acpw: AccompanyingPeriodWork }>();
const formatDate = (dateObject: DateTime) => {
if (dateObject) {
const parsedDate = ISOToDate(dateObject.datetime);
if (parsedDate) {
return new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(
parsedDate,
);
} else {
return "";
}
}
};
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="results">
<div
v-for="acpw in accompanyingPeriodWorks"
:key="acpw.id"
class="list-item"
>
<label class="acpw-item">
<div>
<input
type="radio"
:value="acpw"
v-model="selectedAcpw"
name="item"
/>
</div>
<accompanying-period-work-item :acpw="acpw" />
</label>
</div>
</div>
</template>
<script setup lang="ts">
import AccompanyingPeriodWorkItem from "./AccompanyingPeriodWorkItem.vue";
import { AccompanyingPeriodWork } from "../../../types";
import { defineProps, ref, watch } from "vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
accompanyingPeriodWorks: AccompanyingPeriodWork[];
}>();
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
// eslint-disable-next-line vue/valid-define-emits
const emit = defineEmits();
watch(selectedAcpw, (newValue) => {
emit("update:selectedAcpw", newValue);
});
</script>
<style>
.acpw-item {
display: flex;
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div>
<div class="row justify-content-end">
<div class="col-md-6 col-sm-10" v-if="selectedAcpw">
<ul class="list-suggest remove-items">
<li>
<span @click="selectedAcpw = null" class="chill-denomination">{{
selectedAcpw?.socialAction?.title.fr
}}</span>
</li>
</ul>
</div>
</div>
<ul class="record_actions">
<li>
<a class="btn btn-sm btn-create mt-3" @click="openModal">
{{ trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK) }}
</a>
</li>
</ul>
<teleport to="body">
<modal
v-if="showModal"
@close="closeModal"
modal-dialog-class="modal-dialog-scrollable modal-xl"
>
<template #header>
<h3>{{ trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK) }}</h3>
</template>
<template #body>
<accompanying-period-work-list
:accompanying-period-works="accompanyingPeriodWorks"
v-model:selectedAcpw="selectedAcpw"
/>
</template>
<template #footer>
<button type="button" class="btn btn-save" @click="confirmSelection">
{{ trans(CONFIRM) }}
</button>
</template>
</modal>
</teleport>
</div>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import AccompanyingPeriodWorkList from "./AccompanyingPeriodWorkList.vue";
import {AccompanyingPeriodWork} from "../../../types";
import {ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK, CONFIRM, trans,} from "translator";
import {fetchResults} from "ChillMainAssets/lib/api/apiMethods";
interface AccompanyingPeriodWorkSelectorModalProps {
accompanyingPeriodId: number;
}
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
const showModal = ref(false);
const accompanyingPeriodWorks = ref<AccompanyingPeriodWork[]>([]);
const props = defineProps<AccompanyingPeriodWorkSelectorModalProps>();
const emit = defineEmits<{
pickWork: [payload: { work: AccompanyingPeriodWork | null }];
}>();
onMounted(() => {
if (props.accompanyingPeriodId) {
getAccompanyingPeriodWorks(props.accompanyingPeriodId);
} else {
console.error("No accompanyingperiod id was given");
}
});
const getAccompanyingPeriodWorks = async (periodId: number) => {
const url = `/api/1.0/person/accompanying-course/${periodId}/works.json`;
try {
accompanyingPeriodWorks.value = await fetchResults(url);
} catch (error) {
console.log(error);
}
/* makeFetch<number, AccompanyingPeriodWork[]>("GET", url)
.then((response) => {
accompanyingPeriodWorks.value = response;
})
.catch((error) => {
console.log(error);
});*/
};
const openModal = () => (showModal.value = true);
const closeModal = () => (showModal.value = false);
const confirmSelection = () => {
emit("pickWork", { work: selectedAcpw.value });
closeModal();
};
</script>

View File

@ -68,6 +68,14 @@
{% endif %}
</div>
</li>
{% if work.accompanyingPeriod.getWorks|length > 1 %}
<li>
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_assign_duplicate', { 'id': work.id }) }}">
<i class="bi bi-chevron-contract"></i>
{{ 'Merge'|trans }}
</a>
</li>
{% endif %}
{% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', work) %}
<li>
<a class="btn btn-edit"

View File

@ -0,0 +1,9 @@
{%- macro details(w, accompanyingCourse, options) -%}
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with {
'displayAction': false,
'displayContent': 'short',
'displayFontSmall': true,
'itemBlocClass': '',
'displayNotification': false
} %}
{% endmacro %}

View File

@ -0,0 +1,53 @@
{% extends '@ChillPerson/AccompanyingCourse/layout.html.twig' %}
{% set activeRouteKey = 'chill_person_accompanying_period_work_assign_duplicate' %}
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
{% block content %}
<div class="person-duplicate">
<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) }}
<div id="linked-acpw-selector" data-accompanying-period='{{ accompanyingCourse.id }}'></div>
{% endif %}
{{ form_rest(form) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_accompanying_period_work_show', {'id' : acpw.id}) }}" class="btn btn-cancel">
{{ 'Return'|trans }}
</a>
</li>
<li>
<button class="btn btn-action" type="submit">{{ 'Next'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
</div>
{% 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

@ -0,0 +1,72 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
{% block title %}{{ 'acpw_duplicate.title'|trans }}{% endblock %}
{% block content %}
<div class="container-fluid content"><div class="duplicate-content">
<h1>{{ 'acpw_duplicate.title'|trans }}</h1>
<div class="col-md-11 border mb-3 mt-3 p-2">
<p>{{ 'acpw_duplicate.description'|trans }}</p>
</div>
<div class="col-md-11">
<div class="col">
<h4>{{ 'acpw_duplicate.to delete'|trans ~ ':' }}</h4>
<div class="accompanying-course-work">
<div class="flex-table">
{{ details.details(acpw2, accompanyingCourse) }}
</div>
</div>
</div>
</div>
<div class="col-md-11 mt-3">
<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>
</div>
{{ form_start(form) }}
<div class="col-md-12 centered">
<div class="container-fluid" style="padding-top: 1em;">
<div class="clear" style="padding-top: 10px;">
{{ form_widget(form.confirm) }}
</div>
<div class="col-11">
{{ form_label(form.confirm) }}
</div>
</div>
</div>
<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-cancel">
{{ 'Return'|trans }}
</a>
</li>
<li class="cancel">
<a href="{{ path('chill_person_acpw_duplicate_confirm', { acpw1_id : acpw2.id, acpw2_id : acpw.id }) }}"
class="btn btn-action">
<i class="fa fa-exchange"></i>
{{ 'Invert'|trans }}
</a>
</li>
<li>
<button class="btn btn-submit" type="submit"><i class="bi bi-chevron-contract"></i>{{ 'Merge'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
</div></div>
{% endblock %}

View File

@ -96,11 +96,11 @@
</div>
{% endif %}
{% if acp.currentParticipations|length > 1 %}
{% if acp.currentParticipations|length > 1 or (acp.currentParticipations|first).person is not same as person %}
<div class="wl-row">
<div class="wl-col title">
<h3 class="participants">
{{ 'Participants'|trans }}
{{ 'accompanying_period.Participants_without_count'|trans({count: acp.currentParticipations|length}) }}
</h3>
</div>
<div class="wl-col list">

View File

@ -10,28 +10,28 @@
<div class="form-check">
{% if resource is defined and resource.person is not null %}
<input checked type="radio" id="chill_personbundle_person_resource_linkedEntity_0" name="linked-entity" class="form-check-input" value="person" {% if form.vars.submitted and app.request.request.get('linked-entity', null) == 'person' %}checked{% endif %}/>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_0">Usager</label>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_0">{{ 'person_resource.person_non_prof'|trans }}</label>
{% else %}
<input type="radio" id="chill_personbundle_person_resource_linkedEntity_0" name="linked-entity" class="form-check-input" value="person" />
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_0">Usager</label>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_0">{{ 'person_resource.person_non_prof'|trans }}</label>
{% endif %}
</div>
<div class="form-check">
{% if resource is defined and resource.thirdparty is not null %}
<input checked type="radio" id="chill_personbundle_person_resource_linkedEntity_1" name="linked-entity" class="form-check-input" value="thirdparty" {% if form.vars.submitted and app.request.request.get('linked-entity', null) == 'thirdparty' %}checked{% endif %}/>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_1">Tiers</label>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_1">{{ 'person_resource.thirdparty_prof'|trans }}</label>
{% else %}
<input type="radio" id="chill_personbundle_person_resource_linkedEntity_1" name="linked-entity" class="form-check-input" value="thirdparty" />
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_1">Tiers</label>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_1">{{ 'person_resource.thirdparty_prof'|trans }}</label>
{% endif %}
</div>
<div class="form-check">
{% if resource is defined and resource.freeText is not null %}
<input checked type="radio" id="chill_personbundle_person_resource_linkedEntity_2" name="linked-entity" class="form-check-input" value="freetext" {% if form.vars.submitted and app.request.request.get('linked-entity', null) == 'thirdparty' %}checked{% endif %}/>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_2">Description libre</label>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_2">{{ 'person_resource.freetext'|trans }}</label>
{% else %}
<input type="radio" id="chill_personbundle_person_resource_linkedEntity_2" name="linked-entity" class="form-check-input" value="freetext" />
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_2">Description libre</label>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_2">{{ 'person_resource.freetext'|trans }}</label>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,112 @@
<?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\Service\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Doctrine\ORM\EntityManagerInterface;
/**
* Service for merging two AccompanyingPeriodWork entities into a single entity.
*/
class AccompanyingPeriodWorkMergeService
{
public function __construct(private readonly EntityManagerInterface $em) {}
/**
* 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): AccompanyingPeriodWork
{
$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);
});
return $toKeep;
}
private function transferWorkflowsSQL(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
$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'",
['toKeepId' => $toKeep->getId(), 'toDeleteId' => $toDelete->getId()]
);
}
private function alterStartDate(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
$startDate = min($toKeep->getStartDate(), $toDelete->getStartDate());
$toKeep->setStartDate($startDate);
}
private function alterEndDate(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
if (null === $toKeep->getEndDate() || null === $toDelete->getEndDate()) {
$toKeep->setEndDate(null);
return;
}
$endDate = max($toKeep->getEndDate(), $toDelete->getEndDate());
$toKeep->setEndDate($endDate);
}
private function concatenateComments(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
$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);
}
foreach ($toDelete->getGoals() as $goal) {
$toKeep->addGoal($goal);
}
foreach ($toDelete->getResults() as $result) {
$toKeep->addResult($result);
}
}
}

View File

@ -0,0 +1,205 @@
<?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\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal;
use Chill\PersonBundle\Entity\SocialWork\Result;
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());
$toDelete->addGoal($goalA = new AccompanyingPeriodWorkGoal());
$toDelete->addResult($resultA = new Result());
$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));
self::assertTrue($accompanyingPeriodWork->getGoals()->contains($goalA));
self::assertTrue($accompanyingPeriodWork->getResults()->contains($resultA));
}
}

View File

@ -6,7 +6,6 @@ module.exports = function (encore, entries) {
encore.addAliases({
ChillPersonAssets: __dirname + "/Resources/public",
});
encore.addEntry(
"vue_household_members_editor",
__dirname + "/Resources/public/vuejs/HouseholdMembersEditor/index.js",
@ -31,7 +30,6 @@ module.exports = function (encore, entries) {
"vue_export_action_goal_result",
__dirname + "/Resources/public/vuejs/ExportFormActionGoalResult/index.js",
);
encore.addEntry(
"mod_set_referrer",
__dirname + "/Resources/public/mod/AccompanyingPeriod/setReferrer.js",
@ -66,4 +64,8 @@ module.exports = function (encore, entries) {
"page_create_person",
__dirname + "/Resources/public/page/person/create-person.js",
);
encore.addEntry(
"mod_duplicate_selector",
__dirname + "/Resources/public/mod/DuplicateSelector/AccompanyingPeriodWorkSelector.ts",
);
};

View File

@ -13,6 +13,14 @@ Requestor: >-
neutral {Demandeur·euse}
}
accompanying_period:
Participants_without_count: >-
{count, plural,
=0 {Participant}
=1 {Participant}
other {Participants}
}
person:
from_the: depuis le
And himself: >-

View File

@ -208,7 +208,7 @@ Pediod closing form is not valid: Le formulaire n'est pas valide
Accompanying user: Accompagnant
No accompanying user: Aucun accompagnant
No data given: Pas d'information
Participants: Usagers impliquées
Participants: Usagers impliqués
Create an accompanying course: Créer un parcours
Accompanying courses of users: Parcours des utilisateurs
This accompanying course is still a draft: Ce parcours est encore à l'état brouillon.
@ -265,6 +265,10 @@ no comment found: "Aucun commentaire"
Select a type: "Choisissez un type"
Select a person: "Choisissez un usager"
Kind: "Type"
person_resource:
person_non_prof: "Usager/ Tiers non-professionnel"
thirdparty_prof: "Tiers professionnel"
freetext: "Description libre"
# pickAPersonType
@ -1498,6 +1502,18 @@ entity_display_title:
Work (n°%w%): "Action d'accompagnement (n°%w%)"
Accompanying Course (n°%w%): "Parcours d'accompagnement (n°%w%)"
acpw_duplicate:
title: Fusionner les actions d'accompagnement
description: Cette fusion conservera la date de début la plus ancienne, la date de fin la plus récente, toutes les évaluations, documents et workflows. Les agents traitants seront additionnés ainsi que les tiers intervenants. Les commentaires seront mis l'un à la suite de l'autre.
Select accompanying period work: Selectionner un action d'accompagnement
Assign duplicate: Désigner un action d'accompagnement doublon
Accompanying period work to delete: Action d'accompagnement à supprimer
Accompanying period work to delete explanation: Cet action d'accompagnement sera supprimé.
Accompanying period work to keep: Action d'accompagnement à conserver
to keep: Action d'accompagnement à conserver
to delete: Action d'accompagnement à supprimer
Successfully merged: Action d'accompagnement fusionnée avec succès.
my_parcours_filters:
referrer_parcours_and_acpw: Agent traitant ou réferent
referrer_acpw: Agent traitant d'une action

View File

@ -0,0 +1,47 @@
import {
Address,
Center,
Civility,
DateTime,
User,
} from "ChillMainAssets/types";
export interface Thirdparty {
acronym: string | null;
active: boolean;
address: Address | null;
canonicalized: string | null;
categories: ThirdpartyCategory[];
centers: Center[];
children: Thirdparty[];
civility: Civility | null;
comment: string | null;
contactDataAnonymous: boolean;
createdAt: DateTime;
createdBy: User | null;
email: string | null;
firstname: string | null;
id: number | null;
kind: string;
name: string;
nameCompany: string | null;
parent: Thirdparty | null;
profession: string;
telephone: string | null;
thirdPartyTypes: ThirdpartyType[] | null;
updatedAt: DateTime | null;
updatedBy: User | null;
}
interface ThirdpartyType {
key: string;
value: string;
}
export interface ThirdpartyCategory {
id: number;
active: boolean;
name: {
fr: string;
};
}

View File

@ -1,8 +1,6 @@
import { is_object_ready } from "../../../../../../ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers";
import {
StoredObject,
StoredObjectStatus,
StoredObjectStatusChange,
} from "../../../../../../ChillDocStoreBundle/Resources/public/types";
async function reload_if_needed(

View File

@ -14,6 +14,15 @@
"config/routes/annotations.yaml"
]
},
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
}
},
"doctrine/doctrine-bundle": {
"version": "2.13",
"recipe": {