diff --git a/src/Bundle/ChillMainBundle/Resources/public/img/draggable.svg b/src/Bundle/ChillMainBundle/Resources/public/img/draggable.svg new file mode 100644 index 000000000..0b3561cce --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/img/draggable.svg @@ -0,0 +1,121 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/js/date.js b/src/Bundle/ChillMainBundle/Resources/public/js/date.js new file mode 100644 index 000000000..7b9bf88a2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/js/date.js @@ -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 +}; diff --git a/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss b/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss index 37bf2f253..6b91d1bf0 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss @@ -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"; diff --git a/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss index 0c256334c..30c7f561d 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss @@ -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; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js index 025aeeb44..d89470bc4 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js @@ -62,7 +62,7 @@ const messages = { person: "un nouvel usager", thirdparty: "un nouveau tiers" }, - } + }, } }; diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php index ec22b6851..e78838e22 100644 --- a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php @@ -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 ]); } diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 3e54cf9de..4c5ee1f80 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -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', + ] + ], + ] + ], ] ]); } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php index b9eba8e9c..5b186dfc2 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php @@ -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( diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php index 58a731697..b891bbc15 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php @@ -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; } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Position.php b/src/Bundle/ChillPersonBundle/Entity/Household/Position.php index b8fb5a990..b1bfef173 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Position.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Position.php @@ -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; diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index 0d9bb7cf6..0ec04bf35 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -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); + } } diff --git a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php index 527404c75..bc25c725f 100644 --- a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php +++ b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php @@ -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; + } } diff --git a/src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php b/src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php index 611308b19..adf55277e 100644 --- a/src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php +++ b/src/Bundle/ChillPersonBundle/Household/MembersEditorFactory.php @@ -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); } diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php index 6d07f791e..a02de20dd 100644 --- a/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php @@ -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(); + } } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/sass/person_with_period.scss b/src/Bundle/ChillPersonBundle/Resources/public/sass/person_with_period.scss index e7ade6d4f..da37ab580 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/sass/person_with_period.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/sass/person_with_period.scss @@ -59,5 +59,3 @@ div.list-household-members--summary { } } } - - diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue new file mode 100644 index 000000000..7fc138ee9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/api.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/api.js new file mode 100644 index 000000000..8f3d07d63 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/api.js @@ -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 +}; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue new file mode 100644 index 000000000..c3595418f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Confirmation.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Confirmation.vue new file mode 100644 index 000000000..cc97d7ef9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Confirmation.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue new file mode 100644 index 000000000..be332523d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Household.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Household.vue new file mode 100644 index 000000000..db9670d43 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Household.vue @@ -0,0 +1,80 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue new file mode 100644 index 000000000..7147ed9c7 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/index.js new file mode 100644 index 000000000..c9f5bb111 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/index.js @@ -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: ``, +}) +.use(store) +.use(i18n) +.component('app', App) +.mount('#household_members_editor'); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js new file mode 100644 index 000000000..116203ba1 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js @@ -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 +}; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js new file mode 100644 index 000000000..32eb5b722 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js @@ -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 }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Person/Person.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Person/Person.vue new file mode 100644 index 000000000..274d9ee88 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Person/Person.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.js index 0f019c26b..f83004ce8 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.js @@ -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 diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig index 3fe5c56db..4fef55ceb 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig @@ -62,6 +62,19 @@
  • + {% if p.person.isSharingHousehold %} +
  • + + + {{ 'household.Household file'|trans }} + +
  • + {% endif %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Household/members.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Household/members.html.twig index cc1c8f321..72ac0d4a1 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Household/members.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Household/members.html.twig @@ -48,7 +48,18 @@
  • - {{ 'household.Leave'|trans }} + + + {{ 'household.Leave'|trans }} +
  • @@ -129,7 +140,9 @@