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 @@
+
+
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 @@
+
+ {{ $t('household_members_editor.concerned.move_to') }}:
+ {{ $t('household_members_editor.confirmation.check_those_items') }}
+
+
+
+ {{ $t('household_members_editor.concerned.title') }}
+
+
+ {{ $t('household_members_editor.concerned.persons_to_positionnate') }}
+
+
+ {{ $t('household_members_editor.concerned.persons_leaving') }}
+
+
+
+
+
+ {{ position.label.fr }}
+
+
+
+
+
+
+
+
+
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 @@
+
+ {{ $t('household_members_editor.dates_title') }}
+
+ {{ $t('household_members_editor.household_part') }}
+
+
+
+
+
+
+
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 @@
+
+
+
+