Merge branch 'features/household-editor' into features/household-validation

This commit is contained in:
Julien Fastré 2021-06-11 17:08:38 +02:00
commit af740fd87d
36 changed files with 1661 additions and 44 deletions

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg34"
version="1.1"
viewBox="0 0 32 32">
<metadata
id="metadata40">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs38" />
<rect
id="rect2"
x="0"
y="4"
width="4"
height="4" />
<rect
id="rect4"
x="0"
y="12"
width="4"
height="4" />
<rect
id="rect6"
x="0"
y="20"
width="4"
height="4" />
<rect
id="rect8"
x="0"
y="28"
width="4"
height="4" />
<rect
id="rect10"
x="8"
y="4"
width="4"
height="4" />
<rect
id="rect12"
x="8"
y="12"
width="4"
height="4" />
<rect
id="rect14"
x="8"
y="20"
width="4"
height="4" />
<rect
id="rect16"
x="8"
y="28"
width="4"
height="4" />
<rect
id="rect18"
x="16"
y="4"
width="4"
height="4" />
<rect
id="rect20"
x="16"
y="12"
width="4"
height="4" />
<rect
id="rect22"
x="16"
y="20"
width="4"
height="4" />
<rect
id="rect24"
x="16"
y="28"
width="4"
height="4" />
<rect
id="rect26"
x="24"
y="4"
width="4"
height="4" />
<rect
id="rect28"
x="24"
y="12"
width="4"
height="4" />
<rect
id="rect30"
x="24"
y="20"
width="4"
height="4" />
<rect
id="rect32"
x="24"
y="28"
width="4"
height="4" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,88 @@
/**
* Some utils for manipulating dates
*
* **WARNING** experimental
*/
/**
* Return the date to local ISO date, like YYYY-mm-dd
*
* The date is valid for the same timezone as the date's locale
*
* Do not take time into account
*
* **Experimental**
*/
const dateToISO = (date) => {
return [
this.$store.state.startDate.getFullYear(),
(this.$store.state.startDate.getMonth() + 1).toString().padStart(2, '0'),
this.$store.state.startDate.getDate().toString().padStart(2, '0')
].join('-');
};
/**
* Return a date object from iso string formatted as YYYY-mm-dd
*
* **Experimental**
*/
const ISOToDate = (str) => {
let
[year, month, day] = str.split('-');
return new Date(year, month-1, day);
}
/**
* Return a date object from iso string formatted as YYYY-mm-dd:HH:MM:ss+01:00
*
* **Experimental**
*/
const ISOToDatetime = (str) => {
console.log(str);
let
[cal, times] = str.split('T'),
[year, month, date] = cal.split('-'),
[time, timezone] = cal.split(times.charAt(9)),
[hours, minutes, seconds] = cal.split(':')
;
return new Date(year, month-1, date, hours, minutes, seconds);
}
/**
* Convert a date to ISO8601, valid for usage in api
*
*/
const datetimeToISO = (date) => {
let cal, time, offset;
cal = [
date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, '0'),
date.getDate().toString().padStart(2, '0')
].join('-');
time = [
date.getHours().toString().padStart(2, '0'),
date.getMinutes().toString().padStart(2, '0'),
date.getSeconds().toString().padStart(2, '0')
].join(':');
offset = [
date.getTimezoneOffset() <= 0 ? '+' : '-',
Math.abs(Math.floor(date.getTimezoneOffset() / 60)).toString().padStart(2, '0'),
':',
Math.abs(date.getTimezoneOffset() % 60).toString().padStart(2, '0'),
].join('');
let x = cal + 'T' + time + offset;
return x;
};
export {
dateToISO,
ISOToDate,
ISOToDatetime,
datetimeToISO
};

View File

@ -17,7 +17,7 @@
// @import "bootstrap/scss/grid";
// @import "bootstrap/scss/tables";
// @import "bootstrap/scss/forms";
// @import "bootstrap/scss/buttons";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
// @import "bootstrap/scss/dropdown";
// @import "bootstrap/scss/button-group";
@ -30,7 +30,7 @@
// @import "bootstrap/scss/pagination";
@import "bootstrap/scss/badge";
// @import "bootstrap/scss/jumbotron";
// @import "bootstrap/scss/alert";
@import "bootstrap/scss/alert";
// @import "bootstrap/scss/progress";
// @import "bootstrap/scss/media";
// @import "bootstrap/scss/list-group";

View File

@ -121,7 +121,7 @@ div.flex-bloc {
display: flex;
flex-direction: column;
div.item-row {
& > div.item-row {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex;
flex-direction: column;

View File

@ -62,7 +62,7 @@ const messages = {
person: "un nouvel usager",
thirdparty: "un nouveau tiers"
},
}
},
}
};

View File

@ -2,6 +2,10 @@
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Form\HouseholdMemberType;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
@ -31,7 +35,7 @@ class HouseholdMemberController extends ApiController
/**
* @Route(
* "/api/1.0/person/household/members/move.{_format}",
* name="chill_person_household_members_move"
* name="chill_api_person_household_members_move"
* )
*/
public function move(Request $request, $_format): Response
@ -50,13 +54,94 @@ class HouseholdMemberController extends ApiController
//
$em = $this->getDoctrine()->getManager();
// if new household, persist it
if (
$editor->hasHousehold()
&&
FALSE === $em->contains($editor->getHousehold())
) {
$em->persist($editor->getHousehold());
}
foreach ($editor->getPersistable() as $el) {
$em->persist($el);
}
$em->flush();
return $this->json($editor->getHousehold(), Response::HTTP_OK, [], [
"groups" => ["read"],
return $this->json($editor->getHousehold(), Response::HTTP_OK, [], ["groups" => ["read"]]);
}
/**
* Route for showing an editor to leave a household.
*
* Possibles arguments are:
*
* * persons[]: an id of the person to add to the form
* * household: the id of the destination household
* * allow_leave_without_household: if present, the editor will allow
* to leave household without joining another
*
* @Route(
* "/{_locale}/person/household/members/editor",
* name="chill_person_household_members_editor"
* )
*/
public function editor(Request $request)
{
$em = $this->getDoctrine()->getManager();
if ($request->query->has('persons')) {
$ids = $request->query->get('persons', []);
if (0 === count($ids)) {
throw new BadRequestExceptions("parameters persons in query ".
"is not an array or empty");
}
$persons = $em->getRepository(Person::class)
->findById($ids)
;
foreach ($persons as $person) {
$this->denyAccessUnlessGranted(PersonVoter::SEE, $person,
"You are not allowed to see person with id {$person->getId()}"
);
}
}
if ($householdId = $request->query->get('household', false)) {
$household = $em->getRepository(Household::class)
->find($householdId)
;
$allowHouseholdCreate = false;
$allowHouseholdSearch = false;
$allowLeaveWithoutHousehold = false;
if (NULL === $household) {
throw $this->createNotFoundException('household not found');
}
// TODO ACL on household
}
$positions = $this->getDoctrine()->getManager()
->getRepository(Position::class)
->findAll()
;
$data = [
'persons' => $persons ?? false ?
$this->getSerializer()->normalize($persons, 'json', [ 'groups' => [ 'read' ]]) : [],
'household' => $household ?? false ?
$this->getSerializer()->normalize($household, 'json', [ 'groups' => [ 'read' ]]) : null,
'positions' =>
$this->getSerializer()->normalize($positions, 'json', [ 'groups' => [ 'read' ]]),
'allowHouseholdCreate' => $allowHouseholdCreate ?? true,
'allowHouseholdSearch' => $allowHouseholdSearch ?? true,
'allowLeaveWithoutHousehold' => $allowLeaveWithoutHousehold ?? $request->query->has('allow_leave_without_household'),
];
return $this->render('@ChillPerson/Household/members_editor.html.twig', [
'data' => $data
]);
}

View File

@ -514,6 +514,25 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
],
]
],
[
'class' => \Chill\PersonBundle\Entity\Household\Household::class,
'name' => 'household',
'base_path' => '/api/1.0/person/household',
// TODO: acl
'base_role' => 'ROLE_USER',
'actions' => [
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
]
],
]
]);
}

View File

@ -116,8 +116,7 @@ class Household
{
$criteria = new Criteria();
$expr = Criteria::expr();
$date = $now === null ? (new \DateTimeImmutable('now')) : $now;
$date = $now === null ? (new \DateTimeImmutable('today')) : $now;
$criteria
->where($expr->orX(
@ -149,7 +148,7 @@ class Household
{
$criteria = new Criteria();
$expr = Criteria::expr();
$date = $now === null ? (new \DateTimeImmutable('now')) : $now;
$date = $now === null ? (new \DateTimeImmutable('today')) : $now;
$criteria
->where(

View File

@ -50,9 +50,9 @@ class HouseholdMember
private ?string $comment = NULL;
/**
* @ORM\Column(type="boolean")
* @ORM\Column(type="boolean", name="sharedhousehold")
*/
private bool $sharedHousehold = false;
private bool $shareHousehold = false;
/**
* @ORM\Column(type="boolean", options={"default": false})
@ -98,7 +98,7 @@ class HouseholdMember
}
$this->position = $position;
$this->sharedHousehold = $position->getShareHousehold();
$this->shareHousehold = $position->getShareHousehold();
return $this;
}
@ -144,7 +144,7 @@ class HouseholdMember
*/
public function getShareHousehold(): ?bool
{
return $this->sharedHousehold;
return $this->shareHousehold;
}

View File

@ -7,7 +7,7 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Entity(repositoryClass=PositionRepository::class)
* @ORM\Entity
* @ORM\Table(name="chill_person_household_position")
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "household_position"=Position::class
@ -25,21 +25,25 @@ class Position
/**
* @ORM\Column(type="json")
* @Serializer\Groups({ "read" })
*/
private array $label = [];
/**
* @ORM\Column(type="boolean")
* @Serializer\Groups({ "read" })
*/
private bool $shareHouseHold = true;
/**
* @ORM\Column(type="boolean")
* @Serializer\Groups({ "read" })
*/
private bool $allowHolder = false;
/**
* @ORM\Column(type="float")
* @Serializer\Groups({ "read" })
*/
private float $ordering = 0.00;

View File

@ -25,6 +25,7 @@ namespace Chill\PersonBundle\Entity;
use ArrayIterator;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Country;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\MaritalStatus;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\MainBundle\Entity\HasCenterInterface;
@ -281,6 +282,11 @@ class Person implements HasCenterInterface
*/
private Collection $householdParticipations;
/**
* Cache the computation of household
*/
private array $currentHouseholdAt = [];
/**
* Person constructor.
*
@ -1202,4 +1208,43 @@ class Person implements HasCenterInterface
{
return $this->householdParticipations;
}
public function getCurrentHousehold(?\DateTimeImmutable $at = null): ?Household
{
$criteria = new Criteria();
$expr = Criteria::expr();
$date = NULL === $at ? new \DateTimeImmutable('now') : $at;
$datef = $date->format('Y-m-d');
if (
NULL !== ($this->currentHouseholdAt[$datef] ?? NULL)) {
return $this->currentHouseholdAt[$datef];
}
$criteria
->where(
$expr->andX(
$expr->lte('startDate', $date),
$expr->orX(
$expr->isNull('endDate'),
$expr->gte('endDate', $date)
),
$expr->eq('shareHousehold', true)
)
);
$participations = $this->getHouseholdParticipations()
->matching($criteria)
;
return $participations->count() > 0 ?
$this->currentHouseholdAt[$datef] = $participations->first()
->getHousehold()
: null;
}
public function isSharingHousehold(?\DateTimeImmutable $at = null): bool
{
return NULL !== $this->getCurrentHousehold($at);
}
}

View File

@ -3,6 +3,7 @@
namespace Chill\PersonBundle\Household;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Doctrine\Common\Collections\Criteria;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Household\Household;
@ -13,12 +14,12 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
class MembersEditor
{
private ValidatorInterface $validator;
private Household $household;
private ?Household $household = null;
private array $persistables = [];
private array $membershipsAffected = [];
public function __construct(ValidatorInterface $validator, Household $household)
public function __construct(ValidatorInterface $validator, ?Household $household)
{
$this->validation = $validator;
$this->household = $household;
@ -35,9 +36,9 @@ class MembersEditor
->setPerson($person)
->setPosition($position)
->setHolder($holder)
->setHousehold($this->household)
->setComment($comment)
;
$this->household->addMember($membership);
if ($position->getShareHousehold()) {
foreach ($person->getHouseholdParticipations() as $participation) {
@ -62,6 +63,33 @@ class MembersEditor
return $this;
}
public function leaveMovement(
\DateTimeImmutable $date,
Person $person
): self {
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where(
$expr->andX(
$expr->lt('startDate', $date),
$expr->isNull('endDate', $date)
)
);
$participations = $person->getHouseholdParticipations()
->matching($criteria)
;
foreach ($participations as $participation) {
$participation->setEndDate($date);
$this->membershipsAffected[] = $participation;
}
return $this;
}
public function validate(): ConstraintViolationListInterface
{
@ -72,8 +100,13 @@ class MembersEditor
return $this->persistables;
}
public function getHousehold(): Household
public function getHousehold(): ?Household
{
return $this->household;
}
public function hasHousehold(): bool
{
return $this->household !== null;
}
}

View File

@ -14,7 +14,7 @@ class MembersEditorFactory
$this->validator = $validator;
}
public function createEditor(Household $household): MembersEditor
public function createEditor(?Household $household = null): MembersEditor
{
return new MembersEditor($this->validator, $household);
}

View File

@ -3,8 +3,10 @@
namespace Chill\PersonBundle\Repository\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
//use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
//use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
/**
* @method Position|null find($id, $lockMode = null, $lockVersion = null)
@ -12,11 +14,20 @@ use Doctrine\Persistence\ManagerRegistry;
* @method Position[] findAll()
* @method Position[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PositionRepository extends ServiceEntityRepository
final class PositionRepository
{
public function __construct(ManagerRegistry $registry)
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct($registry, Position::class);
$this->repository = $entityManager->getRepository(Position::class);
}
/**
* @return Position[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
}

View File

@ -59,5 +59,3 @@ div.list-household-members--summary {
}
}
}

View File

@ -0,0 +1,33 @@
<template>
<household></household>
<concerned></concerned>
<dates></dates>
<confirmation></confirmation>
</template>
<script>
import { mapState } from 'vuex';
import Concerned from './components/Concerned.vue';
import Household from './components/Household.vue';
import Dates from './components/Dates.vue';
import Confirmation from './components/Confirmation.vue';
export default {
name: 'App',
components: {
Concerned,
Household,
Dates,
Confirmation,
},
computed: {
// for debugging purpose
// (not working)
//...mapState({
// 'concerned', 'household', 'positions'
// })
}
}
</script>

View File

@ -0,0 +1,47 @@
/*
*/
const householdMove = (payload) => {
const url = `/api/1.0/person/household/members/move.json`;
console.log(payload);
console.log(JSON.stringify(payload));
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
.then(response => {
if (response.ok) {
return response.json();
}
throw Error('Error with testing move');
});
};
const householdMoveTest = (payload) => {
const url = `/api/1.0/person/household/members/move/test.json`;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
.then(response => {
if (response.status === 422) {
return response.json();
}
if (response.ok) {
// return an empty array if ok
return new Promise((resolve, reject) => resolve({ violations: [] }) );
}
throw Error('Error with testing move');
});
};
export {
householdMove,
householdMoveTest
};

View File

@ -0,0 +1,215 @@
<template>
<h2>{{ $t('household_members_editor.concerned.title') }}</h2>
<h3 v-if="needsPositionning">
{{ $t('household_members_editor.concerned.persons_to_positionnate') }}
</h3>
<h3 v-else>
{{ $t('household_members_editor.concerned.persons_leaving') }}
</h3>
<div v-if="noPerson">
<div class="alert alert-info">
{{ $t('household_members_editor.add_at_least_onePerson') }}
</div>
</div>
<div v-else-if="allPersonsPositionnated">
<span class="chill-no-data-statement">{{ $t('household_members_editor.all_positionnated') }}</span>
</div>
<div v-else>
<div class="flex-table list-household-members">
<div v-for="conc in concUnpositionned"
class="item-bloc"
v-bind:key="conc.person.id"
draggable="true"
@dragstart="onStartDragConcern($event, conc.person.id)"
>
<div class="item-row person">
<div class="item-col box-person">
<div>
<img src="~ChillMainAssets/img/draggable.svg" class="drag-icon" />
<person :person="conc.person"></person>
</div>
<div>
{{ $t('person.born', {'gender': conc.person.gender} ) }}
{{ $d(conc.person.birthdate.datetime, 'short') }}
</div>
</div>
<div class="item-col box-where">
<ul class="list-content fa-ul">
<li>
<i class="fa fa-li fa-map-marker"></i>
<span class="chill-no-data-statement">Sans adresse</span>
</li>
</ul>
</div>
</div>
<div v-if="needsPositionning" class="item-row move_to">
<div class="item-col">
<p class="move_hint">{{ $t('household_members_editor.concerned.move_to') }}:</p>
<template
v-for="position in positions"
>
<button
class="btn btn-outline-primary"
@click="moveToPosition(conc.person.id, position.id)"
>
{{ position.label.fr }}
</button>&nbsp;
</template>
<button v-if="conc.allowRemove" @click="removeConcerned(conc)" class="btn bt-primary">
{{ $t('household_members_editor.remove_concerned') }}
</button>
</div>
</div>
</div>
</div>
</div>
<div>
<add-persons
buttonTitle="household_members_editor.concerned.add_persons"
modalTitle="household_members_editor.concerned.search"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons"> <!-- to cast child method -->
</add-persons>
</div>
<div v-if="needsPositionning" class="positions">
<div
v-for="position in positions"
>
<h3>{{ position.label.fr }}</h3>
<div class="flex-table list-household-members">
<member-details
v-for="conc in concByPosition(position.id)"
v-bind:key="conc.person.id"
v-bind:conc="conc"
>
</member-details>
<div
class="droppable_zone"
@drop="onDropConcern($event, position.id)"
@dragover.prevent
@dragenter.prevent
>
{{ $t('household_members_editor.drop_persons_here', {'position': position.label.fr }) }}
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
div.person {
cursor: move;
* {
cursor: move
}
}
.drag-icon {
height: 1.1em;
margin-right: 0.5em;
}
.droppable_zone {
background-color: var(--chill-llight-gray);
color: white;
font-size: large;
text-align: center;
display: table-cell;
vertical-align: middle;
padding: 1em;
background: linear-gradient(to top, var(--chill-light-gray), 30%, var(--chill-llight-gray));
}
.move_to {
.move_hint {
text-align: center;
display: inline-block;
padding: 0.400rem 0.5rem;
}
}
</style>
<script>
import { mapGetters } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import Person from 'ChillPersonAssets/vuejs/_components/Person/Person.vue';
import MemberDetails from './MemberDetails.vue';
import { ISOToDatetime } from 'ChillMainAssets/js/date.js';
export default {
name: 'Concerned',
components: {
AddPersons,
MemberDetails,
Person,
},
computed: {
...mapGetters([
'concUnpositionned',
'positions',
'concByPosition',
'needsPositionning'
]),
noPerson () {
return this.$store.getters.persons.length === 0;
},
allPersonsPositionnated () {
return this.$store.getters.persons.length > 0
&& this.$store.getters.concUnpositionned.length === 0;
},
},
data() {
return {
addPersons: {
key: 'household_members_editor_concerned',
options: {
type: ['person'],
priority: null,
uniq: false,
}
}
}
},
methods: {
addNewPersons({ selected, modal }) {
selected.forEach(function(item) {
this.$store.dispatch('addConcerned', item.result);
}, this);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
},
onStartDragConcern(evt, person_id) {
evt.dataTransfer.dropEffect = 'move'
evt.dataTransfer.effectAllowed = 'move'
evt.dataTransfer.setData('application/x.person', person_id)
},
onDropConcern(evt, position_id) {
const person_id = Number(evt.dataTransfer.getData('application/x.person'));
this.moveToPosition(person_id, position_id);
},
moveToPosition(person_id, position_id) {
this.$store.dispatch('markPosition', { person_id, position_id });
},
removeConcerned(conc) {
this.$store.dispatch('removeConcerned', conc);
},
}
}
</script>

View File

@ -0,0 +1,48 @@
<template>
<div v-if="hasWarnings" class="alert alert-warning">
{{ $t('household_members_editor.confirmation.there_are_warnings') }}
</div>
<p v-if="hasWarnings">
{{ $t('household_members_editor.confirmation.check_those_items') }}
</p>
<ul>
<li v-for="(msg, index) in warnings">
{{ $t(msg.m, msg.a) }}
</li>
</ul>
<ul class="record_actions sticky-form-buttons">
<li>
<button class="sc-button bt-save" :disabled="hasWarnings" @click="confirm">
{{ $t('household_members_editor.confirmation.save') }}
</button>
</li>
</ul>
</template>
<style scoped lang="scss">
</style>
<script>
import { mapState } from 'vuex';
export default {
name: 'Confirmation',
computed: {
...mapState({
warnings: (state) => state.warnings,
hasNoWarnings: (state) => state.warnings.length === 0,
hasWarnings: (state) => state.warnings.length > 0,
}),
},
methods: {
confirm() {
this.$store.dispatch('confirm');
}
}
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<h2>{{ $t('household_members_editor.dates_title') }}</h2>
<p>
<label for="start_date">
{{ $t('household_members_editor.dates.start_date') }}
</label>
<input type="date" v-model="startDate" />
</p>
</template>
<script>
export default {
name: 'Dates',
computed: {
startDate: {
get() {
return [
this.$store.state.startDate.getFullYear(),
(this.$store.state.startDate.getMonth() + 1).toString().padStart(2, '0'),
this.$store.state.startDate.getDate().toString().padStart(2, '0')
].join('-');
},
set(value) {
let
[year, month, day] = value.split('-'),
dValue = new Date(year, month-1, day);
this.$store.dispatch('setStartDate', dValue);
}
}
}
}
</script>

View File

@ -0,0 +1,80 @@
<template>
<h2>{{ $t('household_members_editor.household_part') }}</h2>
<div v-if="hasHousehold">
<span v-if="isHouseholdNew">
{{ $t('household_members_editor.household.new_household') }}
</span>
<div v-else>
Ménage existant
</div>
</div>
<div v-else-if="isForceLeaveWithoutHousehold">
{{ $t('household_members_editor.household.will_leave_any_household') }}
</div>
<div v-else>
<div class="alert alert-info">{{ $t('household_members_editor.household.no_household_choose_one') }}</div>
</div>
<ul v-if="allowChangeHousehold" class="record_actions">
<li v-if="allowHouseholdCreate">
<button class="sc-button bt-create" @click="createHousehold">
{{ $t('household_members_editor.household.create_household') }}
</button>
</li>
<li v-if="allowHouseholdSearch">
<button class="sc-button">
<i class="fa fa-search"></i>{{ $t('household_members_editor.household.search_household') }}
</button>
</li>
<li v-if="allowLeaveWithoutHousehold" >
<button @click="forceLeaveWithoutHousehold" class="sc-button bt-orange">
<i class="fa fa-sign-out"></i>{{ $t('household_members_editor.household.leave_without_household') }}
</button>
</li>
</ul>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'Household',
computed: {
...mapGetters([
'hasHousehold',
'isHouseholdNew',
]),
household() {
return this.$store.household;
},
allowHouseholdCreate() {
return this.$store.state.allowHouseholdCreate;
},
allowHouseholdSearch() {
return this.$store.state.allowHouseholdSearch;
},
allowLeaveWithoutHousehold() {
return this.$store.state.allowLeaveWithoutHousehold;
},
allowChangeHousehold() {
return this.allowHouseholdCreate || this.allowHouseholdSearch ||
this.allowLeaveWithoutHousehold;
},
isForceLeaveWithoutHousehold() {
return this.$store.state.forceLeaveWithoutHousehold;
}
},
methods: {
createHousehold() {
this.$store.dispatch('createHousehold');
},
forceLeaveWithoutHousehold() {
this.$store.dispatch('forceLeaveWithoutHousehold');
}
},
};
</script>

View File

@ -0,0 +1,126 @@
<template>
<div class="item-bloc">
<div class="item-row person">
<div class="item-col box-person">
<div>
<person :person="conc.person"></person>
<span v-if="isHolder" class="badge badge-primary holder">
{{ $t('household_members_editor.holder') }}
</span>
</div>
<div>{{ $t('person.born', {'gender': conc.person.gender} ) }}</div>
</div>
<div class="item-col box-where">
<ul class="list-content fa-ul">
<li>
<i class="fa fa-li fa-map-marker"></i>
<span class="chill-no-data-statement">Sans adresse</span>
</li>
</ul>
</div>
</div>
<div class="item-row comment">
<ckeditor :editor="editor" v-model="comment" tag-name="textarea"></ckeditor>
</div>
<div class="item-row participation-details">
<div v-if="conc.position.allowHolder" class="action">
<button class="btn" :class="{ 'btn-primary': isHolder, 'btn-secondary': !isHolder}" @click="toggleHolder">
{{ $t(isHolder ? 'household_members_editor.is_holder' : 'household_members_editor.is_not_holder') }}
</button>
</div>
<div>
<button @click="removePosition" class="btn btn-outline-primary">
{{ $t('household_members_editor.remove_position', {position: conc.position.label.fr}) }}
</button>
</div>
<div>
<button v-if="conc.allowRemove" @click="removeConcerned" class="btn btn-primary">
{{ $t('household_members_editor.remove_concerned') }}
</button>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.drag-icon {
height: 1.1em;
margin-right: 0.5em;
}
div.participation-details {
display: flex;
flex-direction: row !important;
justify-content: flex-end;
.action {
align-self: flex-start;
margin-right: auto;
}
}
.holder {
display: inline;
vertical-align: super;
font-size: 0.6em;
}
</style>
<script>
import { mapGetters } from 'vuex';
import Person from 'ChillPersonAssets/vuejs/_components/Person/Person.vue';
import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from 'ChillMainAssets/modules/ckeditor5/index.js';
export default {
name: 'MemberDetails',
components: {
Person,
ckeditor: CKEditor.component,
},
props: [
'conc'
],
data() {
return {
editor: ClassicEditor,
};
},
computed: {
...mapGetters( [
'concByPersonId'
]),
isHolder() {
return this.conc.holder;
},
comment: {
get() {
return this.conc.comment;
},
set(text) {
console.log('set comment');
console.log('comment', text);
this.$store.dispatch('setComment', { conc: this.conc, comment: text });
}
},
},
methods: {
toggleHolder() {
this.$store.dispatch('toggleHolder', this.conc);
},
removePosition() {
this.$store.dispatch('removePosition', this.conc);
},
removeConcerned() {
this.$store.dispatch('removeConcerned', this.conc);
},
}
};
</script>

View File

@ -0,0 +1,16 @@
import { createApp } from 'vue';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { appMessages } from './js/i18n';
import { store } from './store';
import App from './App.vue';
const i18n = _createI18n(appMessages);
const app = createApp({
template: `<app></app>`,
})
.use(store)
.use(i18n)
.component('app', App)
.mount('#household_members_editor');

View File

@ -0,0 +1,52 @@
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n'
const appMessages = {
fr: {
household_members_editor: {
household: {
no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage.",
new_household: "Nouveau ménage",
create_household: "Créer un ménage",
search_household: "Chercher un ménage",
will_leave_any_household: "Ne rejoignent pas de ménage",
leave_without_household: "Sans nouveau ménage"
},
concerned: {
title: "Usagers concernés",
add_persons: "Ajouter d'autres usagers",
search: "Rechercher des usagers",
move_to: "Déplacer vers",
persons_to_positionnate: 'Usagers à positionner',
persons_leaving: "Usagers quittant leurs ménages",
},
drop_persons_here: "Glissez-déposez ici les usagers pour la position \"{position}\"",
all_positionnated: "Tous les usagers sont positionnés",
holder: "Titulaire",
is_holder: "Sera titulaire",
is_not_holder: "Ne sera pas titulaire",
remove_position: "Retirer des {position}",
remove_concerned: "Ne plus transférer",
household_part: "Ménage de destination",
dates_title: "Période de validité",
dates: {
start_date: "Début de validité",
end_date: "Fin de validité",
},
confirmation: {
save: "Enregistrer",
there_are_warnings: "Impossible de valider actuellement",
check_those_items: "Veuillez corriger les éléments suivants",
},
give_a_position_to_every_person: "Indiquez une position pour chaque usager concerné",
add_destination: "Indiquez un ménage de destination",
add_at_least_onePerson: "Indiquez au moins un usager à transférer",
}
}
};
Object.assign(appMessages.fr, personMessages.fr);
export {
appMessages
};

View File

@ -0,0 +1,239 @@
import { createStore } from 'vuex';
import { householdMove, householdMoveTest } from './../api.js';
import { datetimeToISO } from 'ChillMainAssets/js/date.js';
const debug = process.env.NODE_ENV !== 'production';
const concerned = window.household_members_editor_data.persons.map(p => {
return {
person: p,
position: null,
allowRemove: false,
holder: false,
comment: "",
};
});
const store = createStore({
strict: debug,
state: {
concerned,
household: window.household_members_editor_data.household,
positions: window.household_members_editor_data.positions,
startDate: new Date(),
allowHouseholdCreate: window.household_members_editor_data.allowHouseholdCreate,
allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch,
allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold,
forceLeaveWithoutHousehold: false,
warnings: [],
},
getters: {
isHouseholdNew(state) {
return !Number.isInteger(state.household.id);
},
hasHousehold(state) {
return state.household !== null;
},
persons(state) {
return state.concerned.map(conc => conc.person);
},
concUnpositionned(state) {
return state.concerned
.filter(conc => conc.position === null)
;
},
positions(state) {
return state.positions;
},
personByPosition: (state) => (position_id) => {
return state.concerned
.filter(conc =>
conc.position !== null ? conc.position.id === position_id : false
)
.map(conc => conc.person)
;
},
concByPosition: (state) => (position_id) => {
return state.concerned
.filter(conc =>
conc.position !== null ? conc.position.id === position_id : false
)
;
},
concByPersonId: (state) => (person_id) => {
return state.concerned
.find(conc => conc.person.id === person_id)
;
},
needsPositionning(state) {
return state.forceLeaveWithoutHousehold === false;
},
buildPayload: (state) => {
let
conc,
payload_conc,
payload = {
concerned: [],
destination: null
}
;
if (state.forceLeaveWithoutHousehold === false) {
payload.destination = {
id: state.household.id,
type: state.household.type
};
}
for (let i in state.concerned) {
conc = state.concerned[i];
payload_conc = {
person: {
id: conc.person.id,
type: conc.person.type
},
start_date: {
datetime: datetimeToISO(state.startDate)
}
};
if (state.forceLeaveWithoutHousehold === false) {
payload_conc.position = {
id: conc.position.id,
type: conc.position.type
};
payload_conc.holder = conc.holder;
payload_conc.comment = conc.comment;
}
payload.concerned.push(payload_conc);
}
return payload;
},
},
mutations: {
addConcerned(state, person) {
let persons = state.concerned.map(conc => conc.person.id);
if (!persons.includes(person.id)) {
state.concerned.push({
person,
position: null,
allowRemove: true,
holder: false,
comment: "",
});
} else {
console.err("person already included");
}
},
markPosition(state, { person_id, position_id}) {
let
position = state.positions.find(pos => pos.id === position_id),
conc = state.concerned.find(c => c.person.id === person_id);
conc.position = position;
},
setComment(state, {conc, comment}) {
conc.comment = comment;
},
toggleHolder(state, conc) {
conc.holder = !conc.holder;
},
removePosition(state, conc) {
conc.holder = false;
conc.position = null;
},
removeConcerned(state, conc) {
state.concerned = state.concerned.filter(c =>
c.person.id !== conc.person.id
)
},
createHousehold(state) {
state.household = { type: 'household', members: [], address: null }
state.forceLeaveWithoutHousehold = false;
},
forceLeaveWithoutHousehold(state) {
state.household = null;
state.forceLeaveWithoutHousehold = true;
},
setStartDate(state, dateI) {
state.startDate = dateI;
},
setWarnings(state, warnings) {
state.warnings = warnings;
},
},
actions: {
addConcerned({ commit, dispatch }, person) {
commit('addConcerned', person);
dispatch('computeWarnings');
},
markPosition({ commit, state, dispatch }, { person_id, position_id }) {
commit('markPosition', { person_id, position_id });
dispatch('computeWarnings');
},
toggleHolder({ commit }, conc) {
commit('toggleHolder', conc);
},
removePosition({ commit, dispatch }, conc) {
commit('removePosition', conc);
dispatch('computeWarnings');
},
removeConcerned({ commit, dispatch }, conc) {
commit('removeConcerned', conc);
dispatch('computeWarnings');
},
createHousehold({ commit, dispatch }) {
commit('createHousehold');
dispatch('computeWarnings');
},
forceLeaveWithoutHousehold({ commit, dispatch }) {
commit('forceLeaveWithoutHousehold');
dispatch('computeWarnings');
},
setStartDate({ commit }, date) {
commit('setStartDate', date);
},
setComment({ commit }, payload) {
commit('setComment', payload);
},
computeWarnings({ commit, state, getters }) {
let warnings = [],
payload;
if (!getters.hasHousehold && !state.forceLeaveWithoutHousehold) {
warnings.push({ m: 'household_members_editor.add_destination', a: {} });
}
if (state.concerned.length === 0) {
warnings.push({ m: 'household_members_editor.add_at_least_onePerson', a: {} });
}
if (getters.concUnpositionned.length > 0
&& !state.forceLeaveWithoutHousehold) {
warnings.push({ m: 'household_members_editor.give_a_position_to_every_person', a: {} })
}
commit('setWarnings', warnings);
},
confirm({ getters, state }) {
let payload = getters.buildPayload,
person_id,
household_id;
householdMove(payload).then(household => {
if (household === null) {
person_id = getters.persons[0].id;
window.location.replace(`/fr/person/${person_id}/general`);
} else {
household_id = household.id;
// nothing to do anymore here, bye-bye !
window.location.replace(`/fr/person/household/${household_id}/members`);
}
});
},
}
});
store.dispatch('computeWarnings');
export { store };

View File

@ -0,0 +1,16 @@
<template>
<span class="chill-entity chill-entity__person">
<span class="chill-entity__person__text">
{{ person.text }}
</span>
</span>
</template>
<script>
export default {
name: 'Person',
props: ['person']
}
</script>

View File

@ -15,7 +15,15 @@ const personMessages = {
person: {
firstname: "Prénom",
lastname: "Nom",
born: "né{e} le ",
born: (ctx) => {
if (ctx.gender === 'man') {
return 'Né le';
} else if (ctx.gender === 'woman') {
return 'Née le';
} else {
return 'Né·e le';
}
},
center_id: "Identifiant du centre",
center_type: "Type de centre",
center_name: "Territoire", // vendée

View File

@ -62,6 +62,19 @@
<li>
<a href="{{ path('chill_person_view', { person_id: p.person.id }) }}" class="sc-button bt-show" target="_blank" title="Voir"></a>
</li>
{% if p.person.isSharingHousehold %}
<li>
<a
href="{{ chill_path_add_return_path(
'chill_person_household_summary',
{ 'household_id': p.person.getCurrentHousehold.id }
) }}"
class="sc-button">
<i class="fa fa-home"></i>
{{ 'household.Household file'|trans }}
</a>
</li>
{% endif %}
</ul>
</div>
</div>

View File

@ -48,7 +48,18 @@
</a>
</li>
<li>
<a href="#" class="sc-button" /><i class="fa fa-sign-out"></i>{{ 'household.Leave'|trans }}</a>
<a
href="{{ chill_path_add_return_path(
'chill_person_household_members_editor',
{
'persons': [ m.person.id ],
'allow_leave_without_household': true
} ) }}"
class="sc-button"
/>
<i class="fa fa-sign-out"></i>
{{ 'household.Leave'|trans }}
</a>
</li>
</ul>
</div>
@ -129,7 +140,9 @@
<ul class="record_actions sticky-form-buttons">
<li>
<a class="sc-button bt-create">
<a
href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'household': household.id }) }}"
class="sc-button bt-create">
{{ 'household.Add a member'|trans }}
</a>
</li>

View File

@ -0,0 +1,23 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title 'household.Edit household members'|trans %}
{% block content %}
<div class="grid-12 parent">
<div class="grid-10 push-1 parent">
<h1>{{ block('title') }}</h1>
<div id="household_members_editor"></div>
</div>
</div>
{% endblock %}
{% block js %}
<script type="text/javascript">
window.household_members_editor_data = {{ data|json_encode|raw }};
</script>
{{ encore_entry_script_tags('household_members_editor') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('household_members_editor') }}
{% endblock %}

View File

@ -24,6 +24,58 @@ class MembersEditorNormalizer implements DenormalizerInterface, DenormalizerAwar
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
// some test about schema first...
$this->performChecks($data);
// route to "leave movement" (all concerned leave household)
// or "move to another household" (all concerned go to another
// household)
if (NULL === $data['destination']) {
return $this->denormalizeLeave($data, $type, $format, $context);
} else {
return $this->denormalizeMove($data, $type, $format, $context);
}
}
private function performChecks($data): void
{
if (NULL == $data['concerned'] ?? NULL
&& FALSE === ·\is_array('concerned')) {
throw new Exception\UnexpectedValueException("The schema does not have any key 'concerned'");
}
if (FALSE === \array_key_exists('destination', $data)) {
throw new Exception\UnexpectedValueException("The schema does not have any key 'destination'");
}
}
protected function denormalizeLeave($data, string $type, string $format, array $context = [])
{
$editor = $this->factory->createEditor(null);
foreach ($data['concerned'] as $key => $concerned) {
$person = $this->denormalizer->denormalize($concerned['person'] ?? null, Person::class,
$format, $context);
$startDate = $this->denormalizer->denormalize($concerned['start_date'] ?? null, \DateTimeImmutable::class,
$format, $context);
if (
NULL === $person
&& NULL === $startDate
) {
throw new Exception\InvalidArgumentException("position with ".
"key $key could not be denormalized: missing ".
"person or start_date.");
}
$editor->leaveMovement($startDate, $person);
}
return $editor;
}
protected function denormalizeMove($data, string $type, string $format, array $context = [])
{
$household = $this->denormalizer->denormalize($data['destination'], Household::class,
$format, $context);
@ -34,11 +86,6 @@ class MembersEditorNormalizer implements DenormalizerInterface, DenormalizerAwar
$editor = $this->factory->createEditor($household);
if (NULL == $data['concerned'] ?? []
&& FALSE === ·\is_array('concerned')) {
throw new Exception\UnexpectedValueException("The schema does not have any key 'concerned'");
}
foreach ($data['concerned'] as $key => $concerned) {
$person = $this->denormalizer->denormalize($concerned['person'] ?? null, Person::class,
$format, $context);
@ -62,9 +109,9 @@ class MembersEditorNormalizer implements DenormalizerInterface, DenormalizerAwar
$editor->addMovement($startDate, $person, $position, $holder,
$comment);
return $editor;
}
return $editor;
}
public function supportsDenormalization($data, string $type, string $format = null)

View File

@ -67,6 +67,116 @@ class HouseholdMemberControllerTest extends WebTestCase
);
}
/**
* @dataProvider provideValidDataMove
*/
public function testMoveMemberToNewHousehold($personId, $householdId, $positionId, \DateTimeInterface $date)
{
$client = $this->getClientAuthenticated();
$client->request(
Request::METHOD_POST,
'/api/1.0/person/household/members/move.json',
[], // parameters
[], // files
[], // server
\json_encode(
[
'concerned' =>
[
[
'person' =>
[
'type' => 'person',
'id' => $personId
],
'start_date' =>
[
'datetime' => $date->format(\DateTimeInterface::RFC3339)
],
'position' =>
[
'type' => 'household_position',
'id' => $positionId
],
'holder' => false,
'comment' => "Introduced by automated test",
],
],
'destination' =>
[
'type' => 'household',
]
],
true)
);
$this->assertEquals(Response::HTTP_OK,
$client->getResponse()->getStatusCode()
);
$data = \json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($data);
$this->assertArrayHasKey('members', $data);
$this->assertIsArray($data['members']);
$this->assertEquals(1, count($data['members']),
"assert new household count one member");
$this->assertArrayHasKey('person', $data['members'][0]);
$this->assertArrayHasKey('id', $data['members'][0]['person']);
$this->assertEquals($personId, $data['members'][0]['person']['id']);
}
/**
* @dataProvider provideValidDataMove
*/
public function testLeaveWithoutHousehold($personId, $householdId, $positionId, \DateTimeInterface $date)
{
$client = $this->getClientAuthenticated();
$client->request(
Request::METHOD_POST,
'/api/1.0/person/household/members/move.json',
[], // parameters
[], // files
[], // server
\json_encode(
[
'concerned' =>
[
[
'person' =>
[
'type' => 'person',
'id' => $personId
],
'start_date' =>
[
'datetime' => $date->format(\DateTimeInterface::RFC3339)
],
'position' =>
[
'type' => 'household_position',
'id' => $positionId
],
'holder' => false,
'comment' => "Introduced by automated test",
],
],
'destination' => null
],
true)
);
$this->assertEquals(Response::HTTP_OK,
$client->getResponse()->getStatusCode()
);
$data = \json_decode($client->getResponse()->getContent(), true);
$this->assertEquals(null, $data);
}
/**
* @dataProvider provideValidDataEditMember
*/
@ -97,7 +207,13 @@ class HouseholdMemberControllerTest extends WebTestCase
$em = self::$container->get(EntityManagerInterface::class);
$personIds = $em->createQuery("SELECT p.id FROM ".Person::class." p ".
"JOIN p.center c WHERE c.name = :center")
"JOIN p.center c ".
"JOIN p.householdParticipations hp ".
"WHERE ".
"c.name = :center ".
"AND hp.startDate < CURRENT_DATE() ".
"AND hp.endDate IS NULL "
)
->setParameter('center', "Center A")
->setMaxResults(100)
->getScalarResult()

View File

@ -22,6 +22,9 @@
namespace Chill\PersonBundle\Tests\Entity;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Entity\Address;
@ -205,4 +208,39 @@ class PersonTest extends \PHPUnit\Framework\TestCase
$this::assertEquals($address3, $p->getLastAddress($addressDate3));
}
public function testIsSharingHousehold()
{
$person = new Person();
$household = new Household();
$positionShare = (new Position())
->setShareHousehold(true);
$positionNotShare = (new Position())
->setShareHousehold(false);
$membership1 = (new HouseholdMember())
->setStartDate(new \DateTimeImmutable('10 years ago'))
->setEndDate(new \DateTimeImmutable('5 years ago'))
->setPerson($person)
->setPosition($positionShare)
;
$household->addMember($membership1);
$membership2 = (new HouseholdMember())
->setStartDate(new \DateTimeImmutable('4 years ago'))
->setEndDate(new \DateTimeImmutable('2 years ago'))
->setPerson($person)
->setPosition($positionNotShare)
;
$household->addMember($membership2);
$this->assertEquals(2, $person->getHouseholdParticipations()
->count());
$this->assertFalse($person->isSharingHousehold());
$this->assertTrue($person->isSharingHousehold(
new \DateTimeImmutable('6 years ago')));
$this->assertFalse($person->isSharingHousehold(
new \DateTimeImmutable('3 years ago')));
}
}

View File

@ -802,18 +802,61 @@ paths:
type: object
properties:
person:
$ref: '#/components/schemas/PersonById'
$ref: '#/components/schemas/PersonById'
start_date:
$ref: '#/components/schemas/Date'
$ref: '#/components/schemas/Date'
position:
$ref: '#/components/schemas/HouseholdPosition'
$ref: '#/components/schemas/HouseholdPosition'
holder:
type: boolean
comment:
type: string
destination:
oneOf:
- $ref: '#/components/schemas/Household'
$ref: '#/components/schemas/Household'
examples:
Moving person to a new household:
value:
concerned:
-
person:
id: 0
type: person
position:
type: position
id: 1
start_date:
datetime: 2021-06-01T00:00:00+02:00
comment: "This is my comment for moving"
holder: false
destination:
type: household
Moving person to an existing household:
value:
concerned:
-
person:
id: 0
type: person
position:
type: position
id: 1
start_date:
datetime: 2021-06-01T00:00:00+02:00
comment: "This is my comment for moving"
holder: false
destination:
type: household
id: 54
Removing a person from any household:
value:
concerned:
-
person:
id: 0
type: person
start_date:
datetime: 2021-06-01T00:00:00+02:00
destination: null
responses:
401:
description: "Unauthorized"

View File

@ -8,5 +8,7 @@ module.exports = function(encore, entries)
ChillPersonAssets: __dirname + '/Resources/public'
});
encore.addEntry('accompanying_course', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
encore.addEntry('household_members_editor', __dirname + '/Resources/public/vuejs/HouseholdMembersEditor/index.js');
encore.addEntry('vue_accourse', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
};

View File

@ -17,6 +17,8 @@ household:
Those members does not share address: Ces usagers ne partagent pas l'adresse du ménage.
Any persons into this position: Aucune personne n'appartient au ménage à cette position.
Leave: Quitter le ménage
Household file: Dossier ménage
Add a member: Ajouter un membre
Update membership: Modifier
successfully saved member: Membre enregistré avec succès
Start date: Date de début de l'appartenance au ménage
@ -29,6 +31,7 @@ household:
Current household members: Membres actuels du ménage
Household summary: Résumé
Addresses: Adresses
Edit household members: Modifier l'appartenance au ménage
and x other persons: >-
{x, plural,
one {et une autre personne}