Merge remote-tracking branch 'origin/master' into features/household-editor

This commit is contained in:
2021-06-07 17:32:29 +02:00
64 changed files with 1626 additions and 460 deletions

View File

@@ -0,0 +1,88 @@
<?php
namespace Chill\PersonBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Chill\PersonBundle\Entity\Household\Household;
/**
* @Route("/{_locale}/person/household")
*/
class HouseholdController extends AbstractController
{
/**
* @Route(
* "/{household_id}/summary",
* name="chill_person_household_summary",
* methods={"GET", "HEAD"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function summary(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/summary.html.twig',
[
'household' => $household
]
);
}
/**
* @Route(
* "/{household_id}/members",
* name="chill_person_household_members",
* methods={"GET", "HEAD"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function members(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/members.html.twig',
[
'household' => $household
]
);
}
/**
* @Route(
* "/{household_id}/addresses",
* name="chill_person_household_addresses",
* methods={"GET", "HEAD"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function addresses(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/addresses.html.twig',
[
'household' => $household
]
);
}
/**
* @Route(
* "/{household_id}/address/move",
* name="chill_person_household_address_move",
* methods={"GET", "HEAD", "POST"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function addressMove(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/address_move.html.twig',
[
'household' => $household
]
);
}
}

View File

@@ -13,6 +13,7 @@ use Symfony\Component\Serializer\Exception;
use Symfony\Component\Routing\Annotation\Route;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\PersonBundle\Household\MembersEditor;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class HouseholdMemberController extends ApiController
{
@@ -37,15 +38,10 @@ class HouseholdMemberController extends ApiController
//
$em = $this->getDoctrine()->getManager();
// to ensure closing membership before creating one, we must manually open a transaction
$em->beginTransaction();
foreach ($editor->getPersistable() as $el) {
$em->persist($el);
}
$em->flush();
$em->commit();
return $this->json($editor->getHousehold(), Response::HTTP_OK, ["groups" => ["read"]]);
}

View File

@@ -72,7 +72,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$loader->load('services/command.yaml');
$loader->load('services/actions.yaml');
$loader->load('services/form.yaml');
$loader->load('services/templating.yaml');
$loader->load('services/alt_names.yaml');
$loader->load('services/household.yaml');
// We can get rid of this file when the service 'chill.person.repository.person' is no more used.

View File

@@ -2,10 +2,12 @@
namespace Chill\PersonBundle\Entity\Household;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\Serializer\Annotation as Serializer;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
/**
* @ORM\Entity
@@ -24,16 +26,10 @@ class Household
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private $id;
public function getId(): ?int
{
return $this->id;
}
private ?int $id = null;
/**
* Addresses
* @var Collection
*
* @ORM\ManyToMany(
* targetEntity="Chill\MainBundle\Entity\Address",
@@ -41,8 +37,27 @@ class Household
* @ORM\JoinTable(name="chill_person_household_to_addresses")
* @ORM\OrderBy({"validFrom" = "DESC"})
*/
private $addresses;
private Collection $addresses;
/**
* @ORM\OneToMany(
* targetEntity=HouseholdMember::class,
* mappedBy="household"
* )
* @Serializer\Groups({"read"})
*/
private Collection $members;
public function __construct()
{
$this->addresses = new ArrayCollection();
$this->members = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
/**
* @param Address $address
@@ -74,4 +89,34 @@ class Household
return $this->addresses;
}
/**
* @return Collection|HouseholdMember[]
*/
public function getMembers(): Collection
{
return $this->members;
}
public function addMember(HouseholdMember $member): self
{
if (!$this->members->contains($member)) {
$this->members[] = $member;
$member->setHousehold($this);
}
return $this;
}
public function removeMember(HouseholdMember $member): self
{
if ($this->members->removeElement($member)) {
// set the owning side to null (unless already changed)
if ($member->getHousehold() === $this) {
$member->setHousehold(null);
}
}
return $this;
}
}

View File

@@ -6,6 +6,7 @@ use Doctrine\ORM\Mapping as ORM;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
@@ -20,26 +21,31 @@ class HouseholdMember
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=Position::class)
* @Serializer\Groups({"read"})
*/
private ?Position $position = null;
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Serializer\Groups({"read"})
*/
private ?\DateTimeImmutable $startDate = null;
/**
* @ORM\Column(type="date_immutable", nullable= true, options={"default": null})
* @Serializer\Groups({"read"})
*/
private ?\DateTimeImmutable $endDate = null;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @Serializer\Groups({"read"})
*/
private ?string $comment = NULL;
@@ -50,6 +56,7 @@ class HouseholdMember
/**
* @ORM\Column(type="boolean", options={"default": false})
* @Serializer\Groups({"read"})
*/
private bool $holder = false;
@@ -59,6 +66,7 @@ class HouseholdMember
* @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Person"
* )
* @Serializer\Groups({"read"})
*/
private ?Person $person = null;
@@ -131,6 +139,9 @@ class HouseholdMember
return $this;
}
/**
* @Serializer\Groups({"read"})
*/
public function getShareHousehold(): ?bool
{
return $this->sharedHousehold;

View File

@@ -0,0 +1,56 @@
<?php
namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\Household\Household;
use Knp\Menu\MenuItem;
use Symfony\Contracts\Translation\TranslatorInterface;
class HouseholdMenuBuilder implements LocalMenuBuilderInterface
{
/**
* @var TranslatorInterface
*/
protected $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public static function getMenuIds(): array
{
return [ 'household' ];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{
$household = $parameters['household'];
$menu->addChild($this->translator->trans('Summary'), [
'route' => 'chill_person_household_summary',
'routeParameters' => [
'household_id' => $household->getId()
]])
->setExtras(['order' => 10]);
$menu->addChild($this->translator->trans('Members'), [
'route' => 'chill_person_household_members',
'routeParameters' => [
'household_id' => $household->getId()
]])
->setExtras(['order' => 20]);
$menu->addChild($this->translator->trans('Addresses'), [
'route' => 'chill_person_household_addresses',
'routeParameters' => [
'household_id' => $household->getId()
]])
->setExtras(['order' => 30]);
}
}

View File

@@ -9,8 +9,7 @@
<table class="rounded" v-if="participations.length > 0">
<thead>
<tr>
<th class="chill-orange">{{ $t('persons_associated.firstname') }}</th>
<th class="chill-orange">{{ $t('persons_associated.lastname') }}</th>
<th class="chill-orange">{{ $t('persons_associated.name') }}</th>
<th class="chill-orange">{{ $t('persons_associated.startdate') }}</th>
<th class="chill-orange">{{ $t('persons_associated.enddate') }}</th>
<th class="chill-orange">{{ $t('action.actions') }}</th>

View File

@@ -1,7 +1,9 @@
<template>
<tr>
<td>{{ participation.person.firstName }}</td>
<td>{{ participation.person.lastName }}</td>
<td>
{{ participation.person.firstName }}
{{ participation.person.lastName }}
</td>
<td><span v-if="participation.startDate">
{{ $d(participation.startDate.datetime, 'short') }}</span>
</td>
@@ -11,16 +13,18 @@
<td>
<ul class="record_actions">
<li>
<a class="sc-button bt-show" target="_blank"
:href="url.show"
:title="$t('action.show')">
</a>
<on-the-fly
v-bind:type="participation.person.type"
v-bind:id="participation.person.id"
action="show">
</on-the-fly>
</li>
<li>
<a class="sc-button bt-update" target="_blank"
:href="url.edit"
:title="$t('action.edit')">
</a>
<on-the-fly
v-bind:type="participation.person.type"
v-bind:id="participation.person.id"
action="edit">
</on-the-fly>
</li>
<!--li>
<button class="sc-button bt-delete"
@@ -31,7 +35,7 @@
<li>
<button v-if="!participation.endDate"
class="sc-button bt-remove"
:title="$t('action.remove')"
v-bind:title="$t('action.remove')"
@click.prevent="$emit('close', participation)">
</button>
<button v-else class="sc-button bt-remove disabled"></button>
@@ -42,17 +46,14 @@
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'PersonItem',
props: ['participation'],
data() {
return {
url: {
show: '/fr/person/' + this.participation.person.id + '/general',
edit: '/fr/person/' + this.participation.person.id + '/general/edit'
}
}
components: {
OnTheFly
},
emits: ['remove', 'close']
props: ['participation'],
emits: ['remove', 'close'],
}
</script>

View File

@@ -41,7 +41,18 @@
<ul class="record_actions">
<li>
<a class="sc-button bt-show" :title="$t('action.show')" target="_blank" :href="url.show"></a>
<on-the-fly
v-bind:type="accompanyingCourse.requestor.type"
v-bind:id="accompanyingCourse.requestor.id"
action="show">
</on-the-fly>
</li>
<li>
<on-the-fly
v-bind:type="accompanyingCourse.requestor.type"
v-bind:id="accompanyingCourse.requestor.id"
action="edit">
</on-the-fly>
</li>
</ul>
</div>
@@ -76,12 +87,14 @@
</template>
<script>
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'Requestor',
components: {
AddPersons,
OnTheFly
},
data() {
return {
@@ -107,13 +120,6 @@ export default {
get() {
return this.$store.state.accompanyingCourse.requestorAnonymous;
}
},
url() {
return (this.accompanyingCourse.requestor.type === 'person') ? {
show: `/fr/person/${this.accompanyingCourse.requestor.id}/general`,
} : {
show: `/fr/thirdparty/thirdparty/${this.accompanyingCourse.requestor.id}/show`,
}
}
},
methods: {

View File

@@ -21,24 +21,25 @@
<td>
<ul class="record_actions">
<li>
<a class="sc-button bt-show" target="_blank"
:href="url.show"
:title="$t('action.show')">
</a>
<on-the-fly
v-bind:type="resource.resource.type"
v-bind:id="resource.resource.id"
action="show">
</on-the-fly>
</li>
<li>
<a class="sc-button bt-update" target="_blank"
:href="url.edit"
:title="$t('action.edit')">
</a>
<on-the-fly
v-bind:type="resource.resource.type"
v-bind:id="resource.resource.id"
action="edit">
</on-the-fly>
</li>
<li>
<button
class="sc-button bt-remove"
:title="$t('action.remove')"
v-bind:title="$t('action.remove')"
@click.prevent="$emit('remove', resource)">
</button>
</li>
</ul>
</td>
@@ -46,23 +47,14 @@
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'ResourceItem',
components: {
OnTheFly
},
props: ['resource'],
emits: ['remove'],
computed: {
type() {
return this.resource.resource.type;
},
url() {
return (this.type === 'person') ? {
show: `/fr/person/${this.resource.resource.id}/general`,
edit: `/fr/person/${this.resource.resource.id}/general/edit`
} : {
show: `/fr/thirdparty/thirdparty/${this.resource.resource.id}/show`,
edit: `/fr/thirdparty/thirdparty/${this.resource.resource.id}/update`
}
}
}
emits: ['remove']
}
</script>

View File

@@ -31,6 +31,7 @@ const appMessages = {
counter: "Il n'y a pas encore d'usager | 1 usager | {count} usagers",
firstname: "Prénom",
lastname: "Nom",
name: "Nom",
startdate: "Date d'entrée",
enddate: "Date de sortie",
add_persons: "Ajouter des usagers",
@@ -73,7 +74,7 @@ const appMessages = {
comment: {
title: "Observations",
label: "Ajout d'une note",
content: "Rédigez une première note...",
content: "Rédigez une première note",
created_by: "créé par {0}, le {1}"
},
confirm: {

View File

@@ -0,0 +1,34 @@
/*
* GET a person by id
*/
const getPerson = (id) => {
const url = `/api/1.0/person/person/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* POST a new person
*/
const postPerson = (body) => {
const url = `/api/1.0/person/person.json`;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
getPerson,
postPerson
};

View File

@@ -65,9 +65,14 @@
@updateSelected="updateSelected">
</person-suggestion>
<button v-if="query.length >= 3" class="sc-button bt-create ml-5 mt-2" name="createPerson">
{{ $t('action.create') }} "{{ query }}"
</button>
<div class="create-button">
<on-the-fly
v-if="query.length >= 3"
v-bind:buttonText="$t('onthefly.create.button', {q: query})"
action="create"><!-- TODO first close this modal -->
</on-the-fly>
</div>
</div>
</template>
@@ -84,6 +89,7 @@
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import PersonSuggestion from './AddPersons/PersonSuggestion';
import { searchPersons, searchPersons_2 } from 'ChillPersonAssets/vuejs/_api/AddPersons';
@@ -92,6 +98,7 @@ export default {
components: {
Modal,
PersonSuggestion,
OnTheFly
},
props: [
'buttonTitle',
@@ -170,7 +177,7 @@ export default {
if (query.length >= 3) {
searchPersons_2({ query, options: this.options })
.then(suggested => new Promise((resolve, reject) => {
console.log('suggested', suggested);
//console.log('suggested', suggested);
this.loadSuggestions(suggested.results);
resolve();
}));
@@ -179,14 +186,14 @@ export default {
}
},
loadSuggestions(suggested) {
console.log('suggested', suggested);
//console.log('suggested', suggested);
this.search.suggested = suggested;
this.search.suggested.forEach(function(item) {
item.key = this.itemKey(item);
}, this);
},
updateSelected(value) {
console.log('value', value);
//console.log('value', value);
this.search.selected = value;
},
resetSearch() {
@@ -251,4 +258,8 @@ export default {
}
}
}
.create-button > a {
margin-top: 0.5em;
margin-left: 2.6em;
}
</style>

View File

@@ -42,7 +42,7 @@ export default {
computed: {
selected: {
set(value) {
console.log('value', value);
//console.log('value', value);
this.$emit('updateSelected', value);
},
get() {

View File

@@ -14,22 +14,23 @@
<span class="badge badge-pill badge-secondary" :title="item.key">
{{ $t('item.type_person') }}
</span>
<a class="sc-button bt-show" target="_blank" :title="item.key" :href="url.show"></a>
<on-the-fly
type="person"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'SuggestionPerson',
props: ['item'],
data() {
return {
url: {
show: '/fr/person/' + this.item.result.person_id + '/general',
edit: '/fr/person/' + this.item.result.person_id + '/general/edit'
},
}
components: {
OnTheFly
},
props: ['item']
}
</script>

View File

@@ -15,22 +15,23 @@
<span class="badge badge-pill badge-secondary" :title="item.key">
{{ $t('item.type_thirdparty') }}
</span>
<a class="sc-button bt-show" target="_blank" :title="item.key" :href="url.show"></a>
<on-the-fly
type="thirdparty"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default {
name: 'SuggestionThirdParty',
props: ['item'],
data() {
return {
url: {
show: '/fr/thirdparty/thirdparty/' + this.item.result.thirdparty_id + '/show',
edit: '/fr/thirdparty/thirdparty/' + this.item.result.thirdparty_id + '/edit'
},
}
components: {
OnTheFly
},
props: ['item']
}
</script>

View File

@@ -0,0 +1,200 @@
<template>
<div v-if="action === 'show'">
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
<h3 :title="person.id">{{ person.text }}</h3>
<p>
<i class="fa fa-fw"
:class="genderClass">
<!--
:title="$t(genderTranslation)"
-->
</i>
<span v-if="person.birthdate">
{{ $t('person.born', { e: feminized }) }}
{{ $d(person.birthdate.datetime, 'short') }}
</span>
</p>
</div>
<div class="item-col">
<dl class="list-content">
<dt>{{ $t('person.firstname') }}</dt>
<dd>{{ person.firstName }}</dd>
<dt>{{ $t('person.lastname') }}</dt>
<dd>{{ person.lastName }}</dd>
<dt>{{ $t('person.altnames') }}</dt>
<dd>{{ person.altNames }}</dd>
<span v-if="person.center">
<dt>{{ $t('person.center_name') }}</dt>
<dd :title="person.center.id">{{ person.center.name }}</dd>
</span>
<dt>{{ $t('person.phonenumber') }}</dt>
<dd>{{ person.phonenumber }}</dd>
<dt>{{ $t('person.mobilenumber') }}</dt>
<dd>{{ person.mobilenumber }}</dd>
<dt>{{ $t('person.gender.title') }}</dt>
<!--
<dd>{{ $t(genderTranslation) }}</dd>
-->
</dl>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="action === 'edit' || action === 'create'">
<input v-model="firstName" :placeholder="$t('person.firstname')" />
<input v-model="lastName" :placeholder="$t('person.lastname')" />
<!-- TODO fix placeholder if undefined
TODO dynamically get gender options
-->
<select v-model="gender">
<option disabled value="">{{ $t('person.gender.placeholder') }}</option>
<option value="woman">{{ $t('person.gender.woman') }}</option>
<option value="man">{{ $t('person.gender.man') }}</option>
<option value="neuter">{{ $t('person.gender.neuter') }}</option>
</select>
<i class="fa fa-birthday-cake"></i>
<input type="date"
id="chill_personbundle_person_birthdate"
name="chill_personbundle_person[birthdate]"
v-model="birthDate"
/>
<i class="fa fa-phone">
</i><input v-model="phonenumber" :placeholder="$t('person.phonenumber')" />
<i class="fa fa-mobile">
</i><input v-model="mobilenumber" :placeholder="$t('person.mobilenumber')" />
</div>
</template>
<script>
import { getPerson, postPerson } from '../../_api/OnTheFly';
export default {
name: "OnTheFlyPerson",
props: ['id', 'type', 'action'],
data() {
return {
person: {
type: 'person'
}
}
},
computed: {
firstName: {
set(value) { this.person.firstName = value; },
get() { return this.person.firstName; }
},
lastName: {
set(value) { this.person.lastName = value; },
get() { return this.person.lastName; }
},
gender: {
set(value) { this.person.gender = value; },
get() { return this.person.gender; }
},
birthDate: {
set(value) {
if (this.person.birthdate) {
this.person.birthdate.datetime = value + "T00:00:00+0100";
} else {
this.person.birthdate = { datetime: value + "T00:00:00+0100"};
}
},
get() {
return (this.person.birthdate) ? this.person.birthdate.datetime.split('T')[0] : '';
}
},
phonenumber: {
set(value) { this.person.phonenumber = value; },
get() { return this.person.phonenumber; }
},
mobilenumber: {
set(value) { this.person.mobilenumber = value; },
get() { return this.person.mobilenumber; }
},
genderClass() {
switch (this.person.gender) {
case 'woman':
return 'fa-venus';
case 'man':
return 'fa-mars';
case 'neuter':
return 'fa-neuter';
}
},
genderTranslation() {
switch (this.person.gender) {
case 'woman':
return 'person.gender.woman';
case 'man':
return 'person.gender.man';
case 'neuter':
return 'person.gender.neuter';
}
},
feminized() {
return (this.person.gender === 'woman')? 'e' : '';
}
},
mounted() {
if (this.action !== 'create') {
this.loadData();
}
},
methods: {
loadData() {
getPerson(this.id)
.then(person => new Promise((resolve, reject) => {
this.person = person;
console.log('get person', this.person);
resolve();
}));
},
postData() {
postPerson(this.person)
.then(person => new Promise((resolve, reject) => {
this.person = person;
console.log('post person', person);
resolve();
}));
}
}
}
</script>
<style lang="scss" scoped>
ul {
li::marker {
}
}
div.flex-table {
div.item-bloc {
div.item-row {
div.item-col:last-child {
justify-content: flex-start;
}
}
}
}
dl {
dd {
margin-left: 1em;
}
}
</style>

View File

@@ -15,7 +15,21 @@ const personMessages = {
person: {
firstname: "Prénom",
lastname: "Nom",
born: "né le ",
born: "né{e} le ",
center_id: "Identifiant du centre",
center_type: "Type de centre",
center_name: "Territoire", // vendée
phonenumber: "Téléphone",
mobilenumber: "Mobile",
altnames: "Autres noms",
gender: {
title: "Genre",
placeholder: "Choisissez le genre de l'usager",
woman: "Femme",
man: "Homme",
neuter: "Neutre",
}
},
error_only_one_person: "Une seule personne peut être sélectionnée !"
}

View File

@@ -31,7 +31,7 @@
{% set gender = (p.person.gender == 'woman') ? 'fa-venus' :
(p.person.gender == 'man') ? 'fa-mars' : 'fa-neuter' %}
{% set genderTitle = (p.person.gender == 'woman') ? 'femme' :
(p.person.gender == 'homme') ? 'fa-mars' : 'neutre' %}
(p.person.gender == 'man') ? 'homme' : 'neutre' %}
<i class="fa fa-fw {{ gender }}" title="{{ genderTitle }}"></i>{{ born ~ ' le ' ~ p.person.birthdate|format_date('short') }}
</p>
</div>

View File

@@ -0,0 +1,11 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Move household'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Addresses history for household'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
<a class="sc-button bt-update"
href="{{ chill_path_add_return_path('chill_person_household_address_move', { 'household_id': household.id }) }}">
{{ 'Move household'|trans }}
</a>
{% endblock %}

View File

@@ -0,0 +1,26 @@
<div class="subheader">
<div class="grid-12 parent" id="header-accompanying_course-name" >
<div class="grid-10 push-1 grid-mobile-12 grid-tablet-12 push-mobile-0 push-tablet-0 parent">
<div class="grid-6">{% set title = title %}
<h1>
<i class="fa fa-child"></i>
{{ 'Household'|trans }}
<span style="font-weight: lighter; font-size: 50%;">(n°{{ household.id }})</span>
</h1>
</div>
<div class="grid-3" id="banner-flags"></div>
<div class="grid-3" id="banner-status"></div>
</div>
</div>
<div class="grid-12 parent" id="header-accompanying_course-details" >
<div class="grid-10 push-1 grid-mobile-12 grid-tablet-12 push-mobile-0 push-tablet-0 parent">
<div id="banner-misc"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
{% extends "ChillMainBundle::layoutWithVerticalMenu.html.twig" %}
{% block top_banner %}
{{ include('@ChillPerson/Household/banner.html.twig', { title: block('title') }) }}
{% endblock %}
{% block layout_wvm_content %}
{% block content %}{% endblock %}
{% endblock %}
{% block vertical_menu_content %}
{{ chill_menu('household', {
'layout': '@ChillPerson/Household/menu.html.twig',
'args' : { 'household': household }
}) }}
{% endblock %}

View File

@@ -0,0 +1,10 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Household members'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
{% endblock %}

View File

@@ -0,0 +1,7 @@
<ul class="tab-nav">
{% for menu in menus %}
<li class="">
<a href="{{ menu.uri }}" >{{ menu.label|upper }}</a>
</li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,14 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Household summary'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
<h2>{{ 'Actual household members'|trans }}</h2>
<p>TODO</p>
{% endblock %}

View File

@@ -5,9 +5,10 @@ namespace Chill\PersonBundle\Templating\Entity;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use RuntimeException;
use Symfony\Component\Templating\EngineInterface;
class SocialIssueRender implements ChillEntityRenderInterface
final class SocialIssueRender implements ChillEntityRenderInterface
{
private TranslatableStringHelper $translatableStringHelper;
private EngineInterface $engine;
@@ -28,11 +29,14 @@ class SocialIssueRender implements ChillEntityRenderInterface
{
return $entity instanceof SocialIssue;
}
/**
* @param SocialIssue $socialIssue
*/
public function renderString($socialIssue, array $options): string
{
/** @var $socialIssue SocialIssue */
$options = \array_merge(self::DEFAULT_ARGS, $options);
$options = array_merge(self::DEFAULT_ARGS, $options);
$str = $this->translatableStringHelper->localize($socialIssue->getTitle());
@@ -46,26 +50,36 @@ class SocialIssueRender implements ChillEntityRenderInterface
return $str;
}
protected function buildParents($socialIssue): array
protected function buildParents(SocialIssue $socialIssue): array
{
$parents = [];
while ($socialIssue->hasParent()) {
$socialIssue = $parents[] = $socialIssue->getParent();
}
return $parents;
}
}
/**
*
* @param SocialIssue $socialIssue
*/
public function renderBox($socialIssue, array $options): string
{
$options = \array_merge(self::DEFAULT_ARGS, $options);
$options = array_merge(self::DEFAULT_ARGS, $options);
// give some help to twig: an array of parents
$parents = $this->buildParents($socialIssue);
return $this->engine->render('@ChillPerson/Entity/social_issue.html.twig', [
'socialIssue' => $socialIssue,
'parents' => $parents,
'options' => $options
]);
return $this
->engine
->render(
'@ChillPerson/Entity/social_issue.html.twig',
[
'socialIssue' => $socialIssue,
'parents' => $parents,
'options' => $options
]
);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Chill\PersonBundle\Tests\Controller;
use Chill\PersonBundle\Entity\Household\Household;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
class HouseholdControllerTest extends WebTestCase
{
use PrepareClientTrait;
private ?KernelBrowser $client = null;
protected function setUp()
{
$this->client = $this->getClientAuthenticated();
}
/**
* @dataProvider generateValidHouseholdIds
*/
public function testSummary($householdId)
{
$this->client->request(
Request::METHOD_GET,
"/fr/person/household/{$householdId}/summary"
);
$this->assertResponseIsSuccessful();
}
/**
* @dataProvider generateValidHouseholdIds
*/
public function testMembers($householdId)
{
$this->client->request(
Request::METHOD_GET,
"/fr/person/household/{$householdId}/members"
);
$this->assertResponseIsSuccessful();
}
/**
* @dataProvider generateValidHouseholdIds
*/
public function testAddresses($householdId)
{
$this->client->request(
Request::METHOD_GET,
"/fr/person/household/{$householdId}/addresses"
);
$this->assertResponseIsSuccessful();
}
/**
* @dataProvider generateValidHouseholdIds
*/
public function testAddressMove($householdId)
{
$this->client->request(
Request::METHOD_GET,
"/fr/person/household/{$householdId}/address/move"
);
$this->assertResponseIsSuccessful();
// ici, il faudrait tester la requête POST
}
public function generateValidHouseholdIds()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$ids = $em->createQuery("SELECT DISTINCT h.id FROM ".Household::class." h ".
"JOIN h.members m ".
"JOIN m.person p ".
"JOIN p.center c ".
"WHERE c.name = :center"
)
->setParameter('center', "Center A")
->setMaxResults(100)
->getScalarResult()
;
\shuffle($ids);
yield [ \array_pop($ids)['id'] ];
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Chill\PersonBundle\Tests\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class HouseholdNormalizerTest extends KernelTestCase
{
private ?NormalizerInterface $normalizer;
protected function setUp()
{
self::bootKernel();
$this->normalizer= self::$container->get(NormalizerInterface::class);
}
public function testNormalizationRecursive()
{
$person = new Person();
$member = new HouseholdMember();
$household = new Household();
$position = (new Position())
->setShareHousehold(true)
->setAllowHolder(true)
;
$member->setPerson($person)
->setStartDate(new \DateTimeImmutable('1 year ago'))
->setEndDate(new \DateTimeImmutable('1 month ago'));
$household->addMember($member);
$normalized = $this->normalizer->normalize($household,
'json', [ 'groups' => [ 'read' ]]);
$this->assertIsArray($normalized);
$this->assertArrayHasKey('type', $normalized);
$this->assertEquals('household', $normalized['type']);
}
}

View File

@@ -1,7 +1,7 @@
parameters:
# cl_chill_person.example.class: Chill\PersonBundle\Example
services:
services:
_defaults:
autowire: true
autoconfigure: true
@@ -62,3 +62,10 @@ services:
autoconfigure: true
resource: '../Repository/'
tags: ['doctrine.repository_service']
Chill\PersonBundle\Templating\Entity\:
autowire: true
autoconfigure: true
resource: '../Templating/Entity'
tags:
- 'chill.render_entity'

View File

@@ -1,17 +1,23 @@
services:
Chill\PersonBundle\Menu\SectionMenuBuilder:
arguments:
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
tags:
- { name: 'chill.menu_builder' }
Chill\PersonBundle\Menu\AdminMenuBuilder:
arguments:
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
Chill\PersonBundle\Menu\:
resource: './../../Menu'
autowire: true
tags:
- { name: 'chill.menu_builder' }
# Chill\PersonBundle\Menu\SectionMenuBuilder:
# arguments:
# $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
# $translator: '@Symfony\Component\Translation\TranslatorInterface'
# tags:
# - { name: 'chill.menu_builder' }
#
# Chill\PersonBundle\Menu\AdminMenuBuilder:
# arguments:
# $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
# tags:
# - { name: 'chill.menu_builder' }
#
Chill\PersonBundle\Menu\PersonMenuBuilder:
arguments:
$showAccompanyingPeriod: '%chill_person.accompanying_period%'
@@ -19,8 +25,8 @@ services:
tags:
- { name: 'chill.menu_builder' }
Chill\PersonBundle\Menu\AccompanyingCourseMenuBuilder:
arguments:
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
tags:
- { name: 'chill.menu_builder' }
# Chill\PersonBundle\Menu\AccompanyingCourseMenuBuilder:
# arguments:
# $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
# tags:
# - { name: 'chill.menu_builder' }

View File

@@ -1,25 +0,0 @@
services:
Chill\PersonBundle\Templating\Entity\:
resource: '../../Templating/Entity'
tags:
- 'chill.render_entity'
Chill\PersonBundle\Templating\Entity\PersonRender:
arguments:
$configAltNamesHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper'
$engine: '@Symfony\Component\Templating\EngineInterface'
tags:
- 'chill.render_entity'
Chill\PersonBundle\Templating\Entity\ClosingMotiveRender:
arguments:
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
tags:
- 'chill.render_entity'
Chill\PersonBundle\Templating\Entity\SocialIssueRender:
arguments:
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
$engine: '@Symfony\Component\Templating\EngineInterface'
tags:
- 'chill.render_entity'