Merge branch 'feature/form-move-household-with-checkboxes' into 'master'

rewrite form move household

See merge request Chill-Projet/chill-bundles!159
This commit is contained in:
Julien Fastré 2021-10-18 09:14:28 +00:00
commit 89b0b94d22
31 changed files with 1102 additions and 356 deletions

View File

@ -14,6 +14,7 @@ and this project adheres to
* [3party]: show parent in list
* [3party]: change color for badge "child"
* [3party]: fix address creation
* [household members editor] finalisation of editor
@ -40,10 +41,14 @@ and this project adheres to
* [FilterOrder]: add development kit for generating filter and ordering in list
* [Capitalization of names] person names are capitalized on creation, on prePersist event
* [On-The-Fly] modale works for showing, editing and creating person or thirdparty ;
* [AccompanyingCourse Resume page] associated persons list, can see household when hover, and with show on-the-fly modale when clicking person ;
### test release 2021-10-04
* [Household editor][UI] Update how household suggestion and addresses are picked;
See https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/80
* [AddAddress] Handle address suggestion;
* [CenterType][Create a person] when overriding the ACL rules, allow to show a PickCenterType
when no centers are reachable by the default ACL.
@ -62,8 +67,30 @@ and this project adheres to
https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/37
https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/221
* [On-The-Fly] modale works for showing, editing and creating person or thirdparty ;
* [AccompanyingCourse Resume page] associated persons list, can see household when hover, and with show on-the-fly modale when clicking person ;
* [Household editor] suggest only temporarily addresses;
See https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/82
* On-The-Fly modale works for showing, editing and creating person and thirdparty ;
* AccompanyingCourse Resume page: list associated persons by household, see household when hover, and show on-the-fly modale when clicking on person ;
* [AddAddress] Handle address suggestion;
* [AddAddress][Entity address]: add a link between address and address reference;
* [Household editor] suggest household by comparing the temporary addresses from courses;
## Test release yyyy-mm-dd
See https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/81
* On-The-Fly modale works for showing, editing and creating person and thirdparty
## Test released
<!--
Coming soon...
### Test release yyyy-mm-dd
-->
## Stable releases
No stable releases for v2+
>>>>>>> 107b8131 (update changelog)

View File

@ -23,7 +23,7 @@ class Address
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @groups({"write"})
* @Groups({"write"})
*/
private $id;
@ -31,7 +31,7 @@ class Address
* @var string
*
* @ORM\Column(type="string", length=255)
* @groups({"write"})
* @Groups({"write"})
*/
private $street = '';
@ -39,7 +39,7 @@ class Address
* @var string
*
* @ORM\Column(type="string", length=255)
* @groups({"write"})
* @Groups({"write"})
*/
private $streetNumber = '';
@ -47,7 +47,7 @@ class Address
* @var PostalCode
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @groups({"write"})
* @Groups({"write"})
*/
private $postcode;
@ -55,7 +55,7 @@ class Address
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private $floor;
@ -63,7 +63,7 @@ class Address
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private $corridor;
@ -71,7 +71,7 @@ class Address
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private $steps;
@ -79,7 +79,7 @@ class Address
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private $buildingName;
@ -87,7 +87,7 @@ class Address
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private $flat;
@ -95,7 +95,7 @@ class Address
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private $distribution;
@ -103,7 +103,7 @@ class Address
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private $extra;
@ -114,7 +114,7 @@ class Address
* @var \DateTime
*
* @ORM\Column(type="date")
* @groups({"write"})
* @Groups({"write"})
*/
private \DateTime $validFrom;
@ -125,13 +125,13 @@ class Address
* @var \DateTime|null
*
* @ORM\Column(type="date", nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private ?\DateTime $validTo = null;
/**
* True if the address is a "no address", aka homeless person, ...
* @groups({"write"})
* @Groups({"write"})
* @ORM\Column(type="boolean")
*
* @var bool
@ -144,7 +144,7 @@ class Address
* @var Point|null
*
* @ORM\Column(type="point", nullable=true)
* @groups({"write"})
* @Groups({"write"})
*/
private $point;
@ -154,7 +154,7 @@ class Address
* @var ThirdParty|null
*
* @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty")
* @groups({"write"})
* @Groups({"write"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $linkedToThirdParty;
@ -166,6 +166,12 @@ class Address
*/
private $customs = [];
/**
* @ORM\ManyToOne(targetEntity=AddressReference::class)
* @Groups({"write"})
*/
private ?AddressReference $addressReference = null;
public function __construct()
{
$this->validFrom = new \DateTime();
@ -376,6 +382,7 @@ class Address
public static function createFromAddress(Address $original) : Address
{
return (new Address())
->setAddressReference($original->getAddressReference())
->setBuildingName($original->getBuildingName())
->setCorridor($original->getCorridor())
->setCustoms($original->getCustoms())
@ -402,6 +409,7 @@ class Address
->setPostcode($original->getPostcode())
->setStreet($original->getStreet())
->setStreetNumber($original->getStreetNumber())
->setAddressReference($original)
;
}
@ -549,5 +557,22 @@ class Address
return $this;
}
/**
* @return AddressReference|null
*/
public function getAddressReference(): ?AddressReference
{
return $this->addressReference;
}
/**
* @param AddressReference|null $addressReference
* @return Address
*/
public function setAddressReference(?AddressReference $addressReference = null): Address
{
$this->addressReference = $addressReference;
return $this;
}
}

View File

@ -0,0 +1,39 @@
const _fetchAction = (page, uri, params) => {
const item_per_page = 50;
if (params === undefined) {
params = {};
}
let url = uri + '?' + new URLSearchParams({ item_per_page, page, ...params });
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
}).then(response => {
if (response.ok) { return response.json(); }
throw Error({ m: response.statusText });
});
};
const fetchResults = async (uri, params) => {
let promises = [],
page = 1;
let firstData = await _fetchAction(page, uri, params);
promises.push(Promise.resolve(firstData.results));
if (firstData.pagination.more) {
do {
page = ++page;
promises.push(_fetchAction(page, uri, params).then(r => Promise.resolve(r.results)));
} while (page * firstData.pagination.items_per_page < firstData.count)
}
return Promise.all(promises).then(values => values.flat());
};
export {
fetchResults
};

View File

@ -1,15 +1,7 @@
import { fetchResults } from 'ChillMainAssets/lib/api/download.js';
const fetchScopes = () => {
return window.fetch('/api/1.0/main/scope.json').then(response => {
if (response.ok) {
return response.json();
}
}).then(data => {
//console.log(data);
return new Promise((resolve, reject) => {
//console.log(data);
resolve(data.results);
});
});
return fetchResults('/api/1.0/main/scope.json');
};
export {

View File

@ -589,6 +589,14 @@ export default {
'point': this.entity.selected.address.point.coordinates
});
}
// add the address reference, if any
if (this.entity.selected.address.addressReference !== undefined) {
newAddress = Object.assign(newAddress, {
'addressReference': this.entity.selected.address.addressReference
});
}
if (this.validFrom) {
console.log('add validFrom in fetch body', this.entity.selected.valid.from);
newAddress = Object.assign(newAddress, {
@ -733,6 +741,9 @@ export default {
},
/**
*
* Called when the event pick-address is emitted, which is, by the way,
* called when an address suggestion is picked.
*
* @param address the address selected
*/

View File

@ -95,6 +95,9 @@ export default {
},
selectAddress(value) {
this.entity.selected.address = value;
this.entity.selected.address.addressReference = {
id: value.id
};
this.entity.selected.address.street = value.street;
this.entity.selected.address.streetNumber = value.streetNumber;
this.entity.selected.writeNew.address = false;

View File

@ -3,6 +3,7 @@
namespace Chill\MainBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@ -33,6 +34,9 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
$data['extra'] = $address->getExtra();
$data['validFrom'] = $address->getValidFrom();
$data['validTo'] = $address->getValidTo();
$data['addressReference'] = $this->normalizer->normalize($address->getAddressReference(), $format, [
AbstractNormalizer::GROUPS => ['read']
]);
return $data;
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add a link between address and address reference
*/
final class Version20210929192242 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add a link between address and address reference';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address ADD addressReference_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT FK_165051F647069464 FOREIGN KEY (addressReference_id) REFERENCES chill_main_address_reference (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_165051F647069464 ON chill_main_address (addressReference_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address DROP addressReference_id');
}
}

View File

@ -4,24 +4,31 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepositoryInterface;
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class HouseholdApiController extends ApiController
{
private HouseholdRepository $householdRepository;
public function __construct(HouseholdRepository $householdRepository)
{
private HouseholdACLAwareRepositoryInterface $householdACLAwareRepository;
public function __construct(
HouseholdRepository $householdRepository,
HouseholdACLAwareRepositoryInterface $householdACLAwareRepository
) {
$this->householdRepository = $householdRepository;
$this->householdACLAwareRepository = $householdACLAwareRepository;
}
public function householdAddressApi($id, Request $request, string $_format): Response
{
@ -37,7 +44,7 @@ class HouseholdApiController extends ApiController
{
// TODO add acl
$count = $this->householdRepository->countByAccompanyingPeriodParticipation($person);
$count = $this->householdRepository->countByAccompanyingPeriodParticipation($person);
$paginator = $this->getPaginatorFactory()->create($count);
if ($count === 0) {
@ -93,4 +100,27 @@ class HouseholdApiController extends ApiController
return $this->json(\array_values($addresses), Response::HTTP_OK, [],
[ 'groups' => [ 'read' ] ]);
}
/**
*
* @Route("/api/1.0/person/household/by-address-reference/{id}.json",
* name="chill_api_person_household_by_address_reference")
* @param AddressReference $addressReference
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function getHouseholdByAddressReference(AddressReference $addressReference): Response
{
// TODO ACL
$this->denyAccessUnlessGranted('ROLE_USER');
$total = $this->householdACLAwareRepository->countByAddressReference($addressReference);
$paginator = $this->getPaginatorFactory()->create($total);
$households = $this->householdACLAwareRepository->findByAddressReference($addressReference,
$paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
$collection = new Collection($households, $paginator);
return $this->json($collection, Response::HTTP_OK, [], [
AbstractNormalizer::GROUPS => ['read']
]);
}
}

View File

@ -77,13 +77,6 @@ class PersonApiController extends ApiController
$a = $participation->getAccompanyingPeriod()->getAddressLocation();
$addresses[$a->getId()] = $a;
}
if (null !== $personLocation = $participation
->getAccompanyingPeriod()->getPersonLocation()) {
$a = $personLocation->getCurrentHouseholdAddress();
if (null !== $a) {
$addresses[$a->getId()] = $a;
}
}
}
// remove the actual address

View File

@ -0,0 +1,103 @@
<?php
namespace Chill\PersonBundle\Repository\Household;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Security\Authorization\HouseholdVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
final class HouseholdACLAwareRepository implements HouseholdACLAwareRepositoryInterface
{
private EntityManagerInterface $em;
private AuthorizationHelper $authorizationHelper;
private Security $security;
public function __construct(EntityManagerInterface $em, AuthorizationHelper $authorizationHelper, Security $security)
{
$this->em = $em;
$this->authorizationHelper = $authorizationHelper;
$this->security = $security;
}
public function countByAddressReference(AddressReference $addressReference): int
{
$qb = $this->buildQueryByAddressReference($addressReference);
$qb = $this->addACL($qb);
return $qb->select('COUNT(h)')
->getQuery()
->getSingleScalarResult();
}
public function findByAddressReference(
AddressReference $addressReference,
?int $firstResult = 0,
?int $maxResult = 50
): array {
$qb = $this->buildQueryByAddressReference($addressReference);
$qb = $this->addACL($qb);
return $qb
->select('h')
->setFirstResult($firstResult)
->setMaxResults($maxResult)
->getQuery()
->getResult();
}
public function buildQueryByAddressReference(AddressReference $addressReference): QueryBuilder
{
$qb = $this->em->createQueryBuilder();
$qb
->select('h')
->from(Household::class, 'h')
->join('h.addresses', 'address')
->where(
$qb->expr()->eq('address.addressReference', ':reference')
)
->setParameter(':reference', $addressReference)
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte('address.validFrom', ':today'),
$qb->expr()->orX(
$qb->expr()->isNull('address.validTo'),
$qb->expr()->gt('address.validTo', ':today')
)
)
)
->setParameter('today', new \DateTime('today'))
;
return $qb;
}
public function addACL(QueryBuilder $qb, string $alias = 'h'): QueryBuilder
{
$centers = $this->authorizationHelper->getReachableCenters(
$this->security->getUser(),
HouseholdVoter::SHOW
);
if ([] === $centers) {
return $qb
->andWhere("'FALSE' = 'TRUE'");
}
$qb
->join($alias.'.members', 'members')
->join('members.person', 'person')
->andWhere(
$qb->expr()->in('person.center', ':centers')
)
->setParameter('centers', $centers);
return $qb;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Chill\PersonBundle\Repository\Household;
use Chill\MainBundle\Entity\AddressReference;
use Chill\PersonBundle\Entity\Household\Household;
interface HouseholdACLAwareRepositoryInterface
{
public function countByAddressReference(AddressReference $addressReference): int;
/**
* @param AddressReference $addressReference
* @param int|null $firstResult
* @param int|null $maxResult
* @return array|Household[]
*/
public function findByAddressReference(
AddressReference $addressReference,
?int $firstResult = 0,
?int $maxResult = 50
): array;
}

View File

@ -0,0 +1,10 @@
import { fetchResults } from 'ChillMainAssets/lib/api/download.js';
const fetchHouseholdByAddressReference = async (reference) => {
const url = `/api/1.0/person/household/by-address-reference/${reference.id}.json`
return fetchResults(url);
};
export {
fetchHouseholdByAddressReference
};

View File

@ -1,34 +1,150 @@
<template>
<household></household>
<concerned v-if="hasHouseholdOrLeave"></concerned>
<dates v-if="showConfirm"></dates>
<confirmation v-if="showConfirm"></confirmation>
<ol class="breadcrumb">
<li
v-for="s in steps"
class="breadcrumb-item" :class="{ active: step === s }"
>
{{ $t('household_members_editor.app.steps.'+s) }}
</li>
</ol>
<concerned v-if="step === 'concerned'"></concerned>
<household v-if="step === 'household'" @ready-to-go="goToNext"></household>
<household-address v-if="step === 'household_address'"></household-address>
<positioning v-if="step === 'positioning'"></positioning>
<dates v-if="step === 'confirm'"></dates>
<confirmation v-if="step === 'confirm'"></confirmation>
<ul class="record_actions sticky-form-buttons">
<li class="cancel" v-if="step !== 'concerned' || hasReturnPath">
<button class="btn btn-cancel" @click="goToPrevious">
{{ $t('household_members_editor.app.cancel') }}
</button>
</li>
<li v-if="step !== 'confirm'">
<button class="btn btn-action" @click="goToNext" :disabled="!isNextAllowed">
{{ $t('household_members_editor.app.next') }}&nbsp;<i class="fa fa-arrow-right"></i>
</button>
</li>
<li v-else>
<button class="btn btn-save" @click="confirm" :disabled="hasWarnings">
{{ $t('household_members_editor.app.save') }}
</button>
</li>
</ul>
</template>
<script>
import { mapGetters } from 'vuex';
import {mapGetters, mapState} from 'vuex';
import Concerned from './components/Concerned.vue';
import Household from './components/Household.vue';
import HouseholdAddress from './components/HouseholdAddress';
import Dates from './components/Dates.vue';
import Confirmation from './components/Confirmation.vue';
import Positioning from "./components/Positioning";
export default {
name: 'App',
components: {
Positioning,
Concerned,
Household,
HouseholdAddress,
Dates,
Confirmation,
},
data() {
return {
step: 'concerned',
};
},
computed: {
...mapGetters([
'hasHouseholdOrLeave',
'hasPersonsWellPositionnated',
]),
showConfirm () {
return this.$store.getters.hasHouseholdOrLeave
&& this.$store.getters.hasPersonsWellPositionnated;
...mapState({
hasWarnings: (state) => state.warnings.length > 0 || state.errors.length > 0,
}),
steps() {
let s = ['concerned', 'household'];
if (this.$store.getters.isHouseholdNew) {
s.push('household_address');
}
if (!this.$store.getters.isModeLeave) {
s.push('positioning');
}
s.push('confirm');
return s;
},
hasReturnPath() {
let params = new URLSearchParams(window.location.search);
return params.has('returnPath');
},
// return true if the next step is allowed
isNextAllowed() {
switch (this.$data.step) {
case 'concerned':
return this.$store.state.concerned.length > 0;
case 'household':
return this.$store.state.mode !== null;
case 'household_address':
return this.$store.getters.hasHouseholdAddress || this.$store.getters.isHouseholdForceNoAddress;
case 'positioning':
return this.$store.getters.hasHouseholdOrLeave
&& this.$store.getters.hasPersonsWellPositionnated;
}
return false;
},
},
methods: {
goToNext() {
console.log('go to next');
switch (this.$data.step) {
case 'concerned':
this.$data.step = 'household';
break;
case 'household':
if (this.$store.getters.isHouseholdNew) {
this.$data.step = 'household_address';
break;
} else if (this.$store.getters.isModeLeave) {
this.$data.step = 'confirm';
break;
} else {
this.$data.step = 'positioning';
break;
}
case 'household_address':
this.$data.step = 'positioning';
break;
case 'positioning':
this.$data.step = 'confirm';
break;
}
},
goToPrevious() {
if (this.$data.step === 'concerned') {
let params = new URLSearchParams(window.location.search);
if (params.has('returnPath')) {
window.location.replace(params.get('returnPath'));
} else {
return;
}
}
let s = this.steps;
let index = s.indexOf(this.$data.step);
if (s[index - 1] === undefined) {
throw Error("step not found");
}
this.$data.step = s[index - 1];
},
confirm() {
this.$store.dispatch('confirm');
},
}
}

View File

@ -1,118 +1,41 @@
<template>
<h2 class="mt-4">{{ $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') }}
{{ $t('household_members_editor.concerned.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"
>
<div class="item-row">
<div class="item-col">
<div>
<person-render-box render="badge" :options="{}" :person="conc.person"></person-render-box>
</div>
<div v-if="conc.person.birthdate !== null">
{{ $t('person.born', {'gender': conc.person.gender} ) }}
{{ $d(conc.person.birthdate.datetime, 'short') }}
</div>
</div>
<div class="item-col">
<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 btn-primary">
{{ $t('household_members_editor.remove_concerned') }}
</button>
</div>
</div>
</div>
</div>
<p>
{{ $t('household_members_editor.concerned.persons_will_be_moved') }}&nbsp;:
<span v-for="c in concerned">
<person-render-box render="badge" :options="{addLink: false}" :person="c.person"></person-render-box>
<button class="btn" @click="removePerson(c.person)" v-if="c.allowRemove" style="padding-left:0;">
<span class="fa-stack fa-lg" :title="$t('household_members_editor.concerned.remove_concerned')">
<i class="fa fa-circle fa-stack-1x text-danger"></i>
<i class="fa fa-times fa-stack-1x"></i>
</span>
</button>
</span>
</p>
</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 v-if="concByPosition(position.id).length > 0" 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>
<div v-else>
<p class="chill-no-data-statement">{{ $t('household_members_editor.concerned.no_person_in_position') }}</p>
</div>
</div>
</div>
<ul class="record_actions">
<li>
<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>
</li>
</ul>
</template>
<style lang="scss">
div.person {
cursor: move;
* {
cursor: move
}
}
.move_to {
.move_hint {
@ -124,33 +47,26 @@ div.person {
</style>
<script>
import { mapGetters } from 'vuex';
import { mapState, mapGetters } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import MemberDetails from './MemberDetails.vue';
import { ISOToDatetime } from 'ChillMainAssets/chill/js/date.js';
export default {
name: 'Concerned',
components: {
AddPersons,
MemberDetails,
PersonRenderBox,
},
computed: {
...mapState([
'concerned'
]),
...mapGetters([
'concUnpositionned',
'positions',
'concByPosition',
'needsPositionning'
'persons',
]),
noPerson () {
return this.$store.getters.persons.length === 0;
},
allPersonsPositionnated () {
return this.$store.getters.persons.length > 0
&& this.$store.getters.concUnpositionned.length === 0;
},
},
data() {
return {
@ -172,11 +88,9 @@ export default {
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
},
moveToPosition(person_id, position_id) {
this.$store.dispatch('markPosition', { person_id, position_id });
},
removeConcerned(conc) {
this.$store.dispatch('removeConcerned', conc);
removePerson(person) {
console.log('remove person in concerned', person);
this.$store.dispatch('removePerson', person);
},
}
}

View File

@ -1,28 +1,19 @@
<template>
<div v-if="hasWarnings" class="alert alert-warning">
<div v-if="hasWarning" class="alert alert-warning">
{{ $t('household_members_editor.confirmation.there_are_warnings') }}
</div>
<p v-if="hasWarnings">
<p v-if="hasWarning">
{{ $t('household_members_editor.confirmation.check_those_items') }}
</p>
<ul>
<li v-for="(msg, index) in warnings">
<li v-for="(msg, index) in warnings" class="warning">
{{ $t(msg.m, msg.a) }}
</li>
<li v-for="msg in errors">
<li v-for="msg in errors" class="error">
{{ msg }}
</li>
</ul>
<ul class="record_actions sticky-form-buttons">
<li>
<button class="btn btn-save" :disabled="hasWarnings" @click="confirm">
{{ $t('household_members_editor.confirmation.save') }}
</button>
</li>
</ul>
</template>
@ -36,17 +27,11 @@ export default {
name: 'Confirmation',
computed: {
...mapState({
hasWarnings: (state) => state.warnings.length > 0 || state.errors.length > 0,
warnings: (state) => state.warnings,
errors: (state) => state.errors,
hasNoWarnings: (state) => state.warnings.length === 0 && state.errors.length === 0,
hasWarnings: (state) => state.warnings.length > 0 || state.errors.length > 0,
}),
},
methods: {
confirm() {
this.$store.dispatch('confirm');
}
}
}
</script>

View File

@ -0,0 +1,50 @@
<template>
<div class="flex-table" v-if="hasHousehold">
<div class="item-bloc">
<household-render-box :household="fakeHouseholdWithConcerned"></household-render-box>
</div>
</div>
<div class="flex-table" v-if="isModeLeave">
<div class="item-bloc">
<section>
<div class="item-row">
<div class="item-col">
<div class="h4">
<span class="fa-stack fa-lg">
<i class="fa fa-home fa-stack-1x"></i>
<i class="fa fa-ban fa-stack-2x text-danger"></i>
</span>
{{ $t('household_members_editor.household.leave_without_household') }}
</div>
</div>
</div>
<div class="item-row">
{{ $t('household_members_editor.household.will_leave_any_household_explanation')}}
</div>
</section>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue';
export default {
name: "CurrentHousehold",
components: {
HouseholdRenderBox,
},
computed: {
...mapGetters([
'hasHousehold',
'fakeHouseholdWithConcerned',
'isModeLeave'
])
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,5 +1,8 @@
<template>
<h2>{{ $t('household_members_editor.dates_title') }}</h2>
<current-household></current-household>
<h2>{{ $t('household_members_editor.dates.dates_title') }}</h2>
<p>
<label for="start_date">
@ -11,8 +14,13 @@
<script>
import CurrentHousehold from "./CurrentHousehold";
export default {
name: 'Dates',
components: {
CurrentHousehold
},
computed: {
startDate: {
get() {
@ -23,10 +31,10 @@ export default {
].join('-');
},
set(value) {
let
let
[year, month, day] = value.split('-'),
dValue = new Date(year, month-1, day);
this.$store.dispatch('setStartDate', dValue);
}
}

View File

@ -2,141 +2,89 @@
<h2 class="mt-4">{{ $t('household_members_editor.household_part') }}</h2>
<div v-if="mode == null">
<div class="alert alert-info" v-if="!hasHousehold">
{{ $t('household_members_editor.household.no_household_choose_one') }}
</div>
<template v-else>
<current-household></current-household>
</template>
<div class="alert alert-info">{{ $t('household_members_editor.household.no_household_choose_one') }}</div>
<div class="flex-table householdSuggestionList">
<div v-if="isModeNewAllowed" class="item-bloc">
<div>
<section>
<div class="item-row">
<div class="item-col">
<div class="h4">
<i class="fa fa-home"></i> {{ $t('household_members_editor.household.new_household') }}
</div>
<div v-if="hasHouseholdSuggestion" class="householdSuggestions my-5">
<h4 class="mb-3">
{{ $t('household_members_editor.household.household_suggested') }}
</h4>
<p>{{ $t('household_members_editor.household.household_suggested_explanation') }}</p>
<div class="accordion" id="householdSuggestions">
<div class="accordion-item">
<h2 class="accordion-header" id="heading_household_suggestions">
<button v-if="!showHouseholdSuggestion"
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
aria-expanded="false"
@click="toggleHouseholdSuggestion">
{{ $tc('household_members_editor.show_household_suggestion', countHouseholdSuggestion) }}
</button>
<button v-if="showHouseholdSuggestion"
class="accordion-button"
type="button"
data-bs-toggle="collapse"
aria-expanded="true"
@click="toggleHouseholdSuggestion">
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
<!-- disabled bootstrap behaviour: data-bs-target="#collapse_household_suggestions" aria-controls="collapse_household_suggestions" -->
</h2>
<div class="accordion-collapse" id="collapse_household_suggestions"
aria-labelledby="heading_household_suggestions" data-bs-parent="#householdSuggestions">
<div v-if="showHouseholdSuggestion">
<div class="flex-table householdSuggestionList">
<div v-for="s in getSuggestions" class="item-bloc">
<household-render-box :household="s.household"></household-render-box>
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="selectHousehold(s.household)">
{{ $t('household_members_editor.select_household') }}
</button>
</li>
</ul>
</div>
</div>
</section>
<ul class="record_actions">
<li>
<button @click="setModeNew" class="btn btn-sm btn-create">{{ $t('household_members_editor.household.create_household') }}</button>
</li>
</ul>
</div>
</div>
<!-- if allow leave household -->
<div v-if="isModeLeaveAllowed" class="item-bloc">
<div>
<section>
<div class="item-row">
<div class="item-col">
<div class="h4">
<span class="fa-stack fa-lg">
<i class="fa fa-home fa-stack-1x"></i>
<i class="fa fa-ban fa-stack-2x text-danger"></i>
</span>
{{ $t('household_members_editor.household.leave_without_household') }}
</div>
</div>
</div>
<div class="item-row">
{{ $t('household_members_editor.household.will_leave_any_household_explanation')}}
</div>
</section>
<ul class="record_actions">
<li>
<button @click="setModeLeave" class="btn btn-sm">
<i class="fa fa-sign-out"></i>
{{ $t('household_members_editor.household.leave') }}
</button>
</li>
</ul>
</div>
</div>
<div v-for="item in getSuggestions">
<div class="item-bloc">
<household-render-box :household="item.household"></household-render-box>
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="selectHousehold(item.household)">
{{ $t('household_members_editor.select_household') }}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div v-else>
<div class="flex-table">
<div class="item-bloc">
<template v-if="isModeLeave">
<section>
<div class="item-row">
<div class="item-col">
<div class="h4">
<span class="fa-stack fa-lg">
<i class="fa fa-home fa-stack-1x"></i>
<i class="fa fa-ban fa-stack-2x text-danger"></i>
</span>
{{ $t('household_members_editor.household.leave_without_household') }}
</div>
</div>
</div>
<div class="item-row">
{{ $t('household_members_editor.household.will_leave_any_household_explanation')}}
</div>
</section>
</template>
<template v-else>
<household-render-box :household="household" :isAddressMultiline="true"></household-render-box>
<ul class="record_actions">
<li>
<add-address
:context="getAddressContext"
:key="addAddress.key"
:options="addAddress.options"
:addressChangedCallback="addressChanged"
></add-address>
</li>
<li v-if="hasHouseholdAddress">
<button class="btn btn-remove"
@click="removeHouseholdAddress">
{{ $t('household_members_editor.household.remove_address') }}
</button>
</li>
</ul>
</template>
</div>
<ul v-if="isModeNewAllowed || isModeLeaveAllowed || getModeSuggestions.length > 0" class="record_actions">
<li>
<button class="btn btn-sm btn-chill-beige" @click="resetMode">
{{ $t('household_members_editor.household.reset_mode') }}
</button>
</li>
</ul>
</div>
</div>
<ul class="record_actions">
<li v-if="hasHousehold">
<button @click="resetMode" class="btn btn-sm btn-misc">{{ $t('household_members_editor.household.reset_mode')}}</button>
</li>
<li v-if="!hasHousehold">
<button @click="setModeNew" class="btn btn-sm btn-create">{{ $t('household_members_editor.household.create_household') }}</button>
</li>
<li v-if="isModeLeaveAllowed && !hasHousehold">
<button @click="setModeLeave" class="btn btn-sm btn-misc">
<i class="fa fa-sign-out"></i>
{{ $t('household_members_editor.household.leave') }}
</button>
</li>
</ul>
</template>
<script>
import { mapGetters, mapState } from 'vuex';
import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress.vue';
import CurrentHousehold from './CurrentHousehold';
export default {
name: 'Household',
components: {
CurrentHousehold,
HouseholdRenderBox,
AddressRenderBox,
AddAddress,
},
emits: ['readyToGo'],
data() {
return {
addAddress: {
@ -179,6 +127,7 @@ export default {
'getAddressContext',
]),
...mapState([
'household',
'showHouseholdSuggestion',
'showAddressSuggestion',
'mode',
@ -190,13 +139,21 @@ export default {
return false;
return this.$store.state.allowHouseholdSearch && !this.$store.getters.hasHousehold;
},
isHouseholdNewDesactivated() {
return this.$store.state.mode !== null && !this.$store.getters.isHouseholdNew;
},
isHouseholdLeaveDesactivated() {
return this.$store.state.mode !== null && this.$store.state.mode !== "leave";
}
},
methods: {
setModeNew() {
this.$store.dispatch('createHousehold');
this.$emit('readyToGo');
},
setModeLeave() {
this.$store.dispatch('forceLeaveWithoutHousehold');
this.$emit('readyToGo');
},
resetMode() {
this.$store.commit('resetMode');
@ -207,10 +164,14 @@ export default {
},
selectHousehold(h) {
this.$store.dispatch('selectHousehold', h);
this.$emit('readyToGo');
},
removeHouseholdAddress() {
this.$store.commit('removeHouseholdAddress');
},
toggleHouseholdSuggestion() {
this.$store.commit('toggleHouseholdSuggestion');
},
},
};
@ -218,6 +179,18 @@ export default {
<style lang="scss">
.filtered {
filter: grayscale(1) opacity(0.6);
}
.filteredButActive {
filter: grayscale(1) opacity(0.6);
&:hover {
filter: unset;
}
}
div#household_members_editor div,
div.householdSuggestionList {
&.flex-table {

View File

@ -0,0 +1,88 @@
<template>
<current-household></current-household>
<ul class="record_actions">
<li v-if="!hasHouseholdAddress && !isHouseholdForceAddress">
<button class="btn" @click="markNoAddress">
{{ $t('household_members_editor.household_address.mark_no_address') }}
</button>
</li>
<li v-if="!hasHouseholdAddress">
<add-address
:context="getAddressContext"
:key="addAddress.key"
:options="addAddress.options"
:addressChangedCallback="addressChanged"
></add-address>
</li>
<li v-if="hasHouseholdAddress">
<button class="btn btn-remove"
@click="removeHouseholdAddress">
{{ $t('household_members_editor.household_address.remove_address') }}
</button>
</li>
</ul>
</template>
<script>
import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress.vue';
import CurrentHousehold from './CurrentHousehold';
import { mapGetters } from 'vuex';
export default {
name: "HouseholdAddress.vue",
components: {
CurrentHousehold,
AddAddress,
},
data() {
return {
addAddress: {
key: 'household_new',
options: {
useDate: {
validFrom: false,
validTo: false,
},
onlyButton: true,
button: {
text: {
create: 'household_members_editor.household_address.set_address',
edit: 'household_members_editor.household_address.update_address',
}
},
title: {
create: 'household_members_editor.household_address.create_new_address',
edit: 'household_members_editor.household_address.update_address_title',
},
}
}
}
},
computed: {
...mapGetters([
'isHouseholdNew',
'hasHouseholdAddress',
'getAddressContext',
'isHouseholdForceNoAddress'
])
},
methods: {
addressChanged(payload) {
console.log("addressChanged", payload);
this.$store.dispatch('setHouseholdNewAddress', payload.address);
},
markNoAddress() {
this.$store.commit('markHouseholdNoAddress');
},
removeHouseholdAddress() {
this.$store.commit('removeHouseholdAddress');
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,106 @@
<template>
<current-household></current-household>
<h2>{{ $t('household_members_editor.positioning.persons_to_positionnate')}}</h2>
<div class="list-household-members">
<div
v-for="conc in concerned"
class="item-bloc"
v-bind:key="conc.person.id"
>
<div class="pick-position">
<div class="person">
<person-render-box render="badge" :options="{}" :person="conc.person"></person-render-box>
</div>
<div class="holder">
<button
class="btn"
:disabled="!allowHolderForConcerned(conc)"
:class="{'btn-outline-chill-green': !conc.holder, 'btn-chill-green': conc.holder }"
@click="toggleHolder(conc)"
>
{{ $t('household_members_editor.positioning.holder') }}
</button>
</div>
<div
v-for="position in positions"
class="position"
>
<button
class="btn"
:class="{ 'btn-primary': conc.position === position, 'btn-outline-primary': conc.position !== position }"
@click="moveToPosition(conc.person.id, position.id)"
>
{{ position.label.fr }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import MemberDetails from './MemberDetails.vue';
import {mapGetters, mapState} from "vuex";
import CurrentHousehold from "./CurrentHousehold";
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
export default {
name: "Positioning",
components: {
CurrentHousehold,
PersonRenderBox,
},
computed: {
...mapState([
'concerned'
]),
...mapGetters([
'persons',
'concUnpositionned',
'positions',
'concByPosition',
]),
allPersonsPositionnated () {
return this.$store.getters.persons.length > 0
&& this.$store.getters.concUnpositionned.length === 0;
},
allowHolderForConcerned: (app) => (conc) => {
console.log('allow holder for concerned', conc);
if (conc.position === null) {
return false;
}
return conc.position.allowHolder;
}
},
methods: {
moveToPosition(person_id, position_id) {
this.$store.dispatch('markPosition', { person_id, position_id });
},
toggleHolder(conc) {
console.log('toggle holder', conc);
this.$store.dispatch('toggleHolder', conc);
}
},
}
</script>
<style lang="scss" scoped>
.pick-position {
margin: 0;
padding: 0;
display: flex;
justify-content: flex-end;
align-items: center;
.person {
margin-right: auto;
}
.holder {
margin-right: 1.2rem;
}
}
</style>

View File

@ -5,65 +5,80 @@ const appMessages = {
fr: {
household_members_editor: {
household: {
no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.",
new_household: "Nouveau ménage",
no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage.",
// new_household: "Nouveau ménage",
create_household: "Créer",
search_household: "Chercher un ménage",
will_leave_any_household: "Les usagers ne rejoignent pas de ménage",
leave: "Quitter",
leave: "Quitter sans rejoindre un ménage",
will_leave_any_household_explanation: "Les usagers quitteront leur ménage actuel, et ne seront pas associés à un autre ménage. Par ailleurs, ils seront enregistrés comme étant sans adresse connue.",
leave_without_household: "Sans nouveau ménage",
set_address: "Indiquer une adresse",
reset_mode: "Modifier la destination",
remove_address: "Supprimer l'adresse",
update_address: "Mettre à jour l'adresse",
household_suggested: "Suggestions de ménage",
household_suggested_explanation: "Les ménages suivants sont connus et pourraient peut-être correspondre à des ménages recherchés."
// remove ?
/*
where_live_the_household: "À quelle adresse habite ce ménage ?",
household_live_to_this_address: "Sélectionner l'adresse",
no_suggestions: "Aucune adresse à suggérer",
delete_this_address: "Supprimer cette adresse",
create_new_address: "Créer une nouvelle adresse",
or_create_new_address: "Ou créer une nouvelle adresse",
*/
// end remove ?
},
household_address: {
mark_no_address: "Ne pas indiquer d'adresse",
remove_address: "Supprimer l'adresse",
update_address: "Mettre à jour l'adresse",
set_address: "Indiquer une adresse",
create_new_address: "Créer une nouvelle adresse",
},
concerned: {
title: "Nouveaux membres du ménage",
title: "Usagers déplacés",
persons_will_be_moved: "Les usagers suivants vont être déplacés",
add_at_least_onePerson: "Indiquez au moins un usager à déplacer",
remove_concerned: "Ne plus transférer",
// old ?
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",
no_person_in_position: "Aucun usager ne sera ajouté à cette position",
},
positioning: {
persons_to_positionnate: 'Usagers à positionner',
holder: "Titulaire",
},
app: {
next: 'Suivant',
cancel: 'Annuler',
save: 'Enregistrer',
steps: {
concerned: 'Usagers concernés',
household: 'Ménage de destination',
household_address: 'Adresse du nouveau ménage',
positioning: 'Position dans le ménage',
confirm: 'Confirmation'
}
},
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: "Est titulaire",
is_not_holder: "N'est pas titulaire",
remove_position: "Retirer des {position}",
remove_concerned: "Ne plus transférer",
household_part: "Destination",
suggestions: "Suggestions",
hide_household_suggestion: "Masquer les suggestions",
show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} suggestions',
household_for_participants_accompanying_period: "Des ménages partagent le même parcours",
select_household: "Sélectionner le ménage",
dates_title: "Période de validité",
dates: {
start_date: "Début de validité",
end_date: "Fin de validité",
dates_title: "Période 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",
}
}
};

View File

@ -1,5 +1,6 @@
import { createStore } from 'vuex';
import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod, fetchAddressSuggestionByPerson} from './../api.js';
import { fetchHouseholdByAddressReference } from 'ChillPersonAssets/lib/household.js';
import { datetimeToISO } from 'ChillMainAssets/chill/js/date.js';
const debug = process.env.NODE_ENV !== 'production';
@ -42,7 +43,16 @@ const store = createStore({
allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch,
allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold,
forceLeaveWithoutHousehold: false,
householdSuggestionByAccompanyingPeriod: [],
/**
* If true, the user explicitly said that no address is possible
*/
forceHouseholdNoAddress: false,
/**
* Household suggestions
*
* (this is not restricted to "suggestion by accompanying periods")
*/
householdSuggestionByAccompanyingPeriod: [], // TODO rename into householdsSuggestion
showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1,
addressesSuggestion: [],
showAddressSuggestion: true,
@ -74,10 +84,12 @@ const store = createStore({
isModeLeave(state) {
return state.mode === "leave";
},
isHouseholdForceNoAddress(state) {
return state.forceHouseholdNoAddress;
},
getSuggestions(state) {
let suggestions = [];
state.householdSuggestionByAccompanyingPeriod.forEach(h => {
console.log(h);
suggestions.push({household: h});
});
@ -85,15 +97,12 @@ const store = createStore({
},
isHouseholdNew(state) {
return state.mode === "new";
/*
if (state.household === null) {
return false;
}
return !Number.isInteger(state.household.id);
*/
},
getAddressContext(state, getters) {
if (state.household === null) {
return {};
}
if (!getters.hasHouseholdAddress) {
return {
edit: false,
@ -198,6 +207,40 @@ const store = createStore({
needsPositionning(state) {
return state.forceLeaveWithoutHousehold === false;
},
fakeHouseholdWithConcerned(state, getters) {
if (null === state.household) {
throw Error('cannot create fake household without household');
}
let h = {
type: 'household',
members: state.household.members,
current_address: state.household.current_address,
current_members_id: state.household.current_members_id,
new_members: [],
};
if (!getters.isHouseholdNew){
h.id = state.household.id;
}
state.concerned.forEach((c, index) => {
let m = {
id: index * -1,
person: c.person,
holder: c.holder,
position: c.position,
};
if (c.position === null) {
m.position = {
ordering: 999999
}
}
h.new_members.push(m);
})
console.log('fake household', h);
return h;
},
buildPayload: (state, getters) => {
let
conc,
@ -272,6 +315,10 @@ const store = createStore({
position = state.positions.find(pos => pos.id === position_id),
conc = state.concerned.find(c => c.person.id === person_id);
conc.position = position;
// reset position if changed:
if (!position.allowHolder && conc.holder) {
conc.holder = false;
}
},
setComment(state, {conc, comment}) {
conc.comment = comment;
@ -283,9 +330,9 @@ const store = createStore({
conc.holder = false;
conc.position = null;
},
removeConcerned(state, conc) {
removePerson(state, person) {
state.concerned = state.concerned.filter(c =>
c.person.id !== conc.person.id
c.person.id !== person.id
)
},
createHousehold(state) {
@ -310,6 +357,7 @@ const store = createStore({
}
state.household.current_address = address;
state.forceHouseholdNoAddress = false;
},
removeHouseholdAddress(state, address) {
if (null === state.household) {
@ -319,6 +367,9 @@ const store = createStore({
state.household.current_address = null;
},
markHouseholdNoAddress(state) {
state.forceHouseholdNoAddress = true;
},
forceLeaveWithoutHousehold(state) {
state.household = null;
state.mode = "leave";
@ -329,7 +380,7 @@ const store = createStore({
state.mode = "existing";
state.forceLeaveWithoutHousehold = false;
},
setHouseholdSuggestionByAccompanyingPeriod(state, households) {
addHouseholdSuggestionByAccompanyingPeriod(state, households) {
let existingIds = state.householdSuggestionByAccompanyingPeriod
.map(h => h.id);
for (let i in households) {
@ -384,8 +435,8 @@ const store = createStore({
commit('removePosition', conc);
dispatch('computeWarnings');
},
removeConcerned({ commit, dispatch }, conc) {
commit('removeConcerned', conc);
removePerson({ commit, dispatch }, person) {
commit('removePerson', person);
dispatch('computeWarnings');
dispatch('fetchAddressSuggestions');
},
@ -418,20 +469,33 @@ const store = createStore({
fetchHouseholdSuggestionForConcerned({ commit, state }, person) {
fetchHouseholdSuggestionByAccompanyingPeriod(person.id)
.then(households => {
commit('setHouseholdSuggestionByAccompanyingPeriod', households);
commit('addHouseholdSuggestionByAccompanyingPeriod', households);
});
},
fetchAddressSuggestions({ commit, state }) {
fetchAddressSuggestions({ commit, state, dispatch }) {
for (let i in state.concerned) {
fetchAddressSuggestionByPerson(state.concerned[i].person.id)
.then(addresses => {
commit('addAddressesSuggestion', addresses);
dispatch('fetchHouseholdSuggestionByAddresses', addresses);
})
.catch(e => {
console.log(e);
});
}
},
async fetchHouseholdSuggestionByAddresses({commit}, addresses) {
console.log('fetchHouseholdSuggestionByAddresses', addresses);
// foreach address, find household suggestions
addresses.forEach(async a => {
if (a.addressReference !== null) {
let households = await fetchHouseholdByAddressReference(a.addressReference);
commit('addHouseholdSuggestionByAccompanyingPeriod', households);
} else {
console.log('not an adresse reference')
}
});
},
computeWarnings({ commit, state, getters }) {
let warnings = [],
payload;

View File

@ -19,15 +19,18 @@
<!-- member part -->
<li v-if="hasCurrentMembers" class="members" :title="$t('current_members')">
<template v-for="m in currentMembers()" :key="m.id">
<span v-for="m in currentMembers()" :key="m.id" class="m" :class="{ is_new: m.is_new === true}">
<person-render-box render="badge"
:person="m.person"
:options="{
isHolder: m.holder,
addLink: true
}">
<template v-slot:post-badge v-if="m.is_new === true">
<span class="post-badge is_new"><i class="fa fa-sign-in"></i></span>
</template>
</person-render-box>
</template>
</span>
</li>
<li v-else class="members" :title="$t('current_members')">
<p class="chill-no-data-statement">{{ $t('no_members_yet') }}</p>
@ -82,7 +85,7 @@ export default {
return this.household.current_members_id.length > 0;
},
currentMembers() {
return this.household.members.filter(m => this.household.current_members_id.includes(m.id))
let members = this.household.members.filter(m => this.household.current_members_id.includes(m.id))
.sort((a, b) => {
if (a.position.ordering < b.position.ordering) {
return -1;
@ -98,6 +101,17 @@ export default {
}
return 0;
});
if (this.household.new_members !== undefined) {
this.household.new_members.map(m => {
m.is_new = true;
return m;
}).forEach(m => {
members.push(m);
});
}
return members;
},
currentMembersLength() {
return this.household.current_members_id.length;
@ -121,6 +135,13 @@ section.chill-entity {
content: '';
}
.members {
.post-badge.is_new {
margin-left: 0.5rem;
color: var(--bs-chill-green);
}
}
}
}
</style>

View File

@ -126,6 +126,7 @@
</span>
{{ person.text }}
</span>
<slot name="post-badge"></slot>
</span>
</template>

View File

@ -94,7 +94,7 @@
</div>
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-save">
<button type="submit" class="btn btn-save" id="form_household_comment_confirm">
{{ 'Save'|trans }}
</button>
</li>

View File

@ -0,0 +1,8 @@
<?php
namespace Chill\PersonBundle\Security\Authorization;
class HouseholdVoter
{
const SHOW = PersonVoter::SEE;
}

View File

@ -2,9 +2,14 @@
namespace Chill\PersonBundle\Tests\Controller;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Entity\Center;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
@ -15,6 +20,8 @@ class HouseholdApiControllerTest extends WebTestCase
use PrepareClientTrait;
private array $toDelete = [];
/**
* @dataProvider generatePersonId
*/
@ -45,6 +52,77 @@ class HouseholdApiControllerTest extends WebTestCase
$this->assertResponseIsSuccessful();
}
/**
* @dataProvider generateHouseholdAssociatedWithAddressReference
*/
public function testFindHouseholdByAddressReference(int $addressReferenceId, int $expectedHouseholdId)
{
$client = $this->getClientAuthenticated();
$client->request(
Request::METHOD_GET,
"/api/1.0/person/household/by-address-reference/$addressReferenceId.json"
);
$this->assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('count', $data);
$this->assertArrayHasKey('results', $data);
$householdIds = \array_map(function($r) {
return $r['id'];
}, $data['results']);
$this->assertContains($expectedHouseholdId, $householdIds);
}
public function generateHouseholdAssociatedWithAddressReference()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$centerA = $em->getRepository(Center::class)->findOneBy(['name' => 'Center A']);
$nbReference = $em->createQueryBuilder()->select('count(ar)')->from(AddressReference::class, 'ar')
->getQuery()->getSingleScalarResult();
$reference = $em->createQueryBuilder()->select('ar')->from(AddressReference::class, 'ar')
->setFirstResult(\random_int(0, $nbReference))
->setMaxResults(1)
->getQuery()->getSingleResult();
$p = new Person();
$p->setFirstname('test')->setLastName('test lastname')
->setGender(Person::BOTH_GENDER)
->setCenter($centerA)
;
$em->persist($p);
$h = new Household();
$h->addMember($m = (new HouseholdMember())->setPerson($p));
$h->addAddress(Address::createFromAddressReference($reference)->setValidFrom(new \DateTime('today')));
$em->persist($m);
$em->persist($h);
$em->flush();
$this->toDelete = $this->toDelete + [
[HouseholdMember::class, $m->getId()],
[User::class, $p->getId()],
[Household::class, $h->getId()]
];
yield [$reference->getId(), $h->getId()];
}
protected function tearDown()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
foreach ($this->toDelete as list($class, $id)) {
$obj = $em->getRepository($class)->find($id);
$em->remove($obj);
}
$em->flush();
}
public function generatePersonId()
{
self::bootKernel();
@ -64,7 +142,7 @@ class HouseholdApiControllerTest extends WebTestCase
;
$person = $period->getParticipations()
->first()->getPerson();
->first()->getPerson();
yield [ $person->getId() ];
}

View File

@ -18,7 +18,7 @@ class HouseholdControllerTest extends WebTestCase
protected function setUp()
{
$this->client = $this->getClientAuthenticated();
}
}
/**
* @dataProvider generateValidHouseholdIds
@ -49,7 +49,7 @@ class HouseholdControllerTest extends WebTestCase
$this->assertResponseIsSuccessful();
$form = $crawler->selectButton('Enregistrer')
$form = $crawler->filter('#form_household_comment_confirm')
->form();
$form['household[commentMembers][comment]'] = "This is a text **generated** by automatic tests";
@ -109,8 +109,8 @@ class HouseholdControllerTest extends WebTestCase
\shuffle($ids);
yield [ \array_pop($ids)['id'] ];
yield [ \array_pop($ids)['id'] ];
yield [ \array_pop($ids)['id'] ];
yield [ \array_pop($ids)['id'] ];
yield [ \array_pop($ids)['id'] ];
yield [ \array_pop($ids)['id'] ];
}
}

View File

@ -1127,6 +1127,32 @@ paths:
401:
description: "Unauthorized"
/1.0/person/household/by-address-reference/{address_id}.json:
get:
tags:
- household
summary: Return a list of household which are sharing the same address reference
parameters:
- name: address_id
in: path
required: true
description: the address reference id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: "#/components/schemas/Household"
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/person/household/suggest/by-person/{person_id}/through-accompanying-period-participation.json:
get:
tags:

View File

@ -10,3 +10,5 @@ services:
Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\PersonACLAwareRepository'
Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository'
Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepository'