Merge branch 'master' into 'issue378_normalize_requestor'

# Conflicts:
#   CHANGELOG.md
This commit is contained in:
Julien Fastré 2022-02-11 14:31:53 +00:00
commit e157624479
58 changed files with 598 additions and 431 deletions

View File

@ -12,6 +12,24 @@ and this project adheres to
<!-- write down unreleased development here --> <!-- write down unreleased development here -->
* fix normalisation of accompanying course requestor api (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/378) * fix normalisation of accompanying course requestor api (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/378)
* [person] add a returnPath when clicking on some Person or ThirdParty badge (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/427)
* [person] accompanying course work: fix on-the-fly update of thirdParty
* [on-the-fly] close modal only after validation
* [person] correct thirdparty PATCH url + add email and altnames in AddPerson and serializer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/433)
* change order for accompanying course work list
* [person]: style fix in parcours listing per person. (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/432)
* [household]: display address of current household (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/415)
* ajoute un ordre dans les localisation (api)
* [pick entity]: fix translations in modal (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/419)
* [homepage_widget]: fix translation on emergency badge (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/440)
* [person]: create person and household added to button dropdown (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/454)
* display full address in address.text in normalization. Adapt AddressRenderBox
* [address]: Correction residential address 'depuis le' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/459)
## Test releases
### test release 2021-02-01
* renommer "dossier numéro" en "parcours numéro" dans les résultats de recherche * renommer "dossier numéro" en "parcours numéro" dans les résultats de recherche
* renomme date de début en date d'ouverture dans le formulaire parcours * renomme date de début en date d'ouverture dans le formulaire parcours
* [homepage widget] improve content tables, improve counter pluralization with style on number * [homepage widget] improve content tables, improve counter pluralization with style on number
@ -30,9 +48,6 @@ and this project adheres to
* [fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413 * [fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413
* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc. * [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
## Test releases
### test release 2021-01-31 ### test release 2021-01-31
* [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409) * [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409)

View File

@ -927,15 +927,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php path: src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php
-
message:
"""
#^Parameter \\$trans of method Chill\\\\PersonBundle\\\\Controller\\\\AccompanyingCourseWorkController\\:\\:__construct\\(\\) has typehint with deprecated interface Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\:
since Symfony 4\\.2, use Symfony\\\\Contracts\\\\Translation\\\\TranslatorInterface instead$#
"""
count: 1
path: src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php
- -
message: message:
""" """

View File

@ -97,7 +97,6 @@ class CalendarType extends AbstractType
return $res; return $res;
}, },
static function (?string $dateAsString): DateTimeImmutable { static function (?string $dateAsString): DateTimeImmutable {
return new DateTimeImmutable($dateAsString); return new DateTimeImmutable($dateAsString);
} }
)); ));

View File

@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller; namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/** /**
@ -19,7 +21,7 @@ use Symfony\Component\HttpFoundation\Request;
*/ */
class LocationApiController extends ApiController class LocationApiController extends ApiController
{ {
public function customizeQuery(string $action, Request $request, $query): void protected function customizeQuery(string $action, Request $request, $query): void
{ {
$query $query
->leftJoin('e.locationType', 'lt') ->leftJoin('e.locationType', 'lt')
@ -31,4 +33,14 @@ class LocationApiController extends ApiController
) )
); );
} }
/**
* @param QueryBuilder $query
* @param mixed $_format
*/
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format)
{
return $query
->addOrderBy('e.name', 'ASC');
}
} }

View File

@ -236,6 +236,10 @@ class NotificationController extends AbstractController
'_fragment' => 'comment-' . $commentId, '_fragment' => 'comment-' . $commentId,
]); ]);
} }
if ($editedCommentForm->isSubmitted() && !$editedCommentForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
} }
} }
@ -257,6 +261,10 @@ class NotificationController extends AbstractController
'id' => $notification->getId(), 'id' => $notification->getId(),
]); ]);
} }
if ($appendCommentForm->isSubmitted() && !$appendCommentForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
} }
} }

View File

@ -167,6 +167,7 @@ class WorkflowController extends AbstractController
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow); $handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$errors = [];
if (count($workflow->getEnabledTransitions($entityWorkflow)) > 0) { if (count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
// possible transition // possible transition
@ -245,7 +246,7 @@ class WorkflowController extends AbstractController
'handler_template_data' => $handler->getTemplateData($entityWorkflow), 'handler_template_data' => $handler->getTemplateData($entityWorkflow),
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null, 'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
'entity_workflow' => $entityWorkflow, 'entity_workflow' => $entityWorkflow,
'transition_form_errors' => $errors ?? [], 'transition_form_errors' => $errors,
//'comment_form' => $commentForm->createView(), //'comment_form' => $commentForm->createView(),
] ]
); );

View File

@ -18,6 +18,7 @@ use DateTimeInterface;
use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs; use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* @ORM\Entity * @ORM\Entity
@ -28,6 +29,7 @@ class NotificationComment implements TrackCreationInterface, TrackUpdateInterfac
{ {
/** /**
* @ORM\Column(type="text") * @ORM\Column(type="text")
* @Assert\NotBlank(message="notification.Comment content might not be blank")
*/ */
private string $content = ''; private string $content = '';
@ -136,9 +138,9 @@ class NotificationComment implements TrackCreationInterface, TrackUpdateInterfac
$this->recentlyPersisted = true; $this->recentlyPersisted = true;
} }
public function setContent(string $content): self public function setContent(?string $content): self
{ {
$this->content = $content; $this->content = (string) $content;
return $this; return $this;
} }

View File

@ -135,6 +135,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
if (!$this->steps->contains($step)) { if (!$this->steps->contains($step)) {
$this->steps[] = $step; $this->steps[] = $step;
$step->setEntityWorkflow($this); $step->setEntityWorkflow($this);
$this->stepsChainedCache = null;
} }
return $this; return $this;
@ -332,32 +333,26 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
public function isFinal(): bool public function isFinal(): bool
{ {
$steps = $this->getStepsChained(); foreach ($this->getStepsChained() as $step) {
if ($step->isFinal()) {
if (1 === count($steps)) { return true;
// the initial step cannot be finalized }
return false;
} }
/** @var EntityWorkflowStep $last */ return false;
$last = end($steps);
return $last->isFinal();
} }
public function isFreeze(): bool public function isFreeze(): bool
{ {
$steps = $this->getStepsChained(); $steps = $this->getStepsChained();
if (1 === count($steps)) { foreach ($this->getStepsChained() as $step) {
// the initial step cannot be finalized if ($step->isFreezeAfter()) {
return false; return true;
}
} }
/** @var EntityWorkflowStep $last */ return false;
$last = end($steps);
return $last->getPrevious()->isFreezeAfter();
} }
public function isUserSubscribedToFinal(User $user): bool public function isUserSubscribedToFinal(User $user): bool
@ -434,7 +429,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
$newStep->setCurrentStep($step); $newStep->setCurrentStep($step);
// copy the freeze // copy the freeze
if ($this->getCurrentStep()->isFreezeAfter()) { if ($this->isFreeze()) {
$newStep->setFreezeAfter(true); $newStep->setFreezeAfter(true);
} }

View File

@ -79,11 +79,8 @@ class NotificationMailer
continue; continue;
} }
$email = new Email();
$email
->subject($notification->getTitle());
if ($notification->isSystem()) { if ($notification->isSystem()) {
$email = new Email();
$email $email
->text($notification->getMessage()); ->text($notification->getMessage());
} else { } else {
@ -96,7 +93,9 @@ class NotificationMailer
]); ]);
} }
$email->to($addressee->getEmail()); $email
->subject($notification->getTitle())
->to($addressee->getEmail());
try { try {
$this->mailer->send($email); $this->mailer->send($email);

View File

@ -56,7 +56,7 @@ class EntityWorkflowStepRepository implements ObjectRepository
public function getClassName() public function getClassName()
{ {
return EntityWorkflow::class; return EntityWorkflowStep::class;
} }
private function buildQueryByUser(User $user): QueryBuilder private function buildQueryByUser(User $user): QueryBuilder

View File

@ -17,7 +17,12 @@ function loadDynamicPicker(element) {
isMultiple = parseInt(el.dataset.multiple) === 1, isMultiple = parseInt(el.dataset.multiple) === 1,
uniqId = el.dataset.uniqid, uniqId = el.dataset.uniqid,
input = element.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'), input = element.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
picked = (isMultiple) ? (JSON.parse(input.value)) : ((input.value === '[]') ? (null) : ([JSON.parse(input.value)])); picked = isMultiple ?
JSON.parse(input.value) : (
(input.value === '[]' || input.value === '') ?
null : [ JSON.parse(input.value) ]
)
;
if (!isMultiple) { if (!isMultiple) {
if (input.value === '[]'){ if (input.value === '[]'){

View File

@ -32,7 +32,7 @@
</span> </span>
</td> </td>
<td> <td>
<span v-if="c.emergency" class="badge rounded-pill bg-danger">{{ $t('emergency') }}</span> <span v-if="c.emergency" class="badge rounded-pill bg-danger me-1">{{ $t('emergency') }}</span>
<span v-if="c.confidential" class="badge rounded-pill bg-danger">{{ $t('confidential') }}</span> <span v-if="c.confidential" class="badge rounded-pill bg-danger">{{ $t('confidential') }}</span>
</td> </td>
<td> <td>
@ -80,5 +80,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
span.badge.rounded-pill.bg-danger {
text-transform: uppercase;
}
</style> </style>

View File

@ -50,7 +50,9 @@ const appMessages = {
assignated_evaluations: "{n} évaluation assignée | {n} évaluations assignées", assignated_evaluations: "{n} évaluation assignée | {n} évaluations assignées",
alert_tasks: "{n} tâche en rappel | {n} tâches en rappel", alert_tasks: "{n} tâche en rappel | {n} tâches en rappel",
warning_tasks: "{n} tâche à échéance | {n} tâches à échéance", warning_tasks: "{n} tâche à échéance | {n} tâches à échéance",
} },
emergency: "Urgent",
confidential: "Confidentiel",
} }
}; };

View File

@ -90,7 +90,7 @@ export default {
OnTheFlyThirdparty, OnTheFlyThirdparty,
OnTheFlyCreate OnTheFlyCreate
}, },
props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'isDead', 'parent', 'canCloseModal'], props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'isDead', 'parent'],
emits: ['saveFormOnTheFly'], emits: ['saveFormOnTheFly'],
data() { data() {
return { return {
@ -160,17 +160,10 @@ export default {
}, },
badgeType() { badgeType() {
return 'entity-' + this.type + ' badge-' + this.type; return 'entity-' + this.type + ' badge-' + this.type;
}
}, },
watch: { getReturnPath() {
canCloseModal: { return `?returnPath=${window.location.pathname}${window.location.search}${window.location.hash}`;
handler: function(val, oldVal) {
if (val) {
this.closeModal();
}
}, },
deep: true
}
}, },
methods: { methods: {
closeModal() { closeModal() {
@ -217,9 +210,9 @@ export default {
buildLocation(id, type) { buildLocation(id, type) {
if (type === 'person') { if (type === 'person') {
// TODO i18n // TODO i18n
return `/fr/person/${id}/general`; return encodeURI(`/fr/person/${id}/general${this.getReturnPath}`);
} else if (type === 'thirdparty') { } else if (type === 'thirdparty') {
return `/fr/3party/3party/${id}/view`; return encodeURI(`/fr/3party/3party/${id}/view${this.getReturnPath}`);
} }
} }
} }

View File

@ -1,4 +1,7 @@
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n'; import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n';
import { thirdpartyMessages } from 'ChillThirdPartyAssets/vuejs/_js/i18n';
import { addressMessages } from 'ChillMainAssets/vuejs/Address/i18n';
import { ontheflyMessages } from 'ChillMainAssets/vuejs/OnTheFly/i18n';
const appMessages = { const appMessages = {
fr: { fr: {
@ -12,6 +15,6 @@ const appMessages = {
} }
} }
Object.assign(appMessages.fr, personMessages.fr); Object.assign(appMessages.fr, personMessages.fr, thirdpartyMessages.fr, addressMessages.fr, ontheflyMessages.fr );
export { appMessages }; export { appMessages };

View File

@ -42,57 +42,11 @@
class="street"> class="street">
{{ address.text }} {{ address.text }}
</p> </p>
<p v-if="address.postcode"
class="postcode">
{{ address.postcode.code }} {{ address.postcode.name }}
</p>
<p v-if="address.country"
class="country">
{{ address.country.name.fr }}
</p>
</div> </div>
</div> </div>
</component> </component>
<!-- <div v-if="isMultiline === true" class="address-more">
<div v-if="address.floor">
<span class="floor">
<b>{{ $t('floor') }}</b>: {{ address.floor }}
</span>
</div>
<div v-if="address.corridor">
<span class="corridor">
<b>{{ $t('corridor') }}</b>: {{ address.corridor }}
</span>
</div>
<div v-if="address.steps">
<span class="steps">
<b>{{ $t('steps') }}</b>: {{ address.steps }}
</span>
</div>
<div v-if="address.flat">
<span class="flat">
<b>{{ $t('flat') }}</b>: {{ address.flat }}
</span>
</div>
<div v-if="address.buildingName">
<span class="buildingName">
<b>{{ $t('buildingName') }}</b>: {{ address.buildingName }}
</span>
</div>
<div v-if="address.extra">
<span class="extra">
<b>{{ $t('extra') }}</b>: {{ address.extra }}
</span>
</div>
<div v-if="address.distribution">
<span class="distribution">
<b>{{ $t('distribution') }}</b>: {{ address.distribution }}
</span>
</div>
</div> -->
<div v-if="useDatePane === true" class="address-more"> <div v-if="useDatePane === true" class="address-more">
<div v-if="address.validFrom"> <div v-if="address.validFrom">
<span class="validFrom"> <span class="validFrom">

View File

@ -91,8 +91,8 @@ export const multiSelectMessages = {
multiselect: { multiselect: {
placeholder: 'Choisir', placeholder: 'Choisir',
tag_placeholder: 'Créer un nouvel élément', tag_placeholder: 'Créer un nouvel élément',
select_label: 'Appuyer sur "Entrée" pour sélectionner', select_label: '"Entrée" ou cliquez pour sélectionner',
deselect_label: 'Appuyer sur "Entrée" pour désélectionner', deselect_label: '"Entrée" ou cliquez pour désélectionner',
select_group_label: 'Appuyer sur "Entrée" pour sélectionner ce groupe', select_group_label: 'Appuyer sur "Entrée" pour sélectionner ce groupe',
deselect_group_label: 'Appuyer sur "Entrée" pour désélectionner ce groupe', deselect_group_label: 'Appuyer sur "Entrée" pour désélectionner ce groupe',
selected_label: 'Sélectionné' selected_label: 'Sélectionné'

View File

@ -32,6 +32,7 @@
{{ form_start(editedCommentForm) }} {{ form_start(editedCommentForm) }}
{{ form_errors(editedCommentForm) }} {{ form_errors(editedCommentForm) }}
{{ form_widget(editedCommentForm.content) }} {{ form_widget(editedCommentForm.content) }}
{{ form_errors(editedCommentForm.content) }}
<input type="hidden" name="form" value="edit" /> <input type="hidden" name="form" value="edit" />
<ul class="record_actions"> <ul class="record_actions">
<li class="cancel"> <li class="cancel">
@ -64,6 +65,7 @@
{{ form_start(appendCommentForm) }} {{ form_start(appendCommentForm) }}
{{ form_errors(appendCommentForm) }} {{ form_errors(appendCommentForm) }}
{{ form_widget(appendCommentForm.content) }} {{ form_widget(appendCommentForm.content) }}
{{ form_errors(appendCommentForm.content) }}
<input type="hidden" name="form" value="append" /> <input type="hidden" name="form" value="append" />
<ul class="record_actions"> <ul class="record_actions">
<li> <li>

View File

@ -64,7 +64,7 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
if ($address instanceof Address) { if ($address instanceof Address) {
$data = [ $data = [
'address_id' => $address->getId(), 'address_id' => $address->getId(),
'text' => $address->isNoAddress() ? null : $this->addressRender->renderStreetLine($address, []), 'text' => $this->addressRender->renderString($address, []),
'street' => $address->getStreet(), 'street' => $address->getStreet(),
'streetNumber' => $address->getStreetNumber(), 'streetNumber' => $address->getStreetNumber(),
'postcode' => [ 'postcode' => [

View File

@ -124,7 +124,7 @@ class AddressRender implements ChillEntityRenderInterface
*/ */
public function renderString($addr, array $options): string public function renderString($addr, array $options): string
{ {
return implode(' - ', $this->renderLines($addr)); return implode(' ', $this->renderLines($addr));
} }
public function supports($entity, array $options): bool public function supports($entity, array $options): bool

View File

@ -24,30 +24,30 @@ final class EntityWorkflowTest extends TestCase
{ {
$entityWorkflow = new EntityWorkflow(); $entityWorkflow = new EntityWorkflow();
$entityWorkflow->getCurrentStep()->setFinalizeAfter(true);
$entityWorkflow->setStep('final'); $entityWorkflow->setStep('final');
$entityWorkflow->getCurrentStep()->setIsFinal(true);
$this->assertTrue($entityWorkflow->isFinalize()); $this->assertTrue($entityWorkflow->isFinal());
} }
public function testIsFinalizeWith4Steps() public function testIsFinalizeWith4Steps()
{ {
$entityWorkflow = new EntityWorkflow(); $entityWorkflow = new EntityWorkflow();
$this->assertFalse($entityWorkflow->isFinalize()); $this->assertFalse($entityWorkflow->isFinal());
$entityWorkflow->setStep('two'); $entityWorkflow->setStep('two');
$this->assertFalse($entityWorkflow->isFinalize()); $this->assertFalse($entityWorkflow->isFinal());
$entityWorkflow->setStep('previous_final'); $entityWorkflow->setStep('previous_final');
$this->assertFalse($entityWorkflow->isFinalize()); $this->assertFalse($entityWorkflow->isFinal());
$entityWorkflow->getCurrentStep()->setFinalizeAfter(true); $entityWorkflow->getCurrentStep()->setIsFinal(true);
$entityWorkflow->setStep('final'); $entityWorkflow->setStep('final');
$this->assertTrue($entityWorkflow->isFinalize()); $this->assertTrue($entityWorkflow->isFinal());
} }
public function testIsFreeze() public function testIsFreeze()
@ -64,11 +64,8 @@ final class EntityWorkflowTest extends TestCase
$this->assertFalse($entityWorkflow->isFreeze()); $this->assertFalse($entityWorkflow->isFreeze());
$entityWorkflow->getCurrentStep()->setFreezeAfter(true);
$this->assertFalse($entityWorkflow->isFreeze());
$entityWorkflow->setStep('freezed'); $entityWorkflow->setStep('freezed');
$entityWorkflow->getCurrentStep()->setFreezeAfter(true);
$this->assertTrue($entityWorkflow->isFreeze()); $this->assertTrue($entityWorkflow->isFreeze());

View File

@ -48,7 +48,7 @@ final class AddressRenderTest extends KernelTestCase
$addr->setBuildingName('Résidence "Les Bleuets"'); $addr->setBuildingName('Résidence "Les Bleuets"');
yield [$addr, 'Résidence "Les Bleuets" - Rue ABC, 5 - 012345 Locality - Belgium']; yield [$addr, 'Résidence "Les Bleuets" — Rue ABC, 5 — 012345 Locality — Belgium'];
} }
public function addressDataProviderBEWithSteps(): Iterator public function addressDataProviderBEWithSteps(): Iterator
@ -68,7 +68,7 @@ final class AddressRenderTest extends KernelTestCase
$addr->setSteps('4'); $addr->setSteps('4');
yield [$addr, 'esc 4 - Rue ABC, 5 - 012345 Locality - Belgium']; yield [$addr, 'esc 4 — Rue ABC, 5 — 012345 Locality — Belgium'];
} }
public function addressDataProviderFRWithBuilding(): Iterator public function addressDataProviderFRWithBuilding(): Iterator
@ -88,7 +88,7 @@ final class AddressRenderTest extends KernelTestCase
$addr->setBuildingName('Résidence "Les Bleuets"'); $addr->setBuildingName('Résidence "Les Bleuets"');
yield [$addr, 'Résidence "Les Bleuets" - 5, Rue ABC - 012345 Locality - France']; yield [$addr, 'Résidence "Les Bleuets" — 5, Rue ABC — 012345 Locality — France'];
} }
public function addressDataProviderFRWithSteps(): Iterator public function addressDataProviderFRWithSteps(): Iterator
@ -108,7 +108,7 @@ final class AddressRenderTest extends KernelTestCase
$addr->setSteps('4'); $addr->setSteps('4');
yield [$addr, 'esc 4 - 5, Rue ABC - 012345 Locality - France']; yield [$addr, 'esc 4 — 5, Rue ABC — 012345 Locality — France'];
} }
public function complexAddressDataProviderBE(): Iterator public function complexAddressDataProviderBE(): Iterator
@ -132,7 +132,7 @@ final class AddressRenderTest extends KernelTestCase
$addr->setCorridor('3'); $addr->setCorridor('3');
$addr->setSteps('4'); $addr->setSteps('4');
yield [$addr, 'Résidence "Les Bleuets" - appart 1 - ét 2 - coul 3 - esc 4 - Rue ABC, 5 - 012345 Locality - Belgium']; yield [$addr, 'Résidence "Les Bleuets" - appart 1 - ét 2 - coul 3 - esc 4 — Rue ABC, 5 — 012345 Locality — Belgium'];
} }
public function complexAddressDataProviderFR(): Iterator public function complexAddressDataProviderFR(): Iterator
@ -158,7 +158,7 @@ final class AddressRenderTest extends KernelTestCase
$addr->setExtra('A droite de la porte'); $addr->setExtra('A droite de la porte');
$addr->setDistribution('CEDEX'); $addr->setDistribution('CEDEX');
yield [$addr, 'appart 1 - ét 2 - coul 3 - esc 4 - Résidence "Les Bleuets" - 5, Rue ABC - A droite de la porte - 012345 Locality CEDEX - France']; yield [$addr, 'appart 1 - ét 2 - coul 3 - esc 4 — Résidence "Les Bleuets" — 5, Rue ABC — A droite de la porte — 012345 Locality CEDEX — France'];
} }
public function noFullAddressDataProviderBE(): Iterator public function noFullAddressDataProviderBE(): Iterator
@ -175,7 +175,7 @@ final class AddressRenderTest extends KernelTestCase
$addr->setPostcode($postCode) $addr->setPostcode($postCode)
->setIsNoAddress(true); ->setIsNoAddress(true);
yield [$addr, '012345 Locality - Belgium']; yield [$addr, '012345 Locality Belgium'];
} }
public function simpleAddressDataProviderBE(): Iterator public function simpleAddressDataProviderBE(): Iterator
@ -193,7 +193,7 @@ final class AddressRenderTest extends KernelTestCase
->setStreetNumber('5') ->setStreetNumber('5')
->setPostcode($postCode); ->setPostcode($postCode);
yield [$addr, 'Rue ABC, 5 - 012345 Locality - Belgium']; yield [$addr, 'Rue ABC, 5 — 012345 Locality — Belgium'];
} }
public function simpleAddressDataProviderFR(): Iterator public function simpleAddressDataProviderFR(): Iterator
@ -211,7 +211,7 @@ final class AddressRenderTest extends KernelTestCase
->setStreetNumber('5') ->setStreetNumber('5')
->setPostcode($postCode); ->setPostcode($postCode);
yield [$addr, '5, Rue ABC - 012345 Locality - France']; yield [$addr, '5, Rue ABC — 012345 Locality — France'];
} }
/** /**

View File

@ -16,36 +16,36 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepRepository; use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepRepository;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface; use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Workflow\Event\Event; use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class WorkflowByUserCounter implements NotificationCounterInterface, EventSubscriberInterface final class WorkflowByUserCounter implements NotificationCounterInterface, EventSubscriberInterface
{ {
private EntityWorkflowStepRepository $workflowStepRepository;
private CacheItemPoolInterface $cacheItemPool; private CacheItemPoolInterface $cacheItemPool;
private EntityWorkflowStepRepository $workflowStepRepository;
public function __construct(EntityWorkflowStepRepository $workflowStepRepository, CacheItemPoolInterface $cacheItemPool) public function __construct(EntityWorkflowStepRepository $workflowStepRepository, CacheItemPoolInterface $cacheItemPool)
{ {
$this->workflowStepRepository = $workflowStepRepository; $this->workflowStepRepository = $workflowStepRepository;
$this->cacheItemPool = $cacheItemPool; $this->cacheItemPool = $cacheItemPool;
} }
public function addNotification(UserInterface $user): int public function addNotification(UserInterface $u): int
{ {
if (!$user instanceof User) { if (!$u instanceof User) {
return 0; return 0;
} }
$key = self::generateCacheKeyWorkflowByUser($user); $key = self::generateCacheKeyWorkflowByUser($u);
$item = $this->cacheItemPool->getItem($key); $item = $this->cacheItemPool->getItem($key);
if ($item->isHit()) { if ($item->isHit()) {
return $item->get(); return $item->get();
} }
$nb = $this->getCountUnreadByUser($user); $nb = $this->getCountUnreadByUser($u);
$item->set($nb) $item->set($nb)
->expiresAfter(60 * 15); ->expiresAfter(60 * 15);
@ -54,14 +54,21 @@ final class WorkflowByUserCounter implements NotificationCounterInterface, Event
return $nb; return $nb;
} }
public static function generateCacheKeyWorkflowByUser(User $user): string
{
return 'chill_main_workflow_by_u_' . $user->getId();
}
public function getCountUnreadByUser(User $user): int public function getCountUnreadByUser(User $user): int
{ {
return $this->workflowStepRepository->countUnreadByUser($user); return $this->workflowStepRepository->countUnreadByUser($user);
} }
public static function generateCacheKeyWorkflowByUser(User $user): string public static function getSubscribedEvents()
{ {
return 'chill_main_workflow_by_u_'.$user->getId(); return [
'workflow.leave' => 'resetWorkflowCache',
];
} }
public function resetWorkflowCache(Event $event): void public function resetWorkflowCache(Event $event): void
@ -74,11 +81,12 @@ final class WorkflowByUserCounter implements NotificationCounterInterface, Event
$entityWorkflow = $event->getSubject(); $entityWorkflow = $event->getSubject();
$step = $entityWorkflow->getCurrentStep(); $step = $entityWorkflow->getCurrentStep();
if ($step === null) { if (null === $step) {
return; return;
} }
$keys = []; $keys = [];
foreach ($step->getDestUser() as $user) { foreach ($step->getDestUser() as $user) {
$keys[] = self::generateCacheKeyWorkflowByUser($user); $keys[] = self::generateCacheKeyWorkflowByUser($user);
} }
@ -86,15 +94,5 @@ final class WorkflowByUserCounter implements NotificationCounterInterface, Event
if ([] !== $keys) { if ([] !== $keys) {
$this->cacheItemPool->deleteItems($keys); $this->cacheItemPool->deleteItems($keys);
} }
} }
public static function getSubscribedEvents()
{
return [
'workflow.leave' => 'resetWorkflowCache',
];
}
} }

View File

@ -72,9 +72,6 @@ class MetadataExtractor
foreach ($transitions as $transition) { foreach ($transitions as $transition) {
if ($transition->getName() === $transitionName) { if ($transition->getName() === $transitionName) {
break;
}
}
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
return [ return [
@ -84,6 +81,10 @@ class MetadataExtractor
'isForward' => $metadata['isForward'] ?? null, 'isForward' => $metadata['isForward'] ?? null,
]; ];
} }
}
return [];
}
public function buildArrayPresentationForWorkflow(WorkflowInterface $workflow): array public function buildArrayPresentationForWorkflow(WorkflowInterface $workflow): array
{ {

View File

@ -27,4 +27,4 @@ address:
notification: notification:
At least one addressee: Indiquez au moins un destinataire At least one addressee: Indiquez au moins un destinataire
Title must be defined: Un titre doit être indiqué Title must be defined: Un titre doit être indiqué
Comment content might not be blank: Le commentaire ne peut pas être vide

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
@ -23,7 +24,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingCourseWorkController extends AbstractController class AccompanyingCourseWorkController extends AbstractController
{ {
@ -60,7 +61,7 @@ class AccompanyingCourseWorkController extends AbstractController
*/ */
public function createWork(AccompanyingPeriod $period): Response public function createWork(AccompanyingPeriod $period): Response
{ {
// TODO ACL $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::CREATE, $period);
if ($period->getSocialIssues()->count() === 0) { if ($period->getSocialIssues()->count() === 0) {
$this->addFlash( $this->addFlash(
@ -93,7 +94,8 @@ class AccompanyingCourseWorkController extends AbstractController
*/ */
public function deleteWork(AccompanyingPeriodWork $work, Request $request): Response public function deleteWork(AccompanyingPeriodWork $work, Request $request): Response
{ {
// TODO ACL $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::UPDATE, $work);
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$form = $this->createDeleteForm($work->getId()); $form = $this->createDeleteForm($work->getId());
@ -138,7 +140,8 @@ class AccompanyingCourseWorkController extends AbstractController
*/ */
public function editWork(AccompanyingPeriodWork $work): Response public function editWork(AccompanyingPeriodWork $work): Response
{ {
// TODO ACL $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::UPDATE, $work);
$json = $this->serializer->normalize($work, 'json', ['groups' => ['read']]); $json = $this->serializer->normalize($work, 'json', ['groups' => ['read']]);
return $this->render('@ChillPerson/AccompanyingCourseWork/edit.html.twig', [ return $this->render('@ChillPerson/AccompanyingCourseWork/edit.html.twig', [
@ -157,13 +160,13 @@ class AccompanyingCourseWorkController extends AbstractController
*/ */
public function listWorkByAccompanyingPeriod(AccompanyingPeriod $period): Response public function listWorkByAccompanyingPeriod(AccompanyingPeriod $period): Response
{ {
// TODO ACL $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period);
$totalItems = $this->workRepository->countByAccompanyingPeriod($period); $totalItems = $this->workRepository->countByAccompanyingPeriod($period);
$paginator = $this->paginator->create($totalItems); $paginator = $this->paginator->create($totalItems);
$works = $this->workRepository->findByAccompanyingPeriod( $works = $this->workRepository->findByAccompanyingPeriodOpenFirst(
$period, $period,
['startDate' => 'DESC', 'endDate' => 'DESC'],
$paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber() $paginator->getCurrentPageFirstItemNumber()
); );

View File

@ -50,7 +50,6 @@ class HouseholdApiController extends ApiController
*/ */
public function getHouseholdByAddressReference(AddressReference $addressReference): Response public function getHouseholdByAddressReference(AddressReference $addressReference): Response
{ {
// TODO ACL
$this->denyAccessUnlessGranted('ROLE_USER'); $this->denyAccessUnlessGranted('ROLE_USER');
$total = $this->householdACLAwareRepository->countByAddressReference($addressReference); $total = $this->householdACLAwareRepository->countByAddressReference($addressReference);

View File

@ -224,6 +224,8 @@ final class PersonController extends AbstractController
'label' => 'Add the person', 'label' => 'Add the person',
])->add('createPeriod', SubmitType::class, [ ])->add('createPeriod', SubmitType::class, [
'label' => 'Add the person and create an accompanying period', 'label' => 'Add the person and create an accompanying period',
])->add('createHousehold', SubmitType::class, [
'label' => 'Add the person and create a household',
]); ]);
$form->handleRequest($request); $form->handleRequest($request);
@ -252,6 +254,12 @@ final class PersonController extends AbstractController
]); ]);
} }
if ($form->get('createHousehold')->isClicked()) {
return $this->redirectToRoute('chill_person_household_members_editor', [
'persons' => [$person->getId()],
]);
}
return $this->redirectToRoute( return $this->redirectToRoute(
'chill_person_general_edit', 'chill_person_general_edit',
['person_id' => $person->getId()] ['person_id' => $person->getId()]

View File

@ -1153,12 +1153,9 @@ class AccompanyingPeriod implements
$this->removeComment($this->pinnedComment); $this->removeComment($this->pinnedComment);
} }
if ($comment instanceof Comment) {
if (null !== $this->pinnedComment) { if (null !== $this->pinnedComment) {
$this->addComment($this->pinnedComment); $this->addComment($this->pinnedComment);
} }
$this->addComment($comment);
}
$this->pinnedComment = $comment; $this->pinnedComment = $comment;

View File

@ -45,7 +45,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/** /**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class) * @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class)
* @Serializer\Groups({"read", "read:accompanyingPeriodWork:light"}) * @Serializer\Groups({"read", "read:accompanyingPeriodWork:light"})
* @Serializer\Context(normalizationContext={"groups"={"read"}}, groups={"read:accompanyingPeriodWork:light"}) * @Serializer\Context(normalizationContext={"groups": {"read"}}, groups={"read:accompanyingPeriodWork:light"})
*/ */
private ?AccompanyingPeriod $accompanyingPeriod = null; private ?AccompanyingPeriod $accompanyingPeriod = null;

View File

@ -13,7 +13,10 @@ namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -24,15 +27,18 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
{ {
protected Registry $registry; protected Registry $registry;
protected Security $security;
/** /**
* @var TranslatorInterface * @var TranslatorInterface
*/ */
protected $translator; protected $translator;
public function __construct(TranslatorInterface $translator, Registry $registry) public function __construct(TranslatorInterface $translator, Registry $registry, Security $security)
{ {
$this->translator = $translator; $this->translator = $translator;
$this->registry = $registry; $this->registry = $registry;
$this->security = $security;
} }
public function buildMenu($menuId, MenuItem $menu, array $parameters): void public function buildMenu($menuId, MenuItem $menu, array $parameters): void
@ -47,18 +53,21 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
], ]) ], ])
->setExtras(['order' => 10]); ->setExtras(['order' => 10]);
if ($this->security->isGranted(AccompanyingPeriodVoter::EDIT, $period)) {
$menu->addChild($this->translator->trans('Edit Accompanying Course'), [ $menu->addChild($this->translator->trans('Edit Accompanying Course'), [
'route' => 'chill_person_accompanying_course_edit', 'route' => 'chill_person_accompanying_course_edit',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
], ]) ], ])
->setExtras(['order' => 20]); ->setExtras(['order' => 20]);
}
if (AccompanyingPeriod::STEP_DRAFT === $period->getStep()) { if (AccompanyingPeriod::STEP_DRAFT === $period->getStep()) {
// no more menu items if the period is draft // no more menu items if the period is draft
return; return;
} }
if ($this->security->isGranted(AccompanyingPeriodVoter::SEE_DETAILS, $period)) {
$menu->addChild($this->translator->trans('Accompanying Course History'), [ $menu->addChild($this->translator->trans('Accompanying Course History'), [
'route' => 'chill_person_accompanying_course_history', 'route' => 'chill_person_accompanying_course_history',
'routeParameters' => [ 'routeParameters' => [
@ -66,19 +75,22 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
], ]) ], ])
->setExtras(['order' => 30]); ->setExtras(['order' => 30]);
$menu->addChild($this->translator->trans('Accompanying Course Action'), [
'route' => 'chill_person_accompanying_period_work_list',
'routeParameters' => [
'id' => $period->getId(),
], ])
->setExtras(['order' => 40]);
$menu->addChild($this->translator->trans('Accompanying Course Comment'), [ $menu->addChild($this->translator->trans('Accompanying Course Comment'), [
'route' => 'chill_person_accompanying_period_comment_list', 'route' => 'chill_person_accompanying_period_comment_list',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
], ]) ], ])
->setExtras(['order' => 50]); ->setExtras(['order' => 50]);
}
if ($this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $period)) {
$menu->addChild($this->translator->trans('Accompanying Course Action'), [
'route' => 'chill_person_accompanying_period_work_list',
'routeParameters' => [
'id' => $period->getId(),
], ])
->setExtras(['order' => 40]);
}
$workflow = $this->registry->get($period, 'accompanying_period_lifecycle'); $workflow = $this->registry->get($period, 'accompanying_period_lifecycle');

View File

@ -16,17 +16,22 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
final class AccompanyingPeriodWorkRepository implements ObjectRepository final class AccompanyingPeriodWorkRepository implements ObjectRepository
{ {
private EntityManagerInterface $em;
private EntityRepository $repository; private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->em = $entityManager;
$this->repository = $entityManager->getRepository(AccompanyingPeriodWork::class); $this->repository = $entityManager->getRepository(AccompanyingPeriodWork::class);
} }
@ -78,6 +83,36 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset); return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset);
} }
/**
* Return a list of accompanying period with a defined order:.
*
* * first, opened works
* * then, closed works
*
* @return AccompanyingPeriodWork[]
*/
public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, int $limit = 10, int $offset = 0): array
{
$rsm = new ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
WHERE accompanyingPeriod_id = :periodId
ORDER BY
CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
startdate DESC,
enddate DESC,
id DESC
LIMIT :limit OFFSET :offset";
$nq = $this->em->createNativeQuery($sql, $rsm)
->setParameter('periodId', $period->getId(), Types::INTEGER)
->setParameter('limit', $limit, Types::INTEGER)
->setParameter('offset', $offset, Types::INTEGER);
return $nq->getResult();
}
public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
{ {
return $this->buildQueryNearEndDateByUser($user, $since, $until) return $this->buildQueryNearEndDateByUser($user, $since, $until)

View File

@ -14,7 +14,7 @@ namespace Chill\PersonBundle\Repository\Household;
use Chill\MainBundle\Entity\AddressReference; use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Security\Authorization\HouseholdVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use DateTime; use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@ -39,7 +39,7 @@ final class HouseholdACLAwareRepository implements HouseholdACLAwareRepositoryIn
{ {
$centers = $this->authorizationHelper->getReachableCenters( $centers = $this->authorizationHelper->getReachableCenters(
$this->security->getUser(), $this->security->getUser(),
HouseholdVoter::SEE PersonVoter::SEE // the authorization to see a household is the same as seeing a person
); );
if ([] === $centers) { if ([] === $centers) {

View File

@ -60,14 +60,14 @@
<template v-slot:body> <template v-slot:body>
<p>{{ $t('confirm.sure_description') }}</p> <p>{{ $t('confirm.sure_description') }}</p>
<div v-if="accompanyingCourse.user === null"> <div v-if="accompanyingCourse.user === null">
<div v-if="filteredReferrersSuggested.length === 0"> <div v-if="usersSuggestedFilteredByJob.length === 0">
<p class="alert alert-warning">{{ $t('confirm.no_suggested_referrer') }}</p> <p class="alert alert-warning">{{ $t('confirm.no_suggested_referrer') }}</p>
</div> </div>
<div v-if="filteredReferrersSuggested.length === 1" class="alert alert-info"> <div v-if="usersSuggestedFilteredByJob.length === 1" class="alert alert-info">
<p>{{ $t('confirm.one_suggested_referrer') }}:</p> <p>{{ $t('confirm.one_suggested_referrer') }}:</p>
<ul class="list-suggest add-items inline"> <ul class="list-suggest add-items inline">
<li> <li>
<user-render-box-badge :user="filteredReferrersSuggested[0]"></user-render-box-badge> <user-render-box-badge :user="usersSuggestedFilteredByJob[0]"></user-render-box-badge>
</li> </li>
</ul> </ul>
<p>{{ $t('confirm.choose_suggested_referrer') }}</p> <p>{{ $t('confirm.choose_suggested_referrer') }}</p>
@ -150,7 +150,6 @@ export default {
computed: { computed: {
...mapState({ ...mapState({
accompanyingCourse: state => state.accompanyingCourse, accompanyingCourse: state => state.accompanyingCourse,
filteredReferrersSuggested: state => state.filteredReferrersSuggested
}), }),
...mapGetters([ ...mapGetters([
'isParticipationValid', 'isParticipationValid',
@ -160,15 +159,16 @@ export default {
'isLocationValid', 'isLocationValid',
'isJobValid', 'isJobValid',
'validationKeys', 'validationKeys',
'isValidToBeConfirmed' 'isValidToBeConfirmed',
'usersSuggestedFilteredByJob',
]), ]),
deleteLink() { deleteLink() {
return `/fr/parcours/${this.accompanyingCourse.id}/delete`; //TODO locale return `/fr/parcours/${this.accompanyingCourse.id}/delete`; //TODO locale
}, },
disableConfirm() { disableConfirm() {
return this.clickedDoNotChooseReferrer return (this.accompanyingCourse.user === null
? (this.accompanyingCourse.user === null && this.filteredReferrersSuggested.length === 0) && this.usersSuggestedFilteredByJob.length === 1
: (this.accompanyingCourse.user === null && this.filteredReferrersSuggested.length === 0) || (this.filteredReferrersSuggested.length === 1); && this.clickedDoNotChooseReferrer === false);
} }
}, },
methods: { methods: {
@ -183,7 +183,7 @@ export default {
}); });
}, },
chooseSuggestedReferrer() { chooseSuggestedReferrer() {
this.$store.dispatch('updateReferrer', this.filteredReferrersSuggested[0]) this.$store.dispatch('updateReferrer', this.usersSuggestedFilteredByJob[0])
.catch(({name, violations}) => { .catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') { if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation})); violations.forEach((violation) => this.$toast.open({message: violation}));

View File

@ -10,6 +10,7 @@
addAge : true, addAge : true,
hLevel : 3, hLevel : 3,
isConfidential : false, isConfidential : false,
isMultiline: true,
}" }"
:person="participation.person" :person="participation.person"
:returnPath="getAccompanyingCourseReturnPath"> :returnPath="getAccompanyingCourseReturnPath">
@ -28,7 +29,7 @@
</a> </a>
</li> </li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="show"></on-the-fly></li> <li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" :canCloseModal="canCloseOnTheFlyModal"></on-the-fly></li> <li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" ref="onTheFly"></on-the-fly></li>
<li> <li>
<button v-if="!participation.endDate" <button v-if="!participation.endDate"
class="btn btn-sm btn-remove" class="btn btn-sm btn-remove"
@ -90,7 +91,6 @@ export default {
hLevel: 1 hLevel: 1
} }
}, },
canCloseOnTheFlyModal: false
} }
}, },
computed: { computed: {
@ -120,12 +120,14 @@ export default {
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; } if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber; body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber; body.mobilenumber = payload.data.mobilenumber;
body.email = payload.data.email;
body.altNames = payload.data.altNames;
body.gender = payload.data.gender; body.gender = payload.data.gender;
makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body) makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body)
.then(response => { .then(response => {
this.$store.dispatch('addPerson', { target: payload.target, body: response }) this.$store.dispatch('addPerson', { target: payload.target, body: response });
this.canCloseOnTheFlyModal = true; this.$refs.onTheFly.closeModal();
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === 'ValidationException') {
@ -143,10 +145,10 @@ export default {
body.telephone = payload.data.phonenumber; body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id }; body.address = { id: payload.data.address.address_id };
makeFetch('PATCH', `/api/1.0/third-party/third-party/${payload.data.id}.json`, body) makeFetch('PATCH', `/api/1.0/thirdparty/thirdparty/${payload.data.id}.json`, body)
.then(response => { .then(response => {
this.$store.dispatch('addThirdparty', { target: payload.target, body: response }) this.$store.dispatch('addThirdparty', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true; this.$refs.onTheFly.closeModal();
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === 'ValidationException') {

View File

@ -21,8 +21,7 @@
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@select="updateJob"> ></VueMultiselect>
</VueMultiselect>
<label class="col-form-label" for="selectReferrer"> <label class="col-form-label" for="selectReferrer">
{{ $t('referrer.label') }} {{ $t('referrer.label') }}
@ -40,12 +39,11 @@
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@select="updateReferrer"> ></VueMultiselect>
</VueMultiselect>
<template v-if="filteredReferrersSuggested.length > 0"> <template v-if="usersSuggestedFilteredByJob.length > 0">
<ul class="list-suggest add-items inline"> <ul class="list-suggest add-items inline">
<li v-for="(u, i) in filteredReferrersSuggested" @click="updateReferrer(u)" :key="`referrer-${i}`"> <li v-for="(u, i) in usersSuggestedFilteredByJob" @click="updateReferrer(u)" :key="`referrer-${i}`">
<span> <span>
<user-render-box-badge :user="u"></user-render-box-badge> <user-render-box-badge :user="u"></user-render-box-badge>
</span> </span>
@ -94,26 +92,44 @@ export default {
}, },
computed: { computed: {
...mapState({ ...mapState({
value: state => state.accompanyingCourse.user,
valueJob: state => state.accompanyingCourse.job, valueJob: state => state.accompanyingCourse.job,
users: state => state.users.filter(u => { }),
if (u.user_job && state.accompanyingCourse.job) { ...mapGetters(['isJobValid', 'usersSuggestedFilteredByJob']),
return u.user_job.id === state.accompanyingCourse.job.id; users: function () {
} else { let users = this.$store.getters.usersFilteredByJob;
return false;
console.log('users filtered by job', users);
// ensure that the selected user is in the list. add it if necessary
if (this.$store.state.accompanyingCourse.user !== null && users.find(u => this.$store.state.accompanyingCourse.user.id === u.id) === undefined) {
console.log('add user to users');
users.push(this.$store.state.accompanyingCourse.user);
} }
}),
filteredReferrersSuggested: state => state.filteredReferrersSuggested, console.log('users to return', users);
}), return users;
...mapGetters([
'isJobValid'
])
}, },
mounted() { valueJob: {
this.getJobs(); get() {
return this.$store.state.accompanyingCourse.job;
}, },
methods: { set(value) {
updateReferrer(value) { this.$store.dispatch('updateJob', value)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
}
},
value: {
get() {
return this.$store.state.accompanyingCourse.user;
},
set(value) {
console.log('set referrer', value);
this.$store.dispatch('updateReferrer', value) this.$store.dispatch('updateReferrer', value)
.catch(({name, violations}) => { .catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') { if (name === 'ValidationException' || name === 'AccessException') {
@ -122,6 +138,15 @@ export default {
this.$toast.open({message: 'An error occurred'}) this.$toast.open({message: 'An error occurred'})
} }
}); });
}
},
},
mounted() {
this.getJobs();
},
methods: {
updateReferrer(value) {
this.value = value;
}, },
getJobs() { getJobs() {
const url = '/api/1.0/main/user-job.json'; const url = '/api/1.0/main/user-job.json';
@ -133,30 +158,19 @@ export default {
this.$toast.open({message: error.txt}) this.$toast.open({message: error.txt})
}) })
}, },
updateJob(value) {
this.$store.dispatch('updateJob', value)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
},
customJobLabel(value) { customJobLabel(value) {
return value.label.fr; return value.label.fr;
}, },
assignMe() { assignMe() {
const url = `/api/1.0/main/whoami.json`; const url = `/api/1.0/main/whoami.json`;
makeFetch('GET', url) makeFetch('GET', url)
.then(response => { .then(user => {
this.$store.dispatch('updateReferrer', response); this.value = user
return response;
}) })
.catch((error) => { /*.catch((error) => {
commit('catchError', error); commit('catchError', error);
this.$toast.open({message: error.body}) this.$toast.open({message: error.body})
}) })*/
} }
} }
} }

View File

@ -26,7 +26,7 @@
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="record_actions"> <ul class="record_actions">
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li> <li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li> <li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" ref="onTheFly"></on-the-fly></li>
</ul> </ul>
</template> </template>
</third-party-render-box> </third-party-render-box>
@ -52,7 +52,7 @@
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="record_actions"> <ul class="record_actions">
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li> <li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li> <li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" ref="onTheFly"></on-the-fly></li>
</ul> </ul>
</template> </template>
</person-render-box> </person-render-box>
@ -92,7 +92,7 @@
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="record_actions"> <ul class="record_actions">
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li> <li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li> <li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" ref="onTheFly"></on-the-fly></li>
</ul> </ul>
</template> </template>
</third-party-render-box> </third-party-render-box>
@ -114,7 +114,7 @@
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="record_actions"> <ul class="record_actions">
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li> <li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" :canCloseModal="canCloseOnTheFlyModal"></on-the-fly></li> <li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" ref="onTheFly"></on-the-fly></li>
</ul> </ul>
</template> </template>
</person-render-box> </person-render-box>
@ -189,7 +189,6 @@ export default {
uniq: true, uniq: true,
} }
}, },
canCloseOnTheFlyModal: false
} }
}, },
computed: { computed: {
@ -261,12 +260,14 @@ export default {
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; } if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber; body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber; body.mobilenumber = payload.data.mobilenumber;
body.email = payload.data.email;
body.altNames = payload.data.altNames;
body.gender = payload.data.gender; body.gender = payload.data.gender;
makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body) makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body)
.then(response => { .then(response => {
this.$store.dispatch('addPerson', { target: payload.target, body: response }) this.$store.dispatch('addPerson', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true; this.$refs.onTheFly.closeModal();
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === 'ValidationException') {
@ -284,10 +285,10 @@ export default {
body.telephone = payload.data.phonenumber; body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id }; body.address = { id: payload.data.address.address_id };
makeFetch('PATCH', `/api/1.0/third-party/third-party/${payload.data.id}.json`, body) makeFetch('PATCH', `/api/1.0/thirdparty/thirdparty/${payload.data.id}.json`, body)
.then(response => { .then(response => {
this.$store.dispatch('addThirdparty', { target: payload.target, body: response }) this.$store.dispatch('addThirdparty', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true; this.$refs.onTheFly.closeModal();
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === 'ValidationException') {

View File

@ -35,7 +35,7 @@
:id="resource.resource.id" :id="resource.resource.id"
action="edit" action="edit"
@saveFormOnTheFly="saveFormOnTheFly" @saveFormOnTheFly="saveFormOnTheFly"
:canCloseModal="canCloseOnTheFlyModal"> ref="onTheFly">
</on-the-fly> </on-the-fly>
</li> </li>
<li> <li>
@ -82,7 +82,7 @@
:id="resource.resource.id" :id="resource.resource.id"
action="edit" action="edit"
@saveFormOnTheFly="saveFormOnTheFly" @saveFormOnTheFly="saveFormOnTheFly"
:canCloseModal="canCloseOnTheFlyModal"> ref="onTheFly">
</on-the-fly> </on-the-fly>
</li> </li>
<li> <li>
@ -116,11 +116,6 @@ export default {
}, },
props: ['resource'], props: ['resource'],
emits: ['remove'], emits: ['remove'],
data() {
return {
canCloseOnTheFlyModal: false
}
},
computed: { computed: {
parent() { parent() {
return { return {
@ -152,12 +147,14 @@ export default {
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; } if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber; body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber; body.mobilenumber = payload.data.mobilenumber;
body.email = payload.data.email;
body.altNames = payload.data.altNames;
body.gender = payload.data.gender; body.gender = payload.data.gender;
makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body) makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body)
.then(response => { .then(response => {
this.$store.dispatch('addPerson', { target: payload.target, body: response }) this.$store.dispatch('addPerson', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true; this.$refs.onTheFly.closeModal();
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === 'ValidationException') {
@ -175,10 +172,10 @@ export default {
body.telephone = payload.data.phonenumber; body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id }; body.address = { id: payload.data.address.address_id };
makeFetch('PATCH', `/api/1.0/third-party/third-party/${payload.data.id}.json`, body) makeFetch('PATCH', `/api/1.0/thirdparty/thirdparty/${payload.data.id}.json`, body)
.then(response => { .then(response => {
this.$store.dispatch('addThirdparty', { target: payload.target, body: response }) this.$store.dispatch('addThirdparty', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true; this.$refs.onTheFly.closeModal();
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === 'ValidationException') {

View File

@ -140,7 +140,7 @@ const appMessages = {
sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !", sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !",
ok: "Confirmer le parcours", ok: "Confirmer le parcours",
delete: "Supprimer le parcours", delete: "Supprimer le parcours",
no_suggested_referrer: "Il n'y a aucun référent qui puisse être désigné pour ce parcours. Vérifiez la localisation du parcours, les métiers et service indiqués. Si le problème persiste, contactez l'administrateur du logiciel.", no_suggested_referrer: "Il n'y a aucun référent qui puisse être suggéré pour ce parcours. Vérifiez la localisation du parcours, les métiers et service indiqués. Si les données sont correctes, vous pouvez confirmer ce parcours.",
one_suggested_referrer: "Un unique référent peut être suggéré pour ce parcours", one_suggested_referrer: "Un unique référent peut être suggéré pour ce parcours",
choose_suggested_referrer: "Voulez-vous le désigner directement ?", choose_suggested_referrer: "Voulez-vous le désigner directement ?",
choose_button: "Désigner", choose_button: "Désigner",

View File

@ -40,7 +40,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
scopesAtBackend: accompanyingCourse.scopes.map(scope => scope), scopesAtBackend: accompanyingCourse.scopes.map(scope => scope),
// the users which are available for referrer // the users which are available for referrer
referrersSuggested: [], referrersSuggested: [],
filteredReferrersSuggested: [],
// all the users available // all the users available
users: [], users: [],
permissions: {} permissions: {}
@ -93,6 +92,30 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
return false; return false;
}, },
usersFilteredByJob(state) {
return state.users.filter(u => {
if (null !== state.accompanyingCourse.job) {
return (u.user_job === null ? null : u.user_job.id) === state.accompanyingCourse.job.id;
} else {
return true;
}
});
},
usersSuggestedFilteredByJob(state) {
return state.referrersSuggested.filter(u => {
if (null !== state.accompanyingCourse.job) {
return (u.user_job === null ? null : u.user_job.id) === state.accompanyingCourse.job.id;
} else {
return true;
}
}).filter(u => {
if (null !== state.accompanyingCourse.user) {
return u.id !== state.accompanyingCourse.user.id;
}
return true;
});
},
}, },
mutations: { mutations: {
catchError(state, error) { catchError(state, error) {
@ -216,22 +239,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
return u; return u;
}); });
}, },
setFilteredReferrersSuggested(state) {
state.filteredReferrersSuggested = state.referrersSuggested.filter(u => {
if (u.user_job && state.accompanyingCourse.job && state.accompanyingCourse.user) {
return u.user_job.id === state.accompanyingCourse.job.id && state.accompanyingCourse.user.id !== u.id
} else {
if (null === state.accompanyingCourse.user) {
if (u.user_job && state.accompanyingCourse.job) {
return u.user_job.id === state.accompanyingCourse.job.id
} else {
return true;
}
}
return state.accompanyingCourse.user.id !== u.id;
}
})
},
confirmAccompanyingCourse(state, response) { confirmAccompanyingCourse(state, response) {
//console.log('### mutation: confirmAccompanyingCourse: response', response); //console.log('### mutation: confirmAccompanyingCourse: response', response);
state.accompanyingCourse.step = response.step; state.accompanyingCourse.step = response.step;
@ -689,8 +696,14 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
}) })
}, },
updateReferrer({ commit }, payload) { updateReferrer({ commit }, payload) {
console.log('update referrer', payload);
console.log('payload !== null', payload !== null);
const url = `/api/1.0/person/accompanying-course/${id}.json`; const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", user: { id: payload.id, type: payload.type }}; let body = { type: "accompanying_period", user: null };
if (payload !== null) {
body = { type: "accompanying_period", user: { id: payload.id, type: payload.type } };
}
return makeFetch('PATCH', url, body) return makeFetch('PATCH', url, body)
.then((response) => { .then((response) => {
@ -704,12 +717,15 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
}, },
updateJob({ commit }, payload) { updateJob({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`; const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", job: { id: payload.id, type: payload.type }}; let body = { type: "accompanying_period", job: null };
if (payload !== null) {
body = { type: "accompanying_period", job: { id: payload.id, type: payload.type } };
}
return makeFetch('PATCH', url, body) return makeFetch('PATCH', url, body)
.then((response) => { .then((response) => {
commit('updateJob', response.job); commit('updateJob', response.job);
commit('setFilteredReferrersSuggested');
}) })
.catch((error) => { .catch((error) => {
commit('catchError', error); commit('catchError', error);
@ -734,7 +750,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
async fetchReferrersSuggested({ state, commit}) { async fetchReferrersSuggested({ state, commit}) {
let users = await getReferrersSuggested(state.accompanyingCourse); let users = await getReferrersSuggested(state.accompanyingCourse);
commit('setReferrersSuggested', users); commit('setReferrersSuggested', users);
commit('setFilteredReferrersSuggested');
if ( if (
null === state.accompanyingCourse.user null === state.accompanyingCourse.user
&& !state.accompanyingCourse.confidential && !state.accompanyingCourse.confidential

View File

@ -211,7 +211,7 @@
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="record_actions"> <ul class="record_actions">
<li><on-the-fly :type="thirdparty.type" :id="thirdparty.id" action="show"></on-the-fly></li> <li><on-the-fly :type="thirdparty.type" :id="thirdparty.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="thirdparty.type" :id="thirdparty.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li> <li><on-the-fly :type="thirdparty.type" :id="thirdparty.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" ref="onTheFly"></on-the-fly></li>
<li> <li>
<button :title="$t('remove_thirdparty')" class="btn btn-sm btn-remove" @click="removeThirdParty(thirdparty)" /> <button :title="$t('remove_thirdparty')" class="btn btn-sm btn-remove" @click="removeThirdParty(thirdparty)" />
</li> </li>
@ -286,6 +286,8 @@ import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/
import PickWorkflow from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue'; import PickWorkflow from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue'; import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js'; import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
const i18n = { const i18n = {
messages: { messages: {
@ -476,15 +478,27 @@ export default {
}, },
saveFormOnTheFly(payload) { saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data); console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'resource';
this.$store.dispatch('patchOnTheFly', payload) let body = { type: payload.type };
.catch(({name, violations}) => { body.name = payload.data.text;
if (name === 'ValidationException' || name === 'AccessException') { body.email = payload.data.email;
violations.forEach((violation) => this.$toast.open({message: violation})); body.telephone = payload.data.phonenumber;
} else { body.address = { id: payload.data.address.address_id };
this.$toast.open({message: 'An error occurred'})
makeFetch('PATCH', `/api/1.0/thirdparty/thirdparty/${payload.data.id}.json`, body)
.then(response => {
this.$store.dispatch('updateThirdParty', response)
this.$refs.onTheFly.closeModal();
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
} }
}); } else {
this.$toast.open({message: 'An error occurred'});
}
})
} }
} }
}; };

View File

@ -1,7 +1,9 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'; import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { store } from './store'; import { store } from './store';
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n' import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n';
import VueToast from 'vue-toast-notification';
import 'vue-toast-notification/dist/theme-sugar.css';
import App from './App.vue'; import App from './App.vue';
const i18n = _createI18n(personMessages); const i18n = _createI18n(personMessages);
@ -10,6 +12,12 @@ const app = createApp({
template: `<app></app>`, template: `<app></app>`,
}) })
.use(store) .use(store)
.use(VueToast, {
position: "bottom-right",
type: "error",
duration: 5000,
dismissible: true
})
.use(i18n) .use(i18n)
.component('app', App) .component('app', App)
.mount('#accompanying_course_work_edit'); .mount('#accompanying_course_work_edit');

View File

@ -266,6 +266,14 @@ const store = createStore({
state.thirdParties.push(unexistings[i]); state.thirdParties.push(unexistings[i]);
} }
}, },
updateThirdParty(state, thirdParty) {
for (let t of state.thirdParties) {
if (t.id === thirdParty.id){
state.thirdParties = state.thirdParties.filter(t => t.id !== thirdParty.id);
state.thirdParties.push(thirdParty);
}
}
},
removeThirdParty(state, thirdParty) { removeThirdParty(state, thirdParty) {
state.thirdParties = state.thirdParties state.thirdParties = state.thirdParties
.filter(t => t.id !== thirdParty.id); .filter(t => t.id !== thirdParty.id);
@ -278,6 +286,10 @@ const store = createStore({
}, },
}, },
actions: { actions: {
updateThirdParty({ commit }, payload) {
console.log(payload);
commit('updateThirdParty', payload);
},
getReachablesGoalsForAction({ getters, commit, dispatch }) { getReachablesGoalsForAction({ getters, commit, dispatch }) {
let let
socialActionId = getters.socialAction.id, socialActionId = getters.socialAction.id,

View File

@ -69,7 +69,7 @@
:buttonText="$t('onthefly.create.button', {q: query})" :buttonText="$t('onthefly.create.button', {q: query})"
action="create" action="create"
@saveFormOnTheFly="saveFormOnTheFly" @saveFormOnTheFly="saveFormOnTheFly"
:canCloseModal="canCloseOnTheFlyModal"> ref="onTheFly">
</on-the-fly> </on-the-fly>
</div> </div>
@ -121,7 +121,6 @@ export default {
selected: [], selected: [],
priorSuggestion: {} priorSuggestion: {}
}, },
canCloseOnTheFlyModal: false
} }
}, },
computed: { computed: {
@ -271,7 +270,7 @@ export default {
makeFetch('POST', '/api/1.0/person/person.json', data) makeFetch('POST', '/api/1.0/person/person.json', data)
.then(response => { .then(response => {
this.newPriorSuggestion(response); this.newPriorSuggestion(response);
this.canCloseOnTheFlyModal = true; this.$refs.onTheFly.closeModal();
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === 'ValidationException') {
@ -287,7 +286,7 @@ export default {
makeFetch('POST', '/api/1.0/thirdparty/thirdparty.json', data) makeFetch('POST', '/api/1.0/thirdparty/thirdparty.json', data)
.then(response => { .then(response => {
this.newPriorSuggestion(response); this.newPriorSuggestion(response);
this.canCloseOnTheFlyModal = true; this.$refs.onTheFly.closeModal();
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === 'ValidationException') {
@ -299,7 +298,6 @@ export default {
} }
}) })
} }
this.canCloseOnTheFlyModal = false;
} }
}, },
} }

View File

@ -43,10 +43,11 @@
<label for="firstname">{{ $t('person.firstname') }}</label> <label for="firstname">{{ $t('person.firstname') }}</label>
</div> </div>
<div v-for="(a) in config.altNames" :key="a.key" class="form-floating mb-3"> <div v-for="(a, i) in config.altNames" :key="a.key" class="form-floating mb-3">
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
:id="a.key" :id="a.key"
:value="personAltNamesLabels[i]"
@input="onAltNameInput" @input="onAltNameInput"
/> />
<label :for="a.key">{{ a.labels.fr }}</label> <label :for="a.key">{{ a.labels.fr }}</label>
@ -199,6 +200,9 @@ export default {
}, },
feminized() { feminized() {
return (this.person.gender === 'woman')? 'e' : ''; return (this.person.gender === 'woman')? 'e' : '';
},
personAltNamesLabels() {
return this.person.altNames.map(a => a ? a.label : '');
} }
}, },
mounted() { mounted() {

View File

@ -23,6 +23,8 @@
</div> </div>
{% endif %} {% endif %}
{{ chill_pagination(paginator) }}
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<li> <li>
<a href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_new', { 'id': accompanyingCourse.id }) }}" <a href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_new', { 'id': accompanyingCourse.id }) }}"

View File

@ -41,7 +41,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
<div class="wh-col"> <div class="wh-col" style="align-items: center;">
{% if chill_accompanying_periods.fields.user == 'visible' %} {% if chill_accompanying_periods.fields.user == 'visible' %}
{# the tags `data-referrer-text` is used by module `@ChillPerson/mod/AccompanyingPeriod/setReferrer.js` #} {# the tags `data-referrer-text` is used by module `@ChillPerson/mod/AccompanyingPeriod/setReferrer.js` #}
{% if period.user %} {% if period.user %}

View File

@ -115,6 +115,9 @@
<li> <li>
{{ form_widget(form.editPerson, { 'attr': { 'class': 'dropdown-item' }}) }} {{ form_widget(form.editPerson, { 'attr': { 'class': 'dropdown-item' }}) }}
</li> </li>
<li>
{{ form_widget(form.createHousehold, { 'attr': { 'class': 'dropdown-item' }}) }}
</li>
<li> <li>
{{ form_widget(form.createPeriod, { 'attr': { 'class': 'dropdown-item' }}) }} {{ form_widget(form.createPeriod, { 'attr': { 'class': 'dropdown-item' }}) }}
</li> </li>

View File

@ -32,14 +32,28 @@
</div> </div>
<div class="item-row separator"> <div class="item-row separator">
<div class="wrap-list"> <div class="wrap-list">
{% if p.household is same as p.person.getCurrentHousehold %}
<div class="wl-row"> <div class="wl-row">
<div class="wl-col title"> <div class="wl-col title">
<h3>En tant que</h3> <h3>{{ 'Address'|trans }}</h3>
</div> </div>
<div class="wl-col list"> <div class="wl-col list">
<p class="item">{{ p.position.label|localize_translatable_string }} <p class="item">{{ p.household.getCurrentAddress|chill_entity_render_box }}</p>
</div>
</div>
{% endif %}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'household.As member'|trans }}</h3>
</div>
<div class="wl-col list">
<p class="item">
{{ p.position.label|localize_translatable_string }}
{% if p.holder %} {% if p.holder %}
<span class="holder">{{ 'household.holder'|trans }}</span> <span class="fa-stack fa-holder" title="{{ 'houshold.holder'|trans|e('html_attr') }}">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
{% endif %} {% endif %}
</p> </p>
</div> </div>

View File

@ -29,10 +29,10 @@
<div class="item-col" style="width: 33%;"> <div class="item-col" style="width: 33%;">
<ul class="list-unstyled h3"> <ul class="list-unstyled h3">
{% if a.endDate is not null %}
<li><span class="item-key">{{'Since'|trans}} : </span>{{ a.startDate|format_date('long') }}</li> <li><span class="item-key">{{'Since'|trans}} : </span>{{ a.startDate|format_date('long') }}</li>
{% endif %} {% if a.endDate is not null %}
<li><span class="item-key">{{'Until'|trans}} : </span><b>{{ a.endDate|format_date('long') }}</b></li> <li><span class="item-key">{{'Until'|trans}} : </span><b>{{ a.endDate|format_date('long') }}</b></li>
{% endif %}
</ul> </ul>
</div> </div>
<div class="item-col flex-column justify-content-start"> <div class="item-col flex-column justify-content-start">

View File

@ -11,17 +11,23 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Security\Authorization; namespace Chill\PersonBundle\Security\Authorization;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use UnexpectedValueException; use UnexpectedValueException;
use function get_class;
use function in_array; use function in_array;
class AccompanyingPeriodWorkVoter extends Voter class AccompanyingPeriodWorkVoter extends Voter
{ {
public const CREATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_CREATE';
public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE'; public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE';
public const UPDATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE';
private Security $security; private Security $security;
public function __construct(Security $security) public function __construct(Security $security)
@ -31,8 +37,14 @@ class AccompanyingPeriodWorkVoter extends Voter
protected function supports($attribute, $subject): bool protected function supports($attribute, $subject): bool
{ {
return $subject instanceof AccompanyingPeriodWork return
&& in_array($attribute, $this->getRoles(), true); (
$subject instanceof AccompanyingPeriodWork
&& in_array($attribute, $this->getRoles(), true)
) || (
$subject instanceof AccompanyingPeriod
&& in_array($attribute, [self::SEE, self::CREATE], true)
);
} }
/** /**
@ -41,13 +53,28 @@ class AccompanyingPeriodWorkVoter extends Voter
*/ */
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{ {
if ($subject instanceof AccompanyingPeriodWork) {
switch ($attribute) { switch ($attribute) {
case self::SEE: case self::SEE:
return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject->getAccompanyingPeriod()); return $this->security->isGranted(AccompanyingPeriodVoter::SEE_DETAILS, $subject->getAccompanyingPeriod());
default: default:
throw new UnexpectedValueException("attribute {$attribute} is not supported"); throw new UnexpectedValueException("attribute {$attribute} is not supported");
} }
} elseif ($subject instanceof AccompanyingPeriod) {
switch ($attribute) {
case self::SEE:
return $this->security->isGranted(AccompanyingPeriodVoter::SEE_DETAILS, $subject);
default:
throw new UnexpectedValueException(sprintf(
"attribute {$attribute} is not supported on instance %s",
AccompanyingPeriod::class
));
}
}
throw new UnexpectedValueException(sprintf("attribute {$attribute} on instance %s is not supported", get_class($subject)));
} }
private function getRoles(): array private function getRoles(): array

View File

@ -91,6 +91,7 @@ class PersonJsonNormalizer implements
'deathdate', 'deathdate',
'center', 'center',
'altNames', 'altNames',
'email',
]; ];
$fields = array_filter( $fields = array_filter(
@ -161,6 +162,11 @@ class PersonJsonNormalizer implements
} }
} }
break;
case 'email':
$person->setEmail($data[$item]);
break; break;
} }
} }
@ -189,6 +195,7 @@ class PersonJsonNormalizer implements
'centers' => $this->normalizer->normalize($this->centerResolverManager->resolveCenters($person), $format, $context), 'centers' => $this->normalizer->normalize($this->centerResolverManager->resolveCenters($person), $format, $context),
'phonenumber' => $person->getPhonenumber(), 'phonenumber' => $person->getPhonenumber(),
'mobilenumber' => $person->getMobilenumber(), 'mobilenumber' => $person->getMobilenumber(),
'email' => $person->getEmail(),
'altNames' => $this->normalizeAltNames($person->getAltNames()), 'altNames' => $this->normalizeAltNames($person->getAltNames()),
'gender' => $person->getGender(), 'gender' => $person->getGender(),
'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress(), $format, $context), 'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress(), $format, $context),

View File

@ -21,6 +21,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use DateTime; use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use function array_map; use function array_map;
@ -58,10 +59,16 @@ final class HouseholdApiControllerTest extends WebTestCase
$centerA = $em->getRepository(Center::class)->findOneBy(['name' => 'Center A']); $centerA = $em->getRepository(Center::class)->findOneBy(['name' => 'Center A']);
$nbReference = $em->createQueryBuilder()->select('count(ar)')->from(AddressReference::class, 'ar') $nbReference = $em->createQueryBuilder()->select('count(ar)')->from(AddressReference::class, 'ar')
->getQuery()->getSingleScalarResult(); ->getQuery()->getSingleScalarResult();
if (0 === $nbReference) {
throw new RuntimeException('any reference found. Add a reference in database to perform this test');
}
$reference = $em->createQueryBuilder()->select('ar')->from(AddressReference::class, 'ar') $reference = $em->createQueryBuilder()->select('ar')->from(AddressReference::class, 'ar')
->setFirstResult(random_int(0, $nbReference)) ->setFirstResult(random_int(0, $nbReference))
->setMaxResults(1) ->setMaxResults(1)
->getQuery()->getSingleResult(); ->getQuery()->getSingleResult();
$p = new Person(); $p = new Person();
$p->setFirstname('test')->setLastName('test lastname') $p->setFirstname('test')->setLastName('test lastname')
->setGender(Person::BOTH_GENDER) ->setGender(Person::BOTH_GENDER)
@ -79,6 +86,7 @@ final class HouseholdApiControllerTest extends WebTestCase
[HouseholdMember::class, $m->getId()], [HouseholdMember::class, $m->getId()],
[User::class, $p->getId()], [User::class, $p->getId()],
[Household::class, $h->getId()], [Household::class, $h->getId()],
[Person::class, $p->getId()],
]; ];
yield [$reference->getId(), $h->getId()]; yield [$reference->getId(), $h->getId()];

View File

@ -129,20 +129,27 @@ final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
$this->assertNull($period->getPinnedComment()); $this->assertNull($period->getPinnedComment());
$period->setPinnedComment($comment); $period->setPinnedComment($comment);
$this->assertSame($period->getPinnedComment(), $comment); $this->assertSame($period->getPinnedComment(), $comment);
$this->assertSame($period, $comment->getAccompanyingPeriod()); $this->assertNull($comment->getAccompanyingPeriod());
$this->assertEquals(0, count($period->getComments()), 'The initial comment should not appears in the list of comments'); $this->assertEquals(0, count($period->getComments()));
$period->setPinnedComment($replacingComment); $period->setPinnedComment($replacingComment);
$this->assertSame($period->getPinnedComment(), $replacingComment); $this->assertSame($period->getPinnedComment(), $replacingComment);
$this->assertSame($period, $replacingComment->getAccompanyingPeriod()); $this->assertNull($replacingComment->getAccompanyingPeriod());
$this->assertEquals(0, count($period->getComments()), 'The initial comment should not appears in the list of comments'); $this->assertSame($period, $comment->getAccompanyingPeriod());
$this->assertNull($comment->getAccompanyingPeriod()); $this->assertEquals(1, count($period->getComments()));
$this->assertContains($comment, $period->getComments());
$period->setPinnedComment(null); $period->setPinnedComment(null);
$this->assertNull($period->getPinnedComment()); $this->assertNull($period->getPinnedComment());
$this->assertNull($replacingComment->getAccompanyingPeriod()); $this->assertSame($period, $comment->getAccompanyingPeriod());
$this->assertEquals(0, count($period->getComments()), 'The initial comment should not appears in the list of comments'); $this->assertSame($period, $replacingComment->getAccompanyingPeriod());
$this->assertEquals(2, count($period->getComments()));
$this->assertContains($comment, $period->getComments());
$this->assertContains($replacingComment, $period->getComments());
} }
public function testRequestor() public function testRequestor()

View File

@ -36,7 +36,7 @@ class HouseholdMembershipSequentialValidator extends ConstraintValidator
public function validate($person, Constraint $constraint) public function validate($person, Constraint $constraint)
{ {
if (!$person instanceof Person) { if (!$person instanceof Person) {
throw new UnexpectedTypeException($constraint, Person::class); throw new UnexpectedTypeException($person, Person::class);
} }
$participations = $person->getHouseholdParticipationsShareHousehold(); $participations = $person->getHouseholdParticipationsShareHousehold();

View File

@ -98,6 +98,7 @@ household:
from: Depuis from: Depuis
to: Jusqu'au to: Jusqu'au
person history: Ménages person history: Ménages
As member: En tant que
household_composition: household_composition:
Since: >- Since: >-

View File

@ -124,7 +124,7 @@ address_country_code: Code pays
'Alreay existing person': 'Dossiers déjà encodés' 'Alreay existing person': 'Dossiers déjà encodés'
'Add the person': 'Ajouter la personne' 'Add the person': 'Ajouter la personne'
'Add the person and create an accompanying period': "Créer la personne & créer une période d'accompagnement" 'Add the person and create an accompanying period': "Créer la personne & créer une période d'accompagnement"
'Add the person and create an household': "Créer la personne & créer un ménage" 'Add the person and create a household': "Créer la personne & créer un ménage"
Show person: Voir le dossier de la personne Show person: Voir le dossier de la personne
'Confirm the creation': 'Confirmer la création' 'Confirm the creation': 'Confirmer la création'
'You will create this person': 'Vous allez créer le dossier suivant' 'You will create this person': 'Vous allez créer le dossier suivant'
@ -211,7 +211,7 @@ No requestor: Pas de demandeur
No resources: "Pas d'interlocuteurs privilégiés" No resources: "Pas d'interlocuteurs privilégiés"
Persons associated: Usagers concernés Persons associated: Usagers concernés
Referrer: Référent Referrer: Référent
Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur appartenance à un ménage dès que possible. Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur ménage dès que possible.
Add to household now: Ajouter à un ménage Add to household now: Ajouter à un ménage
Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours
course.draft: Brouillon course.draft: Brouillon
@ -557,7 +557,7 @@ household_composition:
numberOfChildren: Nombre d'enfants mineurs au sein du ménage numberOfChildren: Nombre d'enfants mineurs au sein du ménage
Household composition: Composition du ménage Household composition: Composition du ménage
Composition added: Information sur la composition familiale ajoutée Composition added: Information sur la composition familiale ajoutée
Currently no composition: Aucune composition famiale renseignée. Currently no composition: Aucune composition familiale renseignée.
Add a composition: Ajouter une composition familiale Add a composition: Ajouter une composition familiale
Update composition: Modifier la composition familiale Update composition: Modifier la composition familiale

View File

@ -17,8 +17,8 @@ use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* SingleTask. * SingleTask.