Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles

This commit is contained in:
nobohan 2021-10-18 11:24:29 +02:00
commit fcc8f67094
77 changed files with 2122 additions and 736 deletions

View File

@ -10,6 +10,13 @@ and this project adheres to
## Unreleased ## Unreleased
* [3party]: french translation of contact and company
* [3party]: show parent in list
* [3party]: change color for badge "child"
* [3party]: fix address creation
* [household members editor] finalisation of editor
## Test releases ## Test releases
@ -36,10 +43,14 @@ and this project adheres to
* [FilterOrder]: add development kit for generating filter and ordering in list * [FilterOrder]: add development kit for generating filter and ordering in list
* [Capitalization of names] person names are capitalized on creation, on prePersist event * [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 ### test release 2021-10-04
* [Household editor][UI] Update how household suggestion and addresses are picked; * [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; * [AddAddress] Handle address suggestion;
* [CenterType][Create a person] when overriding the ACL rules, allow to show a PickCenterType * [CenterType][Create a person] when overriding the ACL rules, allow to show a PickCenterType
when no centers are reachable by the default ACL. when no centers are reachable by the default ACL.
@ -58,8 +69,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/accent-suivi-developpement/-/issues/37
https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/221 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 ; * [Household editor] suggest only temporarily addresses;
* [AccompanyingCourse Resume page] associated persons list, can see household when hover, and with show on-the-fly modale when clicking person ; 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

@ -141,7 +141,6 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface, C
$ref = 'activity_'.$person->getFullnameCanonical(); $ref = 'activity_'.$person->getFullnameCanonical();
for($i = 0; $i < $activityNbr; $i ++) { for($i = 0; $i < $activityNbr; $i ++) {
print "Creating an activity type for : ".$person." (ref: ".$ref.") \n";
$activity = $this->newRandomActivity($person); $activity = $this->newRandomActivity($person);
$manager->persist($activity); $manager->persist($activity);
} }

View File

@ -1,16 +1,16 @@
{# {#
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop> * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
#} #}
@ -33,7 +33,7 @@
{# CFChoice : render the different elements in a choice list #} {# CFChoice : render the different elements in a choice list #}
{% block cf_choices_row %} {% block cf_choices_row %}
<h3>{{ 'Choices'|trans }}</h3> <h3>{{ 'Choices'|trans }}</h3>
<div id="{{ form.vars.id }}" data-prototype="{{- form_row(form.vars.prototype.children.name) <div id="{{ form.vars.id }}" data-prototype="{{- form_row(form.vars.prototype.children.name)
~ form_row(form.vars.prototype.children.active) ~ form_row(form.vars.prototype.children.active)
~ form_row(form.vars.prototype.children.slug) -}}"> ~ form_row(form.vars.prototype.children.slug) -}}">
@ -47,8 +47,8 @@
{% endfor %} {% endfor %}
</tbody></table> </tbody></table>
</div> </div>
{# we use javascrit to add an additional element. All functions are personnalized with the id ( = form.vars.id) #} {# we use javascrit to add an additional element. All functions are personnalized with the id ( = form.vars.id) #}
<script type="text/javascript"> <script type="text/javascript">
function addElementInDiv(div_id) { function addElementInDiv(div_id) {
@ -109,3 +109,13 @@
{# The choice_with_other_widget widget is defined in the main bundle #} {# The choice_with_other_widget widget is defined in the main bundle #}
{% block pick_address_row %}
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
{% endblock %}
{% block pick_address_widget %}
{{ form_widget(form) }}
<div data-input-address-container="{{ form.vars.uniqid }}"></div>
{% endblock %}

View File

@ -17,20 +17,21 @@ class LoadCivility extends Fixture implements FixtureGroupInterface
public function load(ObjectManager $manager): void public function load(ObjectManager $manager): void
{ {
$civilities = [ $civilities = [
['name' => ['fr' => "Monsieur" ]], ['name' => ['fr' => "Monsieur" ], 'abbrev' => ['fr' => 'M.']],
['name' => ['fr' => "Madame" ]], ['name' => ['fr' => "Madame" ], 'abbrev' => ['fr' => 'Mme']],
['name' => ['fr' => "Docteur" ]], ['name' => ['fr' => "Docteur" ], 'abbrev' => ['fr' => 'Dr']],
['name' => ['fr' => "Professeur" ]], ['name' => ['fr' => "Professeur" ], 'abbrev' => ['fr' => 'Pr']],
['name' => ['fr' => "Madame la Directrice" ]], ['name' => ['fr' => "Madame la Directrice" ], 'abbrev' => ['fr' => 'Mme']],
['name' => ['fr' => "Monsieur le Directeur" ]], ['name' => ['fr' => "Monsieur le Directeur" ], 'abbrev' => ['fr' => 'M.']],
['name' => ['fr' => "Madame la Maire" ]], ['name' => ['fr' => "Madame la Maire" ]],
['name' => ['fr' => "Monsieur le Maire" ]], ['name' => ['fr' => "Monsieur le Maire" ]],
['name' => ['fr' => "Maître" ]], ['name' => ['fr' => "Maître" ], 'abbrev' => ['fr' => 'Me']],
]; ];
foreach ( $civilities as $val) { foreach ( $civilities as $val) {
$civility = (new Civility()) $civility = (new Civility())
->setName($val['name']) ->setName($val['name'])
->setAbbreviation($val['abbrev'] ?? [])
->setActive(true); ->setActive(true);
$manager->persist($civility); $manager->persist($civility);
} }

View File

@ -23,7 +23,7 @@ class Address
* @ORM\Id * @ORM\Id
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO") * @ORM\GeneratedValue(strategy="AUTO")
* @groups({"write"}) * @Groups({"write"})
*/ */
private $id; private $id;
@ -31,7 +31,7 @@ class Address
* @var string * @var string
* *
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $street = ''; private $street = '';
@ -39,7 +39,7 @@ class Address
* @var string * @var string
* *
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $streetNumber = ''; private $streetNumber = '';
@ -47,7 +47,7 @@ class Address
* @var PostalCode * @var PostalCode
* *
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @groups({"write"}) * @Groups({"write"})
*/ */
private $postcode; private $postcode;
@ -55,7 +55,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=16, nullable=true) * @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $floor; private $floor;
@ -63,7 +63,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=16, nullable=true) * @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $corridor; private $corridor;
@ -71,7 +71,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=16, nullable=true) * @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $steps; private $steps;
@ -79,7 +79,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $buildingName; private $buildingName;
@ -87,7 +87,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=16, nullable=true) * @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $flat; private $flat;
@ -95,7 +95,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $distribution; private $distribution;
@ -103,7 +103,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $extra; private $extra;
@ -114,7 +114,7 @@ class Address
* @var \DateTime * @var \DateTime
* *
* @ORM\Column(type="date") * @ORM\Column(type="date")
* @groups({"write"}) * @Groups({"write"})
*/ */
private \DateTime $validFrom; private \DateTime $validFrom;
@ -125,13 +125,13 @@ class Address
* @var \DateTime|null * @var \DateTime|null
* *
* @ORM\Column(type="date", nullable=true) * @ORM\Column(type="date", nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private ?\DateTime $validTo = null; private ?\DateTime $validTo = null;
/** /**
* True if the address is a "no address", aka homeless person, ... * True if the address is a "no address", aka homeless person, ...
* @groups({"write"}) * @Groups({"write"})
* @ORM\Column(type="boolean") * @ORM\Column(type="boolean")
* *
* @var bool * @var bool
@ -144,7 +144,7 @@ class Address
* @var Point|null * @var Point|null
* *
* @ORM\Column(type="point", nullable=true) * @ORM\Column(type="point", nullable=true)
* @groups({"write"}) * @Groups({"write"})
*/ */
private $point; private $point;
@ -154,7 +154,7 @@ class Address
* @var ThirdParty|null * @var ThirdParty|null
* *
* @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty") * @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty")
* @groups({"write"}) * @Groups({"write"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL") * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/ */
private $linkedToThirdParty; private $linkedToThirdParty;
@ -166,6 +166,12 @@ class Address
*/ */
private $customs = []; private $customs = [];
/**
* @ORM\ManyToOne(targetEntity=AddressReference::class)
* @Groups({"write"})
*/
private ?AddressReference $addressReference = null;
public function __construct() public function __construct()
{ {
$this->validFrom = new \DateTime(); $this->validFrom = new \DateTime();
@ -376,6 +382,7 @@ class Address
public static function createFromAddress(Address $original) : Address public static function createFromAddress(Address $original) : Address
{ {
return (new Address()) return (new Address())
->setAddressReference($original->getAddressReference())
->setBuildingName($original->getBuildingName()) ->setBuildingName($original->getBuildingName())
->setCorridor($original->getCorridor()) ->setCorridor($original->getCorridor())
->setCustoms($original->getCustoms()) ->setCustoms($original->getCustoms())
@ -402,6 +409,7 @@ class Address
->setPostcode($original->getPostcode()) ->setPostcode($original->getPostcode())
->setStreet($original->getStreet()) ->setStreet($original->getStreet())
->setStreetNumber($original->getStreetNumber()) ->setStreetNumber($original->getStreetNumber())
->setAddressReference($original)
; ;
} }
@ -549,5 +557,22 @@ class Address
return $this; 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,45 @@
<?php
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Repository\AddressRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
final class AddressToIdDataTransformer implements DataTransformerInterface
{
private AddressRepository $addressRepository;
public function __construct(AddressRepository $addressRepository)
{
$this->addressRepository = $addressRepository;
}
public function reverseTransform($value)
{
if (NULL === $value || '' === $value) {
return null;
}
$address = $this->addressRepository->find($value);
if (NULL === $address) {
$failure = new TransformationFailedException(sprintf("Address with id %s does not exists", $value));
$failure
->setInvalidMessage("The given {{ value }} is not a valid address id", [ '{{ value }}' => $value]);
throw $failure;
}
return $address;
}
public function transform($value)
{
if (NULL === $value) {
return '';
}
return $value->getId();
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Form\Type\DataTransformer\AddressToIdDataTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Form type for picking an address.
*
* In the UI, this resolve to a vuejs component which will insert the created address id into the
* hidden's value. It will also allow to edit existing addresses without changing the id.
*
* In every page where this component is shown, you must include the required module:
*
* ```twig
* {% block js %}
* {{ encore_entry_script_tags('mod_input_address') }}
* {% endblock %}
*
* {% block css %}
* {{ encore_entry_link_tags('mod_input_address') }}
* {% endblock %}
* ```
*/
final class PickAddressType extends AbstractType
{
private AddressToIdDataTransformer $addressToIdDataTransformer;
private TranslatorInterface $translator;
public function __construct(
AddressToIdDataTransformer $addressToIdDataTransformer,
TranslatorInterface $translator
) {
$this->addressToIdDataTransformer = $addressToIdDataTransformer;
$this->translator = $translator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer($this->addressToIdDataTransformer);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['uniqid'] = $view->vars['attr']['data-input-address'] =\uniqid('input_address_');
$view->vars['attr']['data-use-valid-from'] = (int) $options['use_valid_from'];
$view->vars['attr']['data-use-valid-to'] = (int) $options['use_valid_to'];
$view->vars['attr']['data-button-text-create'] = $this->translator->trans($options['button_text_create']);
$view->vars['attr']['data-button-text-update'] = $this->translator->trans($options['button_text_update']);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => Address::class,
'use_valid_to' => false,
'use_valid_from' => false,
'button_text_create' => 'Create an address',
'button_text_update' => 'Update address',
// reset default from hidden type
'required' => true,
'error_bubbling' => false,
]);
}
public function getParent()
{
return HiddenType::class;
}
}

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 = () => { const fetchScopes = () => {
return window.fetch('/api/1.0/main/scope.json').then(response => { return fetchResults('/api/1.0/main/scope.json');
if (response.ok) {
return response.json();
}
}).then(data => {
//console.log(data);
return new Promise((resolve, reject) => {
//console.log(data);
resolve(data.results);
});
});
}; };
export { export {

View File

@ -17,7 +17,8 @@ export default {
components: { components: {
AddAddress AddAddress
}, },
props: ['addAddress'], props: ['addAddress', 'callback'],
emits: ['addressEdited', 'addressCreated'],
computed: { computed: {
context() { context() {
return this.addAddress.context; return this.addAddress.context;
@ -46,6 +47,7 @@ export default {
// address is already linked, just finish ! // address is already linked, just finish !
this.$refs.addAddress.afterLastPaneAction({}); this.$refs.addAddress.afterLastPaneAction({});
this.$emit('addressEdited', payload);
// New created address // New created address
} else { } else {
@ -57,6 +59,8 @@ export default {
* Post new created address to targetEntity * Post new created address to targetEntity
*/ */
postAddressTo(payload) { postAddressTo(payload) {
this.$emit('addressCreated', payload);
console.log('postAddress', payload.addressId, 'To', payload.target, payload.targetId); console.log('postAddress', payload.addressId, 'To', payload.target, payload.targetId);
switch (payload.target) { switch (payload.target) {
case 'household': case 'household':

View File

@ -267,6 +267,9 @@ export default {
title: { create: 'add_an_address_title', edit: 'edit_address' }, title: { create: 'add_an_address_title', edit: 'edit_address' },
openPanesInModal: true, openPanesInModal: true,
stickyActions: false, stickyActions: false,
// show a message when no address.
// if set to undefined, the value will be equivalent to false if stickyActions is false, true otherwise.
showMessageWhenNoAddress: undefined,
useDate: { useDate: {
validFrom: false, validFrom: false,
validTo: false validTo: false
@ -586,6 +589,14 @@ export default {
'point': this.entity.selected.address.point.coordinates '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) { if (this.validFrom) {
console.log('add validFrom in fetch body', this.entity.selected.valid.from); console.log('add validFrom in fetch body', this.entity.selected.valid.from);
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
@ -606,7 +617,7 @@ export default {
let newPostcode = this.entity.selected.postcode; let newPostcode = this.entity.selected.postcode;
newPostcode = Object.assign(newPostcode, { newPostcode = Object.assign(newPostcode, {
'country': {'id': this.entity.selected.country.id }, 'country': {'id': this.entity.selected.country.id },
}); });//TODO why not assign postcodeBody here = Object.assign(postcodeBody, {'origin': 3}); ?
console.log('writeNew postcode is true! newPostcode: ', newPostcode); console.log('writeNew postcode is true! newPostcode: ', newPostcode);
newAddress = Object.assign(newAddress, { newAddress = Object.assign(newAddress, {
'newPostcode': newPostcode 'newPostcode': newPostcode
@ -638,9 +649,7 @@ export default {
if ('newPostcode' in payload) { if ('newPostcode' in payload) {
let postcodeBody = payload.newPostcode; let postcodeBody = payload.newPostcode;
if (this.context.target.name === 'person') { // !!! maintain here ? postcodeBody = Object.assign(postcodeBody, {'origin': 3});
postcodeBody = Object.assign(postcodeBody, {'origin': 3});
}
console.log('juste before post new postcode', postcodeBody); console.log('juste before post new postcode', postcodeBody);
return postPostalCode(postcodeBody) return postPostalCode(postcodeBody)
.then(postalCode => { .then(postalCode => {
@ -730,6 +739,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 * @param address the address selected
*/ */

View File

@ -95,6 +95,9 @@ export default {
}, },
selectAddress(value) { selectAddress(value) {
this.entity.selected.address = 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.street = value.street;
this.entity.selected.address.streetNumber = value.streetNumber; this.entity.selected.address.streetNumber = value.streetNumber;
this.entity.selected.writeNew.address = false; this.entity.selected.writeNew.address = false;

View File

@ -50,7 +50,7 @@ import VueMultiselect from 'vue-multiselect';
export default { export default {
name: 'CitySelection', name: 'CitySelection',
components: { VueMultiselect }, components: { VueMultiselect },
props: ['entity', 'focusOnAddress'], props: ['entity', 'focusOnAddress', 'updateMapCenter'],
emits: ['getReferenceAddresses'], emits: ['getReferenceAddresses'],
data() { data() {
return { return {
@ -95,6 +95,7 @@ export default {
return (value.code && value.name) ? `${value.code}-${value.name}` : ''; return (value.code && value.name) ? `${value.code}-${value.name}` : '';
}, },
selectCity(value) { selectCity(value) {
console.log(value)
this.entity.selected.city = value; this.entity.selected.city = value;
this.entity.selected.postcode.name = value.name; this.entity.selected.postcode.name = value.name;
this.entity.selected.postcode.code = value.code; this.entity.selected.postcode.code = value.code;
@ -102,6 +103,7 @@ export default {
console.log('writeNew.postcode false, in selectCity'); console.log('writeNew.postcode false, in selectCity');
this.$emit('getReferenceAddresses', value); this.$emit('getReferenceAddresses', value);
this.focusOnAddress(); this.focusOnAddress();
this.updateMapCenter(value.center);
}, },
listenInputSearch(query) { listenInputSearch(query) {
//console.log('listenInputSearch', query, this.isCitySelectorOpen); //console.log('listenInputSearch', query, this.isCitySelectorOpen);

View File

@ -31,6 +31,7 @@
<city-selection <city-selection
v-bind:entity="entity" v-bind:entity="entity"
v-bind:focusOnAddress="focusOnAddress" v-bind:focusOnAddress="focusOnAddress"
v-bind:updateMapCenter="updateMapCenter"
@getReferenceAddresses="$emit('getReferenceAddresses', selected.city)"> @getReferenceAddresses="$emit('getReferenceAddresses', selected.city)">
</city-selection> </city-selection>
@ -135,7 +136,7 @@ export default {
} }
}, },
updateMapCenter(point) { updateMapCenter(point) {
//console.log('point', point); console.log('point', point);
this.addressMap.center[0] = point.coordinates[1]; // TODO use reverse() this.addressMap.center[0] = point.coordinates[1]; // TODO use reverse()
this.addressMap.center[1] = point.coordinates[0]; this.addressMap.center[1] = point.coordinates[0];
this.$refs.addressMap.update(); // cast child methods this.$refs.addressMap.update(); // cast child methods

View File

@ -15,7 +15,7 @@
<span v-if="forceRedirect">{{ $t('wait_redirection') }}</span> <span v-if="forceRedirect">{{ $t('wait_redirection') }}</span>
</div> </div>
<div v-if="noAddressWithStickyActions" class="mt-5"> <div v-if="showMessageWhenNoAddress" class="mt-5">
<p class="chill-no-data-statement"> <p class="chill-no-data-statement">
{{ $t('not_yet_address') }} {{ $t('not_yet_address') }}
</p> </p>
@ -50,8 +50,8 @@ export default {
}, },
props: [ props: [
'context', 'context',
'options',
'defaultz', 'defaultz',
'options',
'flag', 'flag',
'entity', 'entity',
'errorMsg', 'errorMsg',
@ -91,7 +91,11 @@ export default {
forceRedirect() { forceRedirect() {
return (!(this.context.backUrl === null || typeof this.context.backUrl === 'undefined')); return (!(this.context.backUrl === null || typeof this.context.backUrl === 'undefined'));
}, },
noAddressWithStickyActions() { showMessageWhenNoAddress() {
let showMessageWhenNoAddress = this.options.showMessageWhenNoAddress === undefined ? this.defaultz.showMessageWhenNoAddress : this.options.showMessageWhenNoAddress;
if (showMessageWhenNoAddress === true || showMessageWhenNoAddress === false) {
return !this.context.edit && !this.address.id && showMessageWhenNoAddress;
}
return !this.context.edit && !this.address.id && this.options.stickyActions; return !this.context.edit && !this.address.id && this.options.stickyActions;
} }
} }

View File

@ -0,0 +1,86 @@
import {createApp} from 'vue';
import {_createI18n} from 'ChillMainAssets/vuejs/_js/i18n';
import {addressMessages} from './i18n';
import App from './App.vue';
const i18n = _createI18n(addressMessages);
let inputs = document.querySelectorAll('input[type="hidden"][data-input-address]');
const isNumeric = function(v) { return !isNaN(v); };
inputs.forEach(el => {
let
addressId = el.value,
uniqid = el.dataset.inputAddress,
container = document.querySelector('div[data-input-address-container="' + uniqid + '"]'),
isEdit = addressId !== '',
addressIdInt = addressId !== '' ? parseInt(addressId) : null
;
if (container === null) {
throw Error("no container");
}
console.log('useValidFrom', el.dataset.useValidFrom === '1');
const app = createApp({
template: `<app v-bind:addAddress="this.addAddress" @address-created="associateToInput"></app>`,
data() {
return {
addAddress: {
context: {
// for legacy ? can be remove ?
target: {
name: 'input-address',
id: addressIdInt,
},
edit: isEdit,
addressId: addressIdInt,
},
options: {
/// Options override default.
/// null value take default component value defined in AddAddress data()
button: {
text: {
create: el.dataset.buttonTextCreate || null,
edit: el.dataset.buttonTextUpdate || null,
},
size: null,
displayText: true
},
/// Modal title text if create or edit address (trans chain, see i18n)
title: {
create: null,
edit: null,
},
/// Display panes in Modal for step123
openPanesInModal: true,
/// Display actions buttons of panes in a sticky-form-button navbar
stickyActions: false,
showMessageWhenNoAddress: true,
/// Use Date fields
useDate: {
validFrom: el.dataset.useValidFrom === '1' || false, //boolean, default: false
validTo: el.dataset.useValidTo === '1' || false, //boolean, default: false
},
/// Don't display show renderbox Address: showPane display only a button
onlyButton: false,
}
}
}
},
methods: {
associateToInput(payload) {
el.value = payload.addressId;
}
}
})
.use(i18n)
.component('app', App)
.mount(container);
});

View File

@ -27,7 +27,7 @@ class SearchUserApiProvider implements SearchApiInterface
->setSelectPertinence("GREATEST(SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical), ->setSelectPertinence("GREATEST(SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical),
SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical))", [ $pattern, $pattern ]) SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical))", [ $pattern, $pattern ])
->setFromClause("users AS u") ->setFromClause("users AS u")
->setWhereClause("SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15 ->setWhereClauses("SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15
OR OR
SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical) > 0.15 SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical) > 0.15
", [ $pattern, $pattern ]); ", [ $pattern, $pattern ]);

View File

@ -12,8 +12,8 @@ class SearchApiQuery
private array $pertinenceParams = []; private array $pertinenceParams = [];
private ?string $fromClause = null; private ?string $fromClause = null;
private array $fromClauseParams = []; private array $fromClauseParams = [];
private ?string $whereClause = null; private array $whereClauses = [];
private array $whereClauseParams = []; private array $whereClausesParams = [];
public function setSelectKey(string $selectKey, array $params = []): self public function setSelectKey(string $selectKey, array $params = []): self
{ {
@ -47,16 +47,39 @@ class SearchApiQuery
return $this; return $this;
} }
public function setWhereClause(string $whereClause, array $params = []): self /**
* Set the where clause and replace all existing ones.
*
*/
public function setWhereClauses(string $whereClause, array $params = []): self
{ {
$this->whereClause = $whereClause; $this->whereClauses = [$whereClause];
$this->whereClauseParams = $params; $this->whereClausesParams = [$params];
return $this;
}
/**
* Add a where clause.
*
* This will add to previous where clauses with and `AND` join
*
* @param string $whereClause
* @param array $params
* @return $this
*/
public function andWhereClause(string $whereClause, array $params = []): self
{
$this->whereClauses[] = $whereClause;
$this->whereClausesParams[] = $params;
return $this; return $this;
} }
public function buildQuery(): string public function buildQuery(): string
{ {
$where = \implode(' AND ', $this->whereClauses);
return \strtr("SELECT return \strtr("SELECT
'{key}' AS key, '{key}' AS key,
{metadata} AS metadata, {metadata} AS metadata,
@ -68,7 +91,7 @@ class SearchApiQuery
'{metadata}' => $this->jsonbMetadata, '{metadata}' => $this->jsonbMetadata,
'{pertinence}' => $this->pertinence, '{pertinence}' => $this->pertinence,
'{from}' => $this->fromClause, '{from}' => $this->fromClause,
'{where}' => $this->whereClause, '{where}' => $where,
]); ]);
} }
@ -79,7 +102,7 @@ class SearchApiQuery
$this->jsonbMetadataParams, $this->jsonbMetadataParams,
$this->pertinenceParams, $this->pertinenceParams,
$this->fromClauseParams, $this->fromClauseParams,
$this->whereClauseParams, \array_merge([], ...$this->whereClausesParams),
); );
} }
} }

View File

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

View File

@ -0,0 +1,40 @@
<?php
namespace Search;
use Chill\MainBundle\Search\SearchApiQuery;
use PHPUnit\Framework\TestCase;
class SearchApiQueryTest extends TestCase
{
public function testMultipleWhereClauses()
{
$q = new SearchApiQuery();
$q->setSelectJsonbMetadata('boum')
->setSelectKey('bim')
->setSelectPertinence('1')
->setFromClause('badaboum')
->andWhereClause('foo', [ 'alpha' ])
->andWhereClause('bar', [ 'beta' ])
;
$query = $q->buildQuery();
$this->assertStringContainsString('foo AND bar', $query);
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
}
public function testWithoutWhereClause()
{
$q = new SearchApiQuery();
$q->setSelectJsonbMetadata('boum')
->setSelectKey('bim')
->setSelectPertinence('1')
->setFromClause('badaboum')
;
$this->assertTrue(\is_string($q->buildQuery()));
$this->assertEquals([], $q->buildParameters());
}
}

View File

@ -60,6 +60,7 @@ module.exports = function(encore, entries)
encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js'); encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js');
encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js'); encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js');
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
// Vue entrypoints // Vue entrypoints
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');
encore.addEntry('vue_onthefly', __dirname + '/Resources/public/vuejs/OnTheFly/index.js'); encore.addEntry('vue_onthefly', __dirname + '/Resources/public/vuejs/OnTheFly/index.js');

View File

@ -1,4 +1,5 @@
services: services:
chill.main.form.type.translatable.string: chill.main.form.type.translatable.string:
class: Chill\MainBundle\Form\Type\TranslatableStringFormType class: Chill\MainBundle\Form\Type\TranslatableStringFormType
arguments: arguments:
@ -128,3 +129,11 @@ services:
tags: tags:
- { name: form.type } - { name: form.type }
Chill\MainBundle\Form\Type\PickAddressType:
autoconfigure: true
autowire: true
Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer:
autoconfigure: true
autowire: true

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

@ -84,6 +84,8 @@ address more:
extra: "" extra: ""
distribution: cedex distribution: cedex
Create a new address: Créer une nouvelle adresse Create a new address: Créer une nouvelle adresse
Create an address: Créer une adresse
Update address: Modifier l'adresse
#serach #serach
Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche. Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche.

View File

@ -4,24 +4,31 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepositoryInterface;
use Chill\PersonBundle\Repository\Household\HouseholdRepository; use Chill\PersonBundle\Repository\Household\HouseholdRepository;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class HouseholdApiController extends ApiController class HouseholdApiController extends ApiController
{ {
private HouseholdRepository $householdRepository; private HouseholdRepository $householdRepository;
public function __construct(HouseholdRepository $householdRepository) private HouseholdACLAwareRepositoryInterface $householdACLAwareRepository;
{
public function __construct(
HouseholdRepository $householdRepository,
HouseholdACLAwareRepositoryInterface $householdACLAwareRepository
) {
$this->householdRepository = $householdRepository; $this->householdRepository = $householdRepository;
$this->householdACLAwareRepository = $householdACLAwareRepository;
} }
public function householdAddressApi($id, Request $request, string $_format): Response public function householdAddressApi($id, Request $request, string $_format): Response
{ {
@ -37,7 +44,7 @@ class HouseholdApiController extends ApiController
{ {
// TODO add acl // TODO add acl
$count = $this->householdRepository->countByAccompanyingPeriodParticipation($person); $count = $this->householdRepository->countByAccompanyingPeriodParticipation($person);
$paginator = $this->getPaginatorFactory()->create($count); $paginator = $this->getPaginatorFactory()->create($count);
if ($count === 0) { if ($count === 0) {
@ -93,4 +100,27 @@ class HouseholdApiController extends ApiController
return $this->json(\array_values($addresses), Response::HTTP_OK, [], return $this->json(\array_values($addresses), Response::HTTP_OK, [],
[ 'groups' => [ 'read' ] ]); [ '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(); $a = $participation->getAccompanyingPeriod()->getAddressLocation();
$addresses[$a->getId()] = $a; $addresses[$a->getId()] = $a;
} }
if (null !== $personLocation = $participation
->getAccompanyingPeriod()->getPersonLocation()) {
$a = $personLocation->getCurrentHouseholdAddress();
if (null !== $a) {
$addresses[$a->getId()] = $a;
}
}
} }
// remove the actual address // 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> <template>
<household></household> <ol class="breadcrumb">
<concerned v-if="hasHouseholdOrLeave"></concerned> <li
<dates v-if="showConfirm"></dates> v-for="s in steps"
<confirmation v-if="showConfirm"></confirmation> 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> </template>
<script> <script>
import { mapGetters } from 'vuex'; import {mapGetters, mapState} from 'vuex';
import Concerned from './components/Concerned.vue'; import Concerned from './components/Concerned.vue';
import Household from './components/Household.vue'; import Household from './components/Household.vue';
import HouseholdAddress from './components/HouseholdAddress';
import Dates from './components/Dates.vue'; import Dates from './components/Dates.vue';
import Confirmation from './components/Confirmation.vue'; import Confirmation from './components/Confirmation.vue';
import Positioning from "./components/Positioning";
export default { export default {
name: 'App', name: 'App',
components: { components: {
Positioning,
Concerned, Concerned,
Household, Household,
HouseholdAddress,
Dates, Dates,
Confirmation, Confirmation,
}, },
data() {
return {
step: 'concerned',
};
},
computed: { computed: {
...mapGetters([ ...mapState({
'hasHouseholdOrLeave', hasWarnings: (state) => state.warnings.length > 0 || state.errors.length > 0,
'hasPersonsWellPositionnated', }),
]), steps() {
showConfirm () { let s = ['concerned', 'household'];
return this.$store.getters.hasHouseholdOrLeave
&& this.$store.getters.hasPersonsWellPositionnated; 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> <template>
<h2 class="mt-4">{{ $t('household_members_editor.concerned.title') }}</h2> <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 v-if="noPerson">
<div class="alert alert-info"> <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> </div>
<div v-else-if="allPersonsPositionnated">
<span class="chill-no-data-statement">{{ $t('household_members_editor.all_positionnated') }}</span>
</div>
<div v-else> <div v-else>
<div class="flex-table list-household-members"> <p>
<div v-for="conc in concUnpositionned" {{ $t('household_members_editor.concerned.persons_will_be_moved') }}&nbsp;:
class="item-bloc" <span v-for="c in concerned">
v-bind:key="conc.person.id" <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;">
<div class="item-row"> <span class="fa-stack fa-lg" :title="$t('household_members_editor.concerned.remove_concerned')">
<div class="item-col"> <i class="fa fa-circle fa-stack-1x text-danger"></i>
<div> <i class="fa fa-times fa-stack-1x"></i>
<person-render-box render="badge" :options="{}" :person="conc.person"></person-render-box> </span>
</div> </button>
<div v-if="conc.person.birthdate !== null"> </span>
{{ $t('person.born', {'gender': conc.person.gender} ) }} </p>
{{ $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>
</div> </div>
<div> <ul class="record_actions">
<add-persons <li>
buttonTitle="household_members_editor.concerned.add_persons" <add-persons
modalTitle="household_members_editor.concerned.search" buttonTitle="household_members_editor.concerned.add_persons"
v-bind:key="addPersons.key" modalTitle="household_members_editor.concerned.search"
v-bind:options="addPersons.options" v-bind:key="addPersons.key"
@addNewPersons="addNewPersons" v-bind:options="addPersons.options"
ref="addPersons"> <!-- to cast child method --> @addNewPersons="addNewPersons"
</add-persons> ref="addPersons"> <!-- to cast child method -->
</div> </add-persons>
</li>
</ul>
<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>
</template> </template>
<style lang="scss"> <style lang="scss">
div.person {
cursor: move;
* {
cursor: move
}
}
.move_to { .move_to {
.move_hint { .move_hint {
@ -124,33 +47,26 @@ div.person {
</style> </style>
<script> <script>
import { mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'; import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.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 { export default {
name: 'Concerned', name: 'Concerned',
components: { components: {
AddPersons, AddPersons,
MemberDetails,
PersonRenderBox, PersonRenderBox,
}, },
computed: { computed: {
...mapState([
'concerned'
]),
...mapGetters([ ...mapGetters([
'concUnpositionned', 'persons',
'positions',
'concByPosition',
'needsPositionning'
]), ]),
noPerson () { noPerson () {
return this.$store.getters.persons.length === 0; return this.$store.getters.persons.length === 0;
}, },
allPersonsPositionnated () {
return this.$store.getters.persons.length > 0
&& this.$store.getters.concUnpositionned.length === 0;
},
}, },
data() { data() {
return { return {
@ -172,11 +88,9 @@ export default {
this.$refs.addPersons.resetSearch(); // to cast child method this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false; modal.showModal = false;
}, },
moveToPosition(person_id, position_id) { removePerson(person) {
this.$store.dispatch('markPosition', { person_id, position_id }); console.log('remove person in concerned', person);
}, this.$store.dispatch('removePerson', person);
removeConcerned(conc) {
this.$store.dispatch('removeConcerned', conc);
}, },
} }
} }

View File

@ -1,28 +1,19 @@
<template> <template>
<div v-if="hasWarning" class="alert alert-warning">
<div v-if="hasWarnings" class="alert alert-warning">
{{ $t('household_members_editor.confirmation.there_are_warnings') }} {{ $t('household_members_editor.confirmation.there_are_warnings') }}
</div> </div>
<p v-if="hasWarnings"> <p v-if="hasWarning">
{{ $t('household_members_editor.confirmation.check_those_items') }} {{ $t('household_members_editor.confirmation.check_those_items') }}
</p> </p>
<ul> <ul>
<li v-for="(msg, index) in warnings"> <li v-for="(msg, index) in warnings" class="warning">
{{ $t(msg.m, msg.a) }} {{ $t(msg.m, msg.a) }}
</li> </li>
<li v-for="msg in errors"> <li v-for="msg in errors" class="error">
{{ msg }} {{ msg }}
</li> </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> </ul>
</template> </template>
@ -36,17 +27,11 @@ export default {
name: 'Confirmation', name: 'Confirmation',
computed: { computed: {
...mapState({ ...mapState({
hasWarnings: (state) => state.warnings.length > 0 || state.errors.length > 0,
warnings: (state) => state.warnings, warnings: (state) => state.warnings,
errors: (state) => state.errors, 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> </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> <template>
<h2>{{ $t('household_members_editor.dates_title') }}</h2>
<current-household></current-household>
<h2>{{ $t('household_members_editor.dates.dates_title') }}</h2>
<p> <p>
<label for="start_date"> <label for="start_date">
@ -11,8 +14,13 @@
<script> <script>
import CurrentHousehold from "./CurrentHousehold";
export default { export default {
name: 'Dates', name: 'Dates',
components: {
CurrentHousehold
},
computed: { computed: {
startDate: { startDate: {
get() { get() {
@ -23,10 +31,10 @@ export default {
].join('-'); ].join('-');
}, },
set(value) { set(value) {
let let
[year, month, day] = value.split('-'), [year, month, day] = value.split('-'),
dValue = new Date(year, month-1, day); dValue = new Date(year, month-1, day);
this.$store.dispatch('setStartDate', dValue); this.$store.dispatch('setStartDate', dValue);
} }
} }

View File

@ -2,141 +2,89 @@
<h2 class="mt-4">{{ $t('household_members_editor.household_part') }}</h2> <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 v-if="hasHouseholdSuggestion" class="householdSuggestions my-5">
<h4 class="mb-3">
<div class="flex-table householdSuggestionList"> {{ $t('household_members_editor.household.household_suggested') }}
<div v-if="isModeNewAllowed" class="item-bloc"> </h4>
<div> <p>{{ $t('household_members_editor.household.household_suggested_explanation') }}</p>
<section> <div class="accordion" id="householdSuggestions">
<div class="item-row"> <div class="accordion-item">
<div class="item-col"> <h2 class="accordion-header" id="heading_household_suggestions">
<div class="h4"> <button v-if="!showHouseholdSuggestion"
<i class="fa fa-home"></i> {{ $t('household_members_editor.household.new_household') }} class="accordion-button collapsed"
</div> 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>
</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> </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"> <ul class="record_actions">
<li> <li v-if="hasHousehold">
<button class="btn btn-sm btn-chill-beige" @click="resetMode"> <button @click="resetMode" class="btn btn-sm btn-misc">{{ $t('household_members_editor.household.reset_mode')}}</button>
{{ $t('household_members_editor.household.reset_mode') }} </li>
</button> <li v-if="!hasHousehold">
</li> <button @click="setModeNew" class="btn btn-sm btn-create">{{ $t('household_members_editor.household.create_household') }}</button>
</ul> </li>
</div> <li v-if="isModeLeaveAllowed && !hasHousehold">
</div> <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> </template>
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue'; import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue'; import CurrentHousehold from './CurrentHousehold';
import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress.vue';
export default { export default {
name: 'Household', name: 'Household',
components: { components: {
CurrentHousehold,
HouseholdRenderBox, HouseholdRenderBox,
AddressRenderBox,
AddAddress,
}, },
emits: ['readyToGo'],
data() { data() {
return { return {
addAddress: { addAddress: {
@ -179,6 +127,7 @@ export default {
'getAddressContext', 'getAddressContext',
]), ]),
...mapState([ ...mapState([
'household',
'showHouseholdSuggestion', 'showHouseholdSuggestion',
'showAddressSuggestion', 'showAddressSuggestion',
'mode', 'mode',
@ -190,13 +139,21 @@ export default {
return false; return false;
return this.$store.state.allowHouseholdSearch && !this.$store.getters.hasHousehold; 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: { methods: {
setModeNew() { setModeNew() {
this.$store.dispatch('createHousehold'); this.$store.dispatch('createHousehold');
this.$emit('readyToGo');
}, },
setModeLeave() { setModeLeave() {
this.$store.dispatch('forceLeaveWithoutHousehold'); this.$store.dispatch('forceLeaveWithoutHousehold');
this.$emit('readyToGo');
}, },
resetMode() { resetMode() {
this.$store.commit('resetMode'); this.$store.commit('resetMode');
@ -207,10 +164,14 @@ export default {
}, },
selectHousehold(h) { selectHousehold(h) {
this.$store.dispatch('selectHousehold', h); this.$store.dispatch('selectHousehold', h);
this.$emit('readyToGo');
}, },
removeHouseholdAddress() { removeHouseholdAddress() {
this.$store.commit('removeHouseholdAddress'); this.$store.commit('removeHouseholdAddress');
}, },
toggleHouseholdSuggestion() {
this.$store.commit('toggleHouseholdSuggestion');
},
}, },
}; };
@ -218,6 +179,18 @@ export default {
<style lang="scss"> <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#household_members_editor div,
div.householdSuggestionList { div.householdSuggestionList {
&.flex-table { &.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: { fr: {
household_members_editor: { household_members_editor: {
household: { household: {
no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.", no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage.",
new_household: "Nouveau ménage", // new_household: "Nouveau ménage",
create_household: "Créer", create_household: "Créer",
search_household: "Chercher un ménage", search_household: "Chercher un ménage",
will_leave_any_household: "Les usagers ne rejoignent pas de 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.", 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", leave_without_household: "Sans nouveau ménage",
set_address: "Indiquer une adresse",
reset_mode: "Modifier la destination", reset_mode: "Modifier la destination",
remove_address: "Supprimer l'adresse", household_suggested: "Suggestions de ménage",
update_address: "Mettre à jour l'adresse", household_suggested_explanation: "Les ménages suivants sont connus et pourraient peut-être correspondre à des ménages recherchés."
// remove ? // remove ?
/* /*
where_live_the_household: "À quelle adresse habite ce ménage ?", where_live_the_household: "À quelle adresse habite ce ménage ?",
household_live_to_this_address: "Sélectionner l'adresse", household_live_to_this_address: "Sélectionner l'adresse",
no_suggestions: "Aucune adresse à suggérer", 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", or_create_new_address: "Ou créer une nouvelle adresse",
*/ */
// end remove ? // 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: { 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", add_persons: "Ajouter d'autres usagers",
search: "Rechercher des usagers", search: "Rechercher des usagers",
move_to: "Déplacer vers", move_to: "Déplacer vers",
persons_to_positionnate: 'Usagers à positionner',
persons_leaving: "Usagers quittant leurs ménages", persons_leaving: "Usagers quittant leurs ménages",
no_person_in_position: "Aucun usager ne sera ajouté à cette position", 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}\"", drop_persons_here: "Glissez-déposez ici les usagers pour la position \"{position}\"",
all_positionnated: "Tous les usagers sont positionnés", 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", household_part: "Destination",
suggestions: "Suggestions", suggestions: "Suggestions",
hide_household_suggestion: "Masquer les suggestions", hide_household_suggestion: "Masquer les suggestions",
show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} 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", household_for_participants_accompanying_period: "Des ménages partagent le même parcours",
select_household: "Sélectionner le ménage", select_household: "Sélectionner le ménage",
dates_title: "Période de validité",
dates: { dates: {
start_date: "Début de validité", start_date: "Début de validité",
end_date: "Fin de validité", end_date: "Fin de validité",
dates_title: "Période de validité",
}, },
confirmation: { confirmation: {
save: "Enregistrer", save: "Enregistrer",
there_are_warnings: "Impossible de valider actuellement", there_are_warnings: "Impossible de valider actuellement",
check_those_items: "Veuillez corriger les éléments suivants", 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 { createStore } from 'vuex';
import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod, fetchAddressSuggestionByPerson} from './../api.js'; import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod, fetchAddressSuggestionByPerson} from './../api.js';
import { fetchHouseholdByAddressReference } from 'ChillPersonAssets/lib/household.js';
import { datetimeToISO } from 'ChillMainAssets/chill/js/date.js'; import { datetimeToISO } from 'ChillMainAssets/chill/js/date.js';
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
@ -42,7 +43,16 @@ const store = createStore({
allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch, allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch,
allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold, allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold,
forceLeaveWithoutHousehold: false, 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, showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1,
addressesSuggestion: [], addressesSuggestion: [],
showAddressSuggestion: true, showAddressSuggestion: true,
@ -74,10 +84,12 @@ const store = createStore({
isModeLeave(state) { isModeLeave(state) {
return state.mode === "leave"; return state.mode === "leave";
}, },
isHouseholdForceNoAddress(state) {
return state.forceHouseholdNoAddress;
},
getSuggestions(state) { getSuggestions(state) {
let suggestions = []; let suggestions = [];
state.householdSuggestionByAccompanyingPeriod.forEach(h => { state.householdSuggestionByAccompanyingPeriod.forEach(h => {
console.log(h);
suggestions.push({household: h}); suggestions.push({household: h});
}); });
@ -85,15 +97,12 @@ const store = createStore({
}, },
isHouseholdNew(state) { isHouseholdNew(state) {
return state.mode === "new"; return state.mode === "new";
/*
if (state.household === null) {
return false;
}
return !Number.isInteger(state.household.id);
*/
}, },
getAddressContext(state, getters) { getAddressContext(state, getters) {
if (state.household === null) {
return {};
}
if (!getters.hasHouseholdAddress) { if (!getters.hasHouseholdAddress) {
return { return {
edit: false, edit: false,
@ -198,6 +207,40 @@ const store = createStore({
needsPositionning(state) { needsPositionning(state) {
return state.forceLeaveWithoutHousehold === false; 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) => { buildPayload: (state, getters) => {
let let
conc, conc,
@ -272,6 +315,10 @@ const store = createStore({
position = state.positions.find(pos => pos.id === position_id), position = state.positions.find(pos => pos.id === position_id),
conc = state.concerned.find(c => c.person.id === person_id); conc = state.concerned.find(c => c.person.id === person_id);
conc.position = position; conc.position = position;
// reset position if changed:
if (!position.allowHolder && conc.holder) {
conc.holder = false;
}
}, },
setComment(state, {conc, comment}) { setComment(state, {conc, comment}) {
conc.comment = comment; conc.comment = comment;
@ -283,9 +330,9 @@ const store = createStore({
conc.holder = false; conc.holder = false;
conc.position = null; conc.position = null;
}, },
removeConcerned(state, conc) { removePerson(state, person) {
state.concerned = state.concerned.filter(c => state.concerned = state.concerned.filter(c =>
c.person.id !== conc.person.id c.person.id !== person.id
) )
}, },
createHousehold(state) { createHousehold(state) {
@ -310,6 +357,7 @@ const store = createStore({
} }
state.household.current_address = address; state.household.current_address = address;
state.forceHouseholdNoAddress = false;
}, },
removeHouseholdAddress(state, address) { removeHouseholdAddress(state, address) {
if (null === state.household) { if (null === state.household) {
@ -319,6 +367,9 @@ const store = createStore({
state.household.current_address = null; state.household.current_address = null;
}, },
markHouseholdNoAddress(state) {
state.forceHouseholdNoAddress = true;
},
forceLeaveWithoutHousehold(state) { forceLeaveWithoutHousehold(state) {
state.household = null; state.household = null;
state.mode = "leave"; state.mode = "leave";
@ -329,7 +380,7 @@ const store = createStore({
state.mode = "existing"; state.mode = "existing";
state.forceLeaveWithoutHousehold = false; state.forceLeaveWithoutHousehold = false;
}, },
setHouseholdSuggestionByAccompanyingPeriod(state, households) { addHouseholdSuggestionByAccompanyingPeriod(state, households) {
let existingIds = state.householdSuggestionByAccompanyingPeriod let existingIds = state.householdSuggestionByAccompanyingPeriod
.map(h => h.id); .map(h => h.id);
for (let i in households) { for (let i in households) {
@ -384,8 +435,8 @@ const store = createStore({
commit('removePosition', conc); commit('removePosition', conc);
dispatch('computeWarnings'); dispatch('computeWarnings');
}, },
removeConcerned({ commit, dispatch }, conc) { removePerson({ commit, dispatch }, person) {
commit('removeConcerned', conc); commit('removePerson', person);
dispatch('computeWarnings'); dispatch('computeWarnings');
dispatch('fetchAddressSuggestions'); dispatch('fetchAddressSuggestions');
}, },
@ -418,20 +469,33 @@ const store = createStore({
fetchHouseholdSuggestionForConcerned({ commit, state }, person) { fetchHouseholdSuggestionForConcerned({ commit, state }, person) {
fetchHouseholdSuggestionByAccompanyingPeriod(person.id) fetchHouseholdSuggestionByAccompanyingPeriod(person.id)
.then(households => { .then(households => {
commit('setHouseholdSuggestionByAccompanyingPeriod', households); commit('addHouseholdSuggestionByAccompanyingPeriod', households);
}); });
}, },
fetchAddressSuggestions({ commit, state }) { fetchAddressSuggestions({ commit, state, dispatch }) {
for (let i in state.concerned) { for (let i in state.concerned) {
fetchAddressSuggestionByPerson(state.concerned[i].person.id) fetchAddressSuggestionByPerson(state.concerned[i].person.id)
.then(addresses => { .then(addresses => {
commit('addAddressesSuggestion', addresses); commit('addAddressesSuggestion', addresses);
dispatch('fetchHouseholdSuggestionByAddresses', addresses);
}) })
.catch(e => { .catch(e => {
console.log(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 }) { computeWarnings({ commit, state, getters }) {
let warnings = [], let warnings = [],
payload; payload;

View File

@ -13,19 +13,19 @@
</div> </div>
<div class="tpartyparent" v-if="hasParent"> <div class="tpartyparent" v-if="hasParent">
<span class="name"> <span class="name">
{{ item.result.parent.text }} > {{ item.result.parent.text }}
</span> </span>
</div> </div>
</div> </div>
<div class="right_actions"> <div class="right_actions">
<span class="badge bg-chill-red" v-if="item.result.kind == 'child'"> <span class="badge bg-thirdparty-child" v-if="item.result.kind == 'child'">
{{ $t('thirdparty.contact')}} {{ $t('thirdparty.child')}}
</span> </span>
<span class="badge bg-info" v-else-if="item.result.kind == 'company'"> <span class="badge bg-thirdparty-company" v-else-if="item.result.kind == 'company'">
{{ $t('thirdparty.company')}} {{ $t('thirdparty.company')}}
</span> </span>
<span class="badge bg-secondary" v-else="item.result.kind == 'contact'"> <span class="badge bg-thirdparty-contact" v-else="item.result.kind == 'contact'">
{{ $t('thirdparty.contact')}} {{ $t('thirdparty.contact')}}
</span> </span>
@ -49,8 +49,8 @@ const i18n = {
messages: { messages: {
fr: { fr: {
thirdparty: { thirdparty: {
contact: "Contact", contact: "Personne physique",
company: "Institution", company: "Personne morale",
child: "Personne de contact" child: "Personne de contact"
} }
} }

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ class SearchPersonApiProvider implements SearchApiInterface
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int". "(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int".
")", [ $pattern, $pattern ]) ")", [ $pattern, $pattern ])
->setFromClause("chill_person_person AS person") ->setFromClause("chill_person_person AS person")
->setWhereClause("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ". ->setWhereClauses("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ", [ $pattern, $pattern ]) "person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ", [ $pattern, $pattern ])
; ;

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; 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\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Test\PrepareClientTrait; use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
@ -15,6 +20,8 @@ class HouseholdApiControllerTest extends WebTestCase
use PrepareClientTrait; use PrepareClientTrait;
private array $toDelete = [];
/** /**
* @dataProvider generatePersonId * @dataProvider generatePersonId
*/ */
@ -45,6 +52,77 @@ class HouseholdApiControllerTest extends WebTestCase
$this->assertResponseIsSuccessful(); $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() public function generatePersonId()
{ {
self::bootKernel(); self::bootKernel();
@ -64,7 +142,7 @@ class HouseholdApiControllerTest extends WebTestCase
; ;
$person = $period->getParticipations() $person = $period->getParticipations()
->first()->getPerson(); ->first()->getPerson();
yield [ $person->getId() ]; yield [ $person->getId() ];
} }

View File

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

@ -177,9 +177,11 @@ class PersonControllerCreateTest extends WebTestCase
$this->assertTrue($form->has(self::CENTER_INPUT), $this->assertTrue($form->has(self::CENTER_INPUT),
'The page contains a "center" input'); 'The page contains a "center" input');
$centerInput = $form->get(self::CENTER_INPUT); $centerInput = $form->get(self::CENTER_INPUT);
/*
$availableValues = $centerInput->availableOptionValues(); $availableValues = $centerInput->availableOptionValues();
$lastCenterInputValue = end($availableValues); $lastCenterInputValue = end($availableValues);
$centerInput->setValue($lastCenterInputValue); $centerInput->setValue($lastCenterInputValue);
*/
$client->submit($form); $client->submit($form);
@ -205,7 +207,7 @@ class PersonControllerCreateTest extends WebTestCase
$form = $this->fillAValidCreationForm($form, 'Charline', 'dd'); $form = $this->fillAValidCreationForm($form, 'Charline', 'dd');
$client->submit($form); $client->submit($form);
$this->assertContains('Depardieu', $client->getCrawler()->text(), $this->assertContains('DEPARDIEU', $client->getCrawler()->text(),
"check that the page has detected the lastname of a person existing in database"); "check that the page has detected the lastname of a person existing in database");
//inversion //inversion
@ -213,7 +215,7 @@ class PersonControllerCreateTest extends WebTestCase
$form = $this->fillAValidCreationForm($form, 'dd', 'Charline'); $form = $this->fillAValidCreationForm($form, 'dd', 'Charline');
$client->submit($form); $client->submit($form);
$this->assertContains('Depardieu', $client->getCrawler()->text(), $this->assertContains('DEPARDIEU', $client->getCrawler()->text(),
"check that the page has detected the lastname of a person existing in database"); "check that the page has detected the lastname of a person existing in database");
} }

View File

@ -30,38 +30,38 @@ class PersonControllerViewTest extends WebTestCase
{ {
/** @var \Doctrine\ORM\EntityManagerInterface The entity manager */ /** @var \Doctrine\ORM\EntityManagerInterface The entity manager */
private $em; private $em;
/** @var Person A person used on which to run the test */ /** @var Person A person used on which to run the test */
private $person; private $person;
/** @var String The url to view the person details */ /** @var String The url to view the person details */
private $viewUrl; private $viewUrl;
public function setUp() public function setUp()
{ {
static::bootKernel(); static::bootKernel();
$this->em = static::$kernel->getContainer() $this->em = static::$kernel->getContainer()
->get('doctrine.orm.entity_manager'); ->get('doctrine.orm.entity_manager');
$center = $this->em->getRepository('ChillMainBundle:Center') $center = $this->em->getRepository('ChillMainBundle:Center')
->findOneBy(array('name' => 'Center A')); ->findOneBy(array('name' => 'Center A'));
$this->person = (new Person()) $this->person = (new Person())
->setLastName("Tested Person") ->setLastName("Tested Person")
->setFirstName("Réginald") ->setFirstName("Réginald")
->setCenter($center) ->setCenter($center)
->setGender(Person::MALE_GENDER); ->setGender(Person::MALE_GENDER);
$this->em->persist($this->person); $this->em->persist($this->person);
$this->em->flush(); $this->em->flush();
$this->viewUrl = '/en/person/'.$this->person->getId().'/general'; $this->viewUrl = '/en/person/'.$this->person->getId().'/general';
} }
/** /**
* Test if the view page is accessible * Test if the view page is accessible
* *
* @group configurable_fields * @group configurable_fields
*/ */
public function testViewPerson() public function testViewPerson()
@ -70,20 +70,20 @@ class PersonControllerViewTest extends WebTestCase
'PHP_AUTH_USER' => 'center a_social', 'PHP_AUTH_USER' => 'center a_social',
'PHP_AUTH_PW' => 'password', 'PHP_AUTH_PW' => 'password',
)); ));
$crawler = $client->request('GET', $this->viewUrl); $crawler = $client->request('GET', $this->viewUrl);
$response = $client->getResponse(); $response = $client->getResponse();
$this->assertTrue($response->isSuccessful()); $this->assertTrue($response->isSuccessful());
$this->assertGreaterThan(0, $crawler->filter('html:contains("Tested Person")')->count()); $this->assertGreaterThan(0, $crawler->filter('html:contains("TESTED PERSON")')->count());
$this->assertGreaterThan(0, $crawler->filter('html:contains("Réginald")')->count()); $this->assertGreaterThan(0, $crawler->filter('html:contains("Réginald")')->count());
$this->assertContains('Email addresses', $crawler->text()); $this->assertContains('Email addresses', $crawler->text());
$this->assertContains('Phonenumber', $crawler->text()); $this->assertContains('Phonenumber', $crawler->text());
$this->assertContains('Langues parlées', $crawler->text()); $this->assertContains('Langues parlées', $crawler->text());
$this->assertContains(/* Etat */ 'civil', $crawler->text()); $this->assertContains(/* Etat */ 'civil', $crawler->text());
} }
/** /**
* Test if the view page of a given person is not accessible for a user * Test if the view page of a given person is not accessible for a user
* of another center of the person * of another center of the person
@ -94,26 +94,26 @@ class PersonControllerViewTest extends WebTestCase
'PHP_AUTH_USER' => 'center b_social', 'PHP_AUTH_USER' => 'center b_social',
'PHP_AUTH_PW' => 'password', 'PHP_AUTH_PW' => 'password',
)); ));
$client->request('GET', $this->viewUrl); $client->request('GET', $this->viewUrl);
$this->assertEquals(403, $client->getResponse()->getStatusCode(), $this->assertEquals(403, $client->getResponse()->getStatusCode(),
"The view page of a person of a center A must not be accessible for user of center B"); "The view page of a person of a center A must not be accessible for user of center B");
} }
/** /**
* Reload the person from the db * Reload the person from the db
*/ */
protected function refreshPerson() protected function refreshPerson()
{ {
$this->person = $this->em->getRepository('ChillPersonBundle:Person') $this->person = $this->em->getRepository('ChillPersonBundle:Person')
->find($this->person->getId()); ->find($this->person->getId());
} }
public function tearDown() public function tearDown()
{ {
$this->refreshPerson(); $this->refreshPerson();
$this->em->remove($this->person); $this->em->remove($this->person);
$this->em->flush(); $this->em->flush();
} }
} }

View File

@ -26,7 +26,6 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/** /**
* Test Person search * Test Person search
* *
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/ */
class PersonSearchTest extends WebTestCase class PersonSearchTest extends WebTestCase
{ {
@ -38,7 +37,7 @@ class PersonSearchTest extends WebTestCase
'q' => '@person Depardieu' 'q' => '@person Depardieu'
)); ));
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testExpectedNamed() public function testExpectedNamed()
@ -49,61 +48,61 @@ class PersonSearchTest extends WebTestCase
'q' => '@person Depardieu', 'name' => 'person_regular' 'q' => '@person Depardieu', 'name' => 'person_regular'
)); ));
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testSearchByLastName() public function testSearchByLastName()
{ {
$crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu'); $crawler = $this->generateCrawlerForSearch('@person lastname:Depardieu');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testSearchByFirstNameLower() public function testSearchByFirstNameLower()
{ {
$crawler = $this->generateCrawlerForSearch('@person firstname:Gérard'); $crawler = $this->generateCrawlerForSearch('@person firstname:Gérard');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testSearchByFirstNamePartim() public function testSearchByFirstNamePartim()
{ {
$crawler = $this->generateCrawlerForSearch('@person firstname:Ger'); $crawler = $this->generateCrawlerForSearch('@person firstname:Ger');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testLastNameAccentued() public function testLastNameAccentued()
{ {
$crawlerSpecial = $this->generateCrawlerForSearch('@person lastname:manço'); $crawlerSpecial = $this->generateCrawlerForSearch('@person lastname:manço');
$this->assertRegExp('/Manço/', $crawlerSpecial->filter('.list-with-period')->text()); $this->assertRegExp('/MANÇO/', $crawlerSpecial->filter('.list-with-period')->text());
$crawlerNoSpecial = $this->generateCrawlerForSearch('@person lastname:manco'); $crawlerNoSpecial = $this->generateCrawlerForSearch('@person lastname:manco');
$this->assertRegExp('/Manço/', $crawlerNoSpecial->filter('.list-with-period')->text()); $this->assertRegExp('/MANÇO/', $crawlerNoSpecial->filter('.list-with-period')->text());
} }
public function testSearchByFirstName() public function testSearchByFirstName()
{ {
$crawler = $this->generateCrawlerForSearch('@person firstname:Jean'); $crawler = $this->generateCrawlerForSearch('@person firstname:Jean');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testSearchByFirstNameLower2() public function testSearchByFirstNameLower2()
{ {
$crawler = $this->generateCrawlerForSearch('@person firstname:jean'); $crawler = $this->generateCrawlerForSearch('@person firstname:jean');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testSearchByFirstNamePartim2() public function testSearchByFirstNamePartim2()
{ {
$crawler = $this->generateCrawlerForSearch('@person firstname:ean'); $crawler = $this->generateCrawlerForSearch('@person firstname:ean');
$this->assertRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testSearchByFirstNameAccented() public function testSearchByFirstNameAccented()
@ -154,7 +153,7 @@ class PersonSearchTest extends WebTestCase
$crawler = $this->generateCrawlerForSearch('@person birthdate:1948-12-27 lastname:(Van Snick)'); $crawler = $this->generateCrawlerForSearch('@person birthdate:1948-12-27 lastname:(Van Snick)');
$this->assertRegExp('/Bart/', $crawler->filter('.list-with-period')->text()); $this->assertRegExp('/Bart/', $crawler->filter('.list-with-period')->text());
$this->assertNotRegExp('/Depardieu/', $crawler->filter('.list-with-period')->text()); $this->assertNotRegExp('/DEPARDIEU/', $crawler->filter('.list-with-period')->text());
} }
public function testSearchCombineGenderAndLastName() public function testSearchCombineGenderAndLastName()
@ -181,12 +180,12 @@ class PersonSearchTest extends WebTestCase
$this->markTestSkipped("skipped until adapted to new fixtures"); $this->markTestSkipped("skipped until adapted to new fixtures");
$crawlerSpecial = $this->generateCrawlerForSearch('@person manço'); $crawlerSpecial = $this->generateCrawlerForSearch('@person manço');
$this->assertRegExp('/Manço/', $crawlerSpecial->filter('.list-with-period')->text()); $this->assertRegExp('/MANÇO/', $crawlerSpecial->filter('.list-with-period')->text());
$crawlerNoSpecial = $this->generateCrawlerForSearch('@person manco'); $crawlerNoSpecial = $this->generateCrawlerForSearch('@person manco');
$this->assertRegExp('/Manço/', $crawlerNoSpecial->filter('.list-with-period')->text()); $this->assertRegExp('/MANÇO/', $crawlerNoSpecial->filter('.list-with-period')->text());
$crawlerSpecial = $this->generateCrawlerForSearch('@person Étienne'); $crawlerSpecial = $this->generateCrawlerForSearch('@person Étienne');
@ -206,10 +205,10 @@ class PersonSearchTest extends WebTestCase
$crawlerCanSee = $this->generateCrawlerForSearch('Gérard', 'center a_social'); $crawlerCanSee = $this->generateCrawlerForSearch('Gérard', 'center a_social');
$crawlerCannotSee = $this->generateCrawlerForSearch('Gérard', 'center b_social'); $crawlerCannotSee = $this->generateCrawlerForSearch('Gérard', 'center b_social');
$this->assertRegExp('/Depardieu/', $crawlerCanSee->text(), $this->assertRegExp('/DEPARDIEU/', $crawlerCanSee->text(),
'center a_social may see "Depardieu" in center a'); 'center a_social may see "Depardieu" in center a');
$this->assertNotRegExp('/Depardieu/', $crawlerCannotSee->text(), $this->assertNotRegExp('/DEPARDIEU/', $crawlerCannotSee->text(),
'center b_social may see "Depardieu" in center b'); 'center b_social may not see "Depardieu" in center b');
} }

View File

@ -1127,6 +1127,32 @@ paths:
401: 401:
description: "Unauthorized" 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: /1.0/person/household/suggest/by-person/{person_id}/through-accompanying-period-participation.json:
get: get:
tags: tags:

View File

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

View File

@ -1,20 +1,20 @@
<?php <?php
/* /*
* Chill is a software for social workers * Chill is a software for social workers
* *
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop> * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
@ -35,11 +35,9 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
{ {
return 15001; return 15001;
} }
public function load(ObjectManager $manager) public function load(ObjectManager $manager)
{ {
echo "loading CustomField...\n";
$cFTypes = [ $cFTypes = [
array('type' => 'text', 'options' => array('maxLength' => '255')), array('type' => 'text', 'options' => array('maxLength' => '255')),
array('type' => 'text', 'options' => array('maxLength' => '1000')), array('type' => 'text', 'options' => array('maxLength' => '1000')),
@ -78,7 +76,6 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
]; ];
for($i=0; $i <= 25; $i++) { for($i=0; $i <= 25; $i++) {
echo "CustomField {$i}\n";
$cFType = $cFTypes[rand(0,sizeof($cFTypes) - 1)]; $cFType = $cFTypes[rand(0,sizeof($cFTypes) - 1)];
$customField = (new CustomField()) $customField = (new CustomField())
@ -92,17 +89,17 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($customField); $manager->persist($customField);
} }
$this->createExpectedFields($manager); $this->createExpectedFields($manager);
$manager->flush(); $manager->flush();
} }
private function createExpectedFields(ObjectManager $manager) private function createExpectedFields(ObjectManager $manager)
{ {
//report logement //report logement
$reportLogement = $this->getReference('cf_group_report_logement'); $reportLogement = $this->getReference('cf_group_report_logement');
$houseTitle = (new CustomField()) $houseTitle = (new CustomField())
->setSlug('house_title') ->setSlug('house_title')
->setType('title') ->setType('title')
@ -112,7 +109,7 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
->setCustomFieldsGroup($reportLogement) ->setCustomFieldsGroup($reportLogement)
; ;
$manager->persist($houseTitle); $manager->persist($houseTitle);
$hasLogement = (new CustomField()) $hasLogement = (new CustomField())
->setSlug('has_logement') ->setSlug('has_logement')
->setName(array('fr' => 'Logement actuel')) ->setName(array('fr' => 'Logement actuel'))
@ -143,13 +140,13 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
'active' => true 'active' => true
) )
] ]
)) ))
->setOrdering(20) ->setOrdering(20)
->setCustomFieldsGroup($reportLogement) ->setCustomFieldsGroup($reportLogement)
; ;
$manager->persist($hasLogement); $manager->persist($hasLogement);
$descriptionLogement = (new CustomField()) $descriptionLogement = (new CustomField())
->setSlug('house-desc') ->setSlug('house-desc')
->setName(array('fr' => 'Plaintes éventuelles sur le logement')) ->setName(array('fr' => 'Plaintes éventuelles sur le logement'))
@ -159,11 +156,11 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
->setCustomFieldsGroup($reportLogement) ->setCustomFieldsGroup($reportLogement)
; ;
$manager->persist($descriptionLogement); $manager->persist($descriptionLogement);
//report problems //report problems
$reportEducation = $this->getReference('cf_group_report_education'); $reportEducation = $this->getReference('cf_group_report_education');
$title = (new CustomField()) $title = (new CustomField())
->setSlug('title') ->setSlug('title')
->setType('title') ->setType('title')
@ -173,7 +170,7 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
->setCustomFieldsGroup($reportEducation) ->setCustomFieldsGroup($reportEducation)
; ;
$manager->persist($title); $manager->persist($title);
$educationLevel = (new CustomField()) $educationLevel = (new CustomField())
->setSlug('level') ->setSlug('level')
->setName(array('fr' => 'Niveau du plus haut diplôme')) ->setName(array('fr' => 'Niveau du plus haut diplôme'))
@ -209,14 +206,14 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
'active' => true 'active' => true
) )
] ]
)) ))
->setOrdering(20) ->setOrdering(20)
->setCustomFieldsGroup($reportEducation) ->setCustomFieldsGroup($reportEducation)
; ;
$manager->persist($educationLevel); $manager->persist($educationLevel);
} }
} }

View File

@ -3,17 +3,17 @@
/* /*
* Chill is a suite of a modules, Chill is a software for social workers * Chill is a suite of a modules, Chill is a software for social workers
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop> * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
@ -38,43 +38,43 @@ use Chill\MainBundle\DataFixtures\ORM\LoadScopes;
class LoadReports extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface class LoadReports extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
{ {
use \Symfony\Component\DependencyInjection\ContainerAwareTrait; use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
/** /**
* *
* @var \Faker\Generator * @var \Faker\Generator
*/ */
private $faker; private $faker;
public function __construct() public function __construct()
{ {
$this->faker = FakerFactory::create('fr_FR'); $this->faker = FakerFactory::create('fr_FR');
} }
public function getOrder() public function getOrder()
{ {
return 15002; return 15002;
} }
public function load(ObjectManager $manager) public function load(ObjectManager $manager)
{ {
$this->createExpected($manager); $this->createExpected($manager);
//create random 2 times, to allow multiple report on some people //create random 2 times, to allow multiple report on some people
$this->createRandom($manager, 90); $this->createRandom($manager, 90);
$this->createRandom($manager, 30); $this->createRandom($manager, 30);
$manager->flush(); $manager->flush();
} }
private function createRandom(ObjectManager $manager, $percentage) private function createRandom(ObjectManager $manager, $percentage)
{ {
$people = $this->getPeopleRandom($percentage); $people = $this->getPeopleRandom($percentage);
foreach ($people as $person) { foreach ($people as $person) {
//create a report, set logement or education report //create a report, set logement or education report
$report = (new Report()) $report = (new Report())
->setPerson($person) ->setPerson($person)
->setCFGroup(rand(0,10) > 5 ? ->setCFGroup(rand(0,10) > 5 ?
$this->getReference('cf_group_report_logement') : $this->getReference('cf_group_report_logement') :
$this->getReference('cf_group_report_education') $this->getReference('cf_group_report_education')
) )
@ -84,27 +84,31 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
$manager->persist($report); $manager->persist($report);
} }
} }
private function createExpected(ObjectManager $manager) private function createExpected(ObjectManager $manager)
{ {
$charline = $this->container->get('doctrine.orm.entity_manager') $charline = $this->container->get('doctrine.orm.entity_manager')
->getRepository('ChillPersonBundle:Person') ->getRepository('ChillPersonBundle:Person')
->findOneBy(array('firstName' => 'Charline', 'lastName' => 'Depardieu')) ->findOneBy(array('firstName' => 'Charline', 'lastName' => 'DEPARDIEU'))
; ;
$report = (new Report()) if (NULL !== $charline) {
$report = (new Report())
->setPerson($charline) ->setPerson($charline)
->setCFGroup($this->getReference('cf_group_report_logement')) ->setCFGroup($this->getReference('cf_group_report_logement'))
->setDate(new \DateTime('2015-01-05')) ->setDate(new \DateTime('2015-01-05'))
->setScope($this->getReference('scope_social')) ->setScope($this->getReference('scope_social'))
; ;
$this->fillReport($report); $this->fillReport($report);
$manager->persist($report); $manager->persist($report);
} else {
print("WARNING: Charline DEPARDIEU not found in database");
}
} }
/** /**
* *
* @return \Chill\MainBundle\Entity\Scope * @return \Chill\MainBundle\Entity\Scope
*/ */
private function getScopeRandom() private function getScopeRandom()
@ -112,14 +116,14 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
$ref = LoadScopes::$references[array_rand(LoadScopes::$references)]; $ref = LoadScopes::$references[array_rand(LoadScopes::$references)];
return $this->getReference($ref); return $this->getReference($ref);
} }
private function getPeopleRandom($percentage) private function getPeopleRandom($percentage)
{ {
$people = $this->container->get('doctrine.orm.entity_manager') $people = $this->container->get('doctrine.orm.entity_manager')
->getRepository('ChillPersonBundle:Person') ->getRepository('ChillPersonBundle:Person')
->findAll() ->findAll()
; ;
//keep only a part ($percentage) of the people //keep only a part ($percentage) of the people
$selectedPeople = array(); $selectedPeople = array();
foreach($people as $person) { foreach($people as $person) {
@ -127,10 +131,10 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
$selectedPeople[] = $person; $selectedPeople[] = $person;
} }
} }
return $selectedPeople; return $selectedPeople;
} }
private function fillReport(Report $report) private function fillReport(Report $report)
{ {
//setUser //setUser
@ -138,7 +142,7 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
$report->setUser( $report->setUser(
$this->getReference($usernameRef) $this->getReference($usernameRef)
); );
//set date if null //set date if null
if ($report->getDate() === NULL) { if ($report->getDate() === NULL) {
//set date. 30% of the dates are 2015-05-01 //set date. 30% of the dates are 2015-05-01
@ -148,9 +152,9 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
} else { } else {
$report->setDate($this->faker->dateTimeBetween('-1 year', 'now') $report->setDate($this->faker->dateTimeBetween('-1 year', 'now')
->setTime(0, 0, 0)); ->setTime(0, 0, 0));
} }
} }
//fill data //fill data
$datas = array(); $datas = array();
foreach ($report->getCFGroup()->getCustomFields() as $field) { foreach ($report->getCFGroup()->getCustomFields() as $field) {
@ -167,66 +171,66 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
} }
} }
$report->setCFData($datas); $report->setCFData($datas);
return $report; return $report;
} }
/** /**
* pick a random choice * pick a random choice
* *
* @param CustomField $field * @param CustomField $field
* @return string[]|string the array of slug if multiple, a single slug otherwise * @return string[]|string the array of slug if multiple, a single slug otherwise
*/ */
private function getRandomChoice(CustomField $field) private function getRandomChoice(CustomField $field)
{ {
$choices = $field->getOptions()['choices']; $choices = $field->getOptions()['choices'];
$multiple = $field->getOptions()['multiple']; $multiple = $field->getOptions()['multiple'];
$other = $field->getOptions()['other']; $other = $field->getOptions()['other'];
//add other if allowed //add other if allowed
if($other) { if($other) {
$choices[] = array('slug' => '_other'); $choices[] = array('slug' => '_other');
} }
//initialize results //initialize results
$picked = array(); $picked = array();
if ($multiple) { if ($multiple) {
$numberSelected = rand(1, count($choices) -1); $numberSelected = rand(1, count($choices) -1);
for ($i = 0; $i < $numberSelected; $i++) { for ($i = 0; $i < $numberSelected; $i++) {
$picked[] = $this->pickChoice($choices); $picked[] = $this->pickChoice($choices);
} }
if ($other) { if ($other) {
$result = array("_other" => NULL, "_choices" => $picked); $result = array("_other" => NULL, "_choices" => $picked);
if (in_array('_other', $picked)) { if (in_array('_other', $picked)) {
$result['_other'] = $this->faker->realText(70); $result['_other'] = $this->faker->realText(70);
} }
return $result; return $result;
} }
} else { } else {
$picked = $this->pickChoice($choices); $picked = $this->pickChoice($choices);
if ($other) { if ($other) {
$result = array('_other' => NULL, '_choices' => $picked); $result = array('_other' => NULL, '_choices' => $picked);
if ($picked === '_other') { if ($picked === '_other') {
$result['_other'] = $this->faker->realText(70); $result['_other'] = $this->faker->realText(70);
} }
return $result; return $result;
} }
} }
} }
/** /**
* pick a choice within a 'choices' options (for choice type) * pick a choice within a 'choices' options (for choice type)
* *
* @param array $choices * @param array $choices
* @return the slug of the selected choice * @return the slug of the selected choice
*/ */
@ -234,7 +238,7 @@ class LoadReports extends AbstractFixture implements OrderedFixtureInterface, Co
{ {
return $choices[array_rand($choices)]['slug']; return $choices[array_rand($choices)]['slug'];
} }
} }

View File

@ -2,6 +2,7 @@
namespace Chill\ThirdPartyBundle; namespace Chill\ThirdPartyBundle;
use Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeProviderInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
use Chill\ThirdPartyBundle\DependencyInjection\CompilerPass\ThirdPartyTypeCompilerPass; use Chill\ThirdPartyBundle\DependencyInjection\CompilerPass\ThirdPartyTypeCompilerPass;
@ -10,6 +11,8 @@ class ChillThirdPartyBundle extends Bundle
public function build(\Symfony\Component\DependencyInjection\ContainerBuilder $container) public function build(\Symfony\Component\DependencyInjection\ContainerBuilder $container)
{ {
parent::build($container); parent::build($container);
$container->registerForAutoconfiguration(ThirdPartyTypeProviderInterface::class)
->addTag('chill_3party.provider');
$container->addCompilerPass(new ThirdPartyTypeCompilerPass()); $container->addCompilerPass(new ThirdPartyTypeCompilerPass());
} }

View File

@ -127,6 +127,8 @@ final class ThirdPartyController extends CRUDController
return $this->getFilterOrderHelperFactory() return $this->getFilterOrderHelperFactory()
->create(self::class) ->create(self::class)
->addSearchBox(['name', 'company_name', 'acronym']) ->addSearchBox(['name', 'company_name', 'acronym'])
//->addToggle('only-active', [])
// ->addOrderBy()
->build(); ->build();
} }
} }

View File

@ -368,7 +368,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
public function setTypes(array $type = null) public function setTypes(array $type = null)
{ {
// remove all keys from the input data // remove all keys from the input data
$this->type = \array_values($type); $this->types = \array_values($type);
foreach ($this->children as $child) { foreach ($this->children as $child) {
$child->setTypes($type); $child->setTypes($type);
@ -387,6 +387,40 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
return $this->types; return $this->types;
} }
public function addType(?string $type): self
{
if (NULL === $type) {
return $this;
}
if (!\in_array($type, $this->types ?? [])) {
$this->types[] = $type;
}
foreach ($this->children as $child) {
$child->addType($type);
}
return $this;
}
public function removeType(?string $type): self
{
if (NULL === $type) {
return $this;
}
if (\in_array($type, $this->types ?? [])) {
$this->types = \array_filter($this->types, fn($e) => !\in_array($e, $this->types));
}
foreach ($this->children as $child) {
$child->removeType($type);
}
return $this;
}
/** /**
* @return bool * @return bool
*/ */
@ -460,6 +494,10 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
*/ */
public function getAddress(): ?Address public function getAddress(): ?Address
{ {
if ($this->isChild()) {
return $this->getParent()->getAddress();
}
return $this->address; return $this->address;
} }
@ -512,9 +550,9 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
* @param string $acronym * @param string $acronym
* @return $this * @return $this
*/ */
public function setAcronym(string $acronym): ThirdParty public function setAcronym(?string $acronym = null): ThirdParty
{ {
$this->acronym = $acronym; $this->acronym = (string) $acronym;
return $this; return $this;
} }
@ -537,7 +575,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
} }
foreach ($this->children as $child) { foreach ($this->children as $child) {
$child->addCategory($child); $child->addCategory($category);
} }
return $this; return $this;
@ -552,7 +590,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
$this->categories->removeElement($category); $this->categories->removeElement($category);
foreach ($this->children as $child) { foreach ($this->children as $child) {
$child->removeCategory($child); $child->removeCategory($category);
} }
return $this; return $this;
@ -627,6 +665,72 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
return $this; return $this;
} }
public function addTypesAndCategories($typeAndCategory): self
{
if ($typeAndCategory instanceof ThirdPartyCategory) {
$this->addCategory($typeAndCategory);
return $this;
}
if (is_string($typeAndCategory)) {
$this->addType($typeAndCategory);
return $this;
}
throw new \UnexpectedValueException(sprintf(
"typeAndCategory should be a string or a %s", ThirdPartyCategory::class));
}
public function removeTypesAndCategories($typeAndCategory): self
{
if ($typeAndCategory instanceof ThirdPartyCategory) {
$this->removeCategory($typeAndCategory);
return $this;
}
if (is_string($typeAndCategory)) {
$this->removeType($typeAndCategory);
return $this;
}
throw new \UnexpectedValueException(sprintf(
"typeAndCategory should be a string or a %s", ThirdPartyCategory::class));
}
public function getTypesAndCategories(): array
{
return \array_merge(
$this->getCategories()->toArray(),
$this->getTypes() ?? []
);
}
public function setTypesAndCategories(array $typesAndCategories): self
{
$types = \array_filter($typesAndCategories, fn($item) => !$item instanceof ThirdPartyCategory);
$this->setTypes($types);
// handle categories
foreach ($typesAndCategories as $t) {
$this->addTypesAndCategories($t);
}
$categories = \array_filter($typesAndCategories, fn($item) => $item instanceof ThirdPartyCategory);
$categoriesHashes = \array_map(fn(ThirdPartyCategory $c) => \spl_object_hash($c), $categories);
foreach ($categories as $c) {
$this->addCategory($c);
}
foreach ($this->getCategories() as $t) {
if (!\in_array(\spl_object_hash($t), $categoriesHashes)) {
$this->removeCategory($t);
}
}
return $this;
}
/** /**
* @param ThirdParty $child * @param ThirdParty $child

View File

@ -5,15 +5,19 @@ namespace Chill\ThirdPartyBundle\Form;
use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Civility; use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\PickAddressType;
use Chill\MainBundle\Form\Type\PickCenterType; use Chill\MainBundle\Form\Type\PickCenterType;
use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory; use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use Chill\ThirdPartyBundle\Entity\ThirdPartyProfession; use Chill\ThirdPartyBundle\Entity\ThirdPartyProfession;
use Chill\ThirdPartyBundle\Form\Type\PickThirdPartyType;
use Chill\ThirdPartyBundle\Form\Type\PickThirdPartyTypeCategoryType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@ -40,14 +44,14 @@ class ThirdPartyType extends AbstractType
protected TranslatableStringHelper $translatableStringHelper; protected TranslatableStringHelper $translatableStringHelper;
protected ObjectManager $om; protected EntityManagerInterface $om;
public function __construct( public function __construct(
AuthorizationHelper $authorizationHelper, AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage, TokenStorageInterface $tokenStorage,
ThirdPartyTypeManager $typesManager, ThirdPartyTypeManager $typesManager,
TranslatableStringHelper $translatableStringHelper, TranslatableStringHelper $translatableStringHelper,
ObjectManager $om EntityManagerInterface $om
) { ) {
$this->authorizationHelper = $authorizationHelper; $this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
@ -85,27 +89,6 @@ class ThirdPartyType extends AbstractType
]) ])
; ;
$builder
->add('address', HiddenType::class)
->get('address')
->addModelTransformer(new CallbackTransformer(
function (?Address $address): string {
if (null === $address) {
return '';
}
return $address->getId();
},
function (?string $addressId): ?Address {
if (null === $addressId) {
return null;
}
return $this->om
->getRepository(Address::class)
->findOneBy(['id' => (int) $addressId]);
}
))
;
// Contact Person ThirdParty (child) // Contact Person ThirdParty (child)
if (ThirdParty::KIND_CONTACT === $options['kind'] || ThirdParty::KIND_CHILD === $options['kind']) { if (ThirdParty::KIND_CONTACT === $options['kind'] || ThirdParty::KIND_CHILD === $options['kind']) {
$builder $builder
@ -144,6 +127,15 @@ class ThirdPartyType extends AbstractType
// Institutional ThirdParty (parent) // Institutional ThirdParty (parent)
} else { } else {
$builder $builder
->add('address', PickAddressType::class, [
'label' => 'Address'
])
->add('address2', PickAddressType::class, [
'label' => 'Address',
'use_valid_from' => true,
'use_valid_to' => true,
'mapped' => false,
])
->add('nameCompany', TextType::class, [ ->add('nameCompany', TextType::class, [
'label' => 'thirdparty.NameCompany', 'label' => 'thirdparty.NameCompany',
'required' => false 'required' => false
@ -171,21 +163,9 @@ class ThirdPartyType extends AbstractType
} }
if (ThirdParty::KIND_CHILD !== $options['kind']) { if (ThirdParty::KIND_CHILD !== $options['kind']) {
$builder $builder
->add('categories', EntityType::class, [ ->add('typesAndCategories', PickThirdPartyTypeCategoryType::class, [
'label' => 'thirdparty.Categories', 'label' => 'thirdparty.Categories'
'class' => ThirdPartyCategory::class,
'choice_label' => function (ThirdPartyCategory $category): string {
return $this->translatableStringHelper->localize($category->getName());
},
'query_builder' => function (EntityRepository $er): QueryBuilder {
return $er->createQueryBuilder('c')
->where('c.active = true');
},
'required' => true,
'multiple' => true,
'attr' => ['class' => 'select2']
]) ])
->add('active', ChoiceType::class, [ ->add('active', ChoiceType::class, [
'label' => 'thirdparty.Status', 'label' => 'thirdparty.Status',
@ -196,42 +176,6 @@ class ThirdPartyType extends AbstractType
'expanded' => true, 'expanded' => true,
'multiple' => false 'multiple' => false
]); ]);
// add the types
$types = [];
foreach ($this->typesManager->getProviders() as $key => $provider) {
$types['chill_3party.key_label.'.$key] = $key;
}
if (count($types) === 1) {
$builder
->add('types', HiddenType::class, [
'data' => array_values($types)
])
->get('types')
->addModelTransformer(new CallbackTransformer(
function (?array $typeArray): ?string {
if (null === $typeArray) {
return null;
}
return implode(',', $typeArray);
},
function (?string $typeStr): ?array {
if (null === $typeStr) {
return null;
}
return explode(',', $typeStr);
}
))
;
} else {
$builder
->add('types', ChoiceType::class, [
'choices' => $types,
'expanded' => true,
'multiple' => true,
'label' => 'thirdparty.Type'
]);
}
} }
} }

View File

@ -0,0 +1,100 @@
<?php
namespace Chill\ThirdPartyBundle\Form\Type;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use Chill\ThirdPartyBundle\Repository\ThirdPartyCategoryRepository;
use Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class PickThirdPartyTypeCategoryType extends \Symfony\Component\Form\AbstractType
{
private ThirdPartyCategoryRepository $thirdPartyCategoryRepository;
private ThirdPartyTypeManager $thirdPartyTypeManager;
private TranslatableStringHelper $translatableStringHelper;
private TranslatorInterface $translator;
private const PREFIX_TYPE = 'chill_3party.key_label.';
public function __construct(
ThirdPartyCategoryRepository $thirdPartyCategoryRepository,
ThirdPartyTypeManager $thirdPartyTypeManager,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator
) {
$this->thirdPartyCategoryRepository = $thirdPartyCategoryRepository;
$this->thirdPartyTypeManager = $thirdPartyTypeManager;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$choices = \array_merge(
$this->thirdPartyCategoryRepository->findBy(['active' => true]),
$this->thirdPartyTypeManager->getTypes()
);
\uasort($choices, function ($itemA, $itemB) {
$strA = $itemA instanceof ThirdPartyCategory ? $this->translatableStringHelper
->localize($itemA->getName()) : $this->translator->trans(self::PREFIX_TYPE.$itemA);
$strB = $itemB instanceof ThirdPartyCategory ? $this->translatableStringHelper
->localize($itemB->getName()) : $this->translator->trans(self::PREFIX_TYPE.$itemB);
return $strA <=> $strB;
});
$resolver->setDefaults([
'choices' => $choices,
'attr' => [ 'class' => 'select2' ],
'multiple' => true,
'choice_label' => function($item) {
if ($item instanceof ThirdPartyCategory) {
return $this->translatableStringHelper->localize($item->getName());
}
return self::PREFIX_TYPE.$item;
},
'choice_value' => function($item) {
return $this->reverseTransform($item);
}
]);
}
public function reverseTransform($value)
{
if ($value === null) {
return null;
}
if (is_array($value)){
$r = [];
foreach ($value as $v) {
$r[] = $this->transform($v);
}
return $r;
}
if ($value instanceof ThirdPartyCategory) {
return 'category:'.$value->getId();
}
if (is_string($value)) {
return 'type:'.$value;
}
throw new UnexpectedTypeException($value, \implode(' or ', ['array', 'string', ThirdPartyCategory::class]));
}
}

View File

@ -0,0 +1,16 @@
@import 'ChillMainAssets/module/bootstrap/shared';
.badge {
&.bg-thirdparty-company {
//@extend .bg-info;
background-color: $yellow;
}
&.bg-thirdparty-child {
//@extend .bg-chill-blue;
background-color: $chill-blue;
}
&.bg-thirdparty-contact {
//@extedn .bg-secondary;
background-color: $secondary;
}
}

View File

@ -0,0 +1 @@
require('./chillthirdparty.scss');

View File

@ -1,2 +1,2 @@
require('./chillthirdparty.scss'); require('./index_3party.scss');

View File

@ -13,16 +13,33 @@
</a> </a>
<span class="name">{{ thirdparty.text }}</span> <span class="name">{{ thirdparty.text }}</span>
<span class="badge bg-thirdparty-child" v-if="thirdparty.kind == 'child'">
{{ $t('thirdparty.child')}}
</span>
<span class="badge bg-thirdparty-company" v-else-if="thirdparty.kind == 'company'">
{{ $t('thirdparty.company')}}
</span>
<span class="badge bg-thirdparty-contact" v-else="thirdparty.kind == 'contact'">
{{ $t('thirdparty.contact')}}
</span>
<span v-if="options.addId == true" class="id-number" :title="'n° ' + thirdparty.id">{{ thirdparty.id }}</span> <span v-if="options.addId == true" class="id-number" :title="'n° ' + thirdparty.id">{{ thirdparty.id }}</span>
<span v-if="options.addEntity == true && thirdparty.type === 'thirdparty'" class="badge rounded-pill bg-secondary">{{ $t('renderbox.type.thirdparty') }}</span> <span v-if="options.addEntity == true && thirdparty.type === 'thirdparty'" class="badge rounded-pill bg-secondary">{{ $t('renderbox.type.thirdparty') }}</span>
</div> </div>
<div v-if="hasParent">
<span class="name tparty-parent">
> {{ thirdparty.parent.text }}
</span>
</div>
<p v-if="this.options.addInfo === true" class="moreinfo"> <p v-if="this.options.addInfo === true" class="moreinfo">
</p> </p>
</div> </div>
</div> </div>
<div class="item-col"> <div class="item-col">
<div class="float-button bottom"> <div class="float-button bottom">
<div class="box"> <div class="box">
@ -57,19 +74,34 @@
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue'; import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import {dateToISO} from 'ChillMainAssets/chill/js/date.js'; import {dateToISO} from 'ChillMainAssets/chill/js/date.js';
const i18n = {
messages: {
fr: {
tparty: {
contact: "Personne physique",
company: "Personne morale"
}
}
}
};
export default { export default {
name: "ThirdPartyRenderBox", name: "ThirdPartyRenderBox",
components: { components: {
AddressRenderBox AddressRenderBox
}, },
i18n,
props: ['thirdparty', 'options'], props: ['thirdparty', 'options'],
computed: { computed: {
isMultiline: function() { isMultiline: function() {
if(this.options.isMultiline){ if (this.options.isMultiline){
return this.options.isMultiline return this.options.isMultiline
} else { } else {
return false return false
} }
},
hasParent() {
return !(this.$props.thirdparty.parent === null || this.$props.thirdparty.parent === undefined);
} }
} }
} }
@ -80,6 +112,10 @@ export default {
&:before{ &:before{
content: " " content: " "
} }
&.tparty-parent {
font-weight: bold;
font-variant: all-small-caps;
}
} }
</style> </style>

View File

@ -20,32 +20,55 @@
</div> </div>
<div v-else-if="action === 'edit' || action === 'create'"> <div v-else-if="action === 'edit' || action === 'create'">
<div class="form-floating mb-3"> <div class="form-floating mb-3" v-if="thirdparty.kind !== 'child'">
<div class="form-check"> <div class="form-check">
<input class="form-check-input mt-0" type="radio" v-model="kind" value="company" id="tpartyKindInstitution"> <input class="form-check-input mt-0" type="radio" v-model="kind" value="company" id="tpartyKindInstitution">
<label for="tpartyKindInstitution" class="required"> <label for="tpartyKindInstitution" class="required">
{{ $t('tparty.company')}} <span class="badge bg-thirdparty-company" style="padding-top: 0;">
{{ $t('tparty.company')}}
</span>
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input mt-0" type="radio" v-model="kind" value="contact" id="tpartyKindContact"> <input class="form-check-input mt-0" type="radio" v-model="kind" value="contact" id="tpartyKindContact">
<label for="tpartyKindContact" class="required"> <label for="tpartyKindContact" class="required">
{{ $t('tparty.contact')}} <span class="badge bg-thirdparty-contact" style="padding-top: 0;">
{{ $t('tparty.contact')}}
</span>
</label> </label>
</div> </div>
</div> </div>
<div v-else>
<p>Contact de&nbsp;:</p>
<third-party-render-box :thirdparty="thirdparty.parent"
:options="{
addInfo: true,
addEntity: false,
addAltNames: true,
addId: false,
addLink: false,
addAge: false,
hLevel: 4,
addCenter: false,
addNoData: true,
isMultiline: false
}"></third-party-render-box>
</div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input class="form-control form-control-lg" id="name" v-model="thirdparty.text" v-bind:placeholder="$t('thirdparty.name')" /> <input class="form-control form-control-lg" id="name" v-model="thirdparty.text" v-bind:placeholder="$t('thirdparty.name')" />
<label for="name">{{ $t('thirdparty.name') }}</label> <label for="name">{{ $t('thirdparty.name') }}</label>
</div> </div>
<add-address <template
key="thirdparty" v-if="thirdparty.kind !== 'child'">
:context="context" <add-address
:options="addAddress.options" key="thirdparty"
:address-changed-callback="submitAddress" :context="context"
ref="addAddress"> :options="addAddress.options"
</add-address> :address-changed-callback="submitAddress"
ref="addAddress">
</add-address>
</template>
<div class="input-group mb-3"> <div class="input-group mb-3">
<span class="input-group-text" id="email"><i class="fa fa-fw fa-envelope"></i></span> <span class="input-group-text" id="email"><i class="fa fa-fw fa-envelope"></i></span>
@ -77,8 +100,8 @@ const i18n = {
messages: { messages: {
fr: { fr: {
tparty: { tparty: {
contact: "Contact", contact: "Personne physique",
company: "Institution" company: "Personne morale"
} }
} }
} }
@ -136,7 +159,7 @@ export default {
edit: false, edit: false,
addressId: null addressId: null
}; };
if ( typeof this.thirdparty.address !== 'undefined' if ( !(this.thirdparty.address === undefined || this.thirdparty.address === null)
&& this.thirdparty.address.address_id !== null && this.thirdparty.address.address_id !== null
) { // to complete ) { // to complete
context.addressId = this.thirdparty.address.address_id; context.addressId = this.thirdparty.address.address_id;
@ -151,10 +174,13 @@ export default {
loadData(){ loadData(){
getThirdparty(this.id).then(thirdparty => new Promise((resolve, reject) => { getThirdparty(this.id).then(thirdparty => new Promise((resolve, reject) => {
this.thirdparty = thirdparty; this.thirdparty = thirdparty;
this.thirdparty.kind = thirdparty.kind;
console.log('get thirdparty', thirdparty); console.log('get thirdparty', thirdparty);
if (this.action !== 'show') { if (this.action !== 'show') {
// bof! we force getInitialAddress because addressId not available when mounted if (thirdparty.address !== null) {
this.$refs.addAddress.getInitialAddress(thirdparty.address.address_id); // bof! we force getInitialAddress because addressId not available when mounted
this.$refs.addAddress.getInitialAddress(thirdparty.address.address_id);
}
} }
resolve(); resolve();
})); }));
@ -170,6 +196,7 @@ export default {
} }
}, },
mounted() { mounted() {
console.log('mounted', this.action);
if (this.action !== 'create') { if (this.action !== 'create') {
this.loadData(); this.loadData();
} else { } else {

View File

@ -81,22 +81,18 @@
<div class="item-row entity-bloc"> <div class="item-row entity-bloc">
<div class="item-col"> <div class="item-col">
{{ _self.label(thirdparty, options) }} {{ _self.label(thirdparty, options) }}
{% if thirdparty.kind == 'company' %} <span class="badge bg-thirdparty-{{ thirdparty.kind }}">{{ ('thirdparty.' ~ thirdparty.kind)|trans }}</span>
<span class="badge bg-info">{{ 'thirdparty.company'|trans }}</span>
{% elseif thirdparty.kind == 'child' %}
<span class="badge bg-chill-red">{{ 'thirdparty.Child'|trans }}</span>
{% elseif thirdparty.kind == 'contact' %}
<span class="badge bg-secondary">{{ 'thirdparty.contact'|trans }}</span>
{% endif %}
</div> </div>
<div class="item-col"> <div class="item-col">
<ul class="list-content fa-ul"> <ul class="list-content fa-ul">
{{ thirdparty.getAddress|chill_entity_render_box({ <li>
'render': 'list', {{ thirdparty.getAddress|chill_entity_render_box({
'with_picto': true, 'render': 'list',
'multiline': false, 'with_picto': true,
'with_valid_from': false 'multiline': false,
}) }} 'with_valid_from': false
}) }}
</li>
<li><i class="fa fa-li fa-phone"></i> <li><i class="fa fa-li fa-phone"></i>
{% if thirdparty.telephone %} {% if thirdparty.telephone %}
<a href="{{ 'tel:' ~ thirdparty.telephone }}">{{ thirdparty.telephone|chill_format_phonenumber }}</a> <a href="{{ 'tel:' ~ thirdparty.telephone }}">{{ thirdparty.telephone|chill_format_phonenumber }}</a>
@ -144,4 +140,16 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if options['showParent'] and thirdparty.isChild %}
<div class="item-row">
{{ 'thirdparty.Contact of'|trans }}&nbsp;:
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdparty.parent.id },
action: 'show',
displayBadge: true,
buttonText: thirdparty.parent|chill_entity_render_string
} %}
</div>
{% endif %}
{%- endif -%} {%- endif -%}

View File

@ -14,8 +14,7 @@
{{ form_row(form.profession) }} {{ form_row(form.profession) }}
{% endif %} {% endif %}
{{ form_row(form.types) }} {{ form_row(form.typesAndCategories) }}
{{ form_row(form.categories) }}
{{ form_row(form.telephone) }} {{ form_row(form.telephone) }}
{{ form_row(form.email) }} {{ form_row(form.email) }}
@ -29,12 +28,16 @@
{{ form_widget(form.activeChildren) }} {{ form_widget(form.activeChildren) }}
{% endif %} {% endif %}
{{ form_row(form.address) }}
{#
<div class="mb-3 row"> <div class="mb-3 row">
{{ form_label(form.address) }} {{ form_label(form.address) }}
{{ form_widget(form.address) }} {{ form_widget(form.address) }}
<div class="col-sm-8"> <div class="col-sm-8">
{% if thirdParty.address %} {% if thirdParty.address %}
{# include vue_address component #} {# include vue_address component #
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with { {% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdParty.id }, targetEntity: { name: 'thirdparty', id: thirdParty.id },
mode: 'edit', mode: 'edit',
@ -43,9 +46,9 @@
} %} } %}
{# {#
backUrl: path('chill_3party_3party_new'), backUrl: path('chill_3party_3party_new'),
#} #
{% else %} {% else %}
{# include vue_address component #} {# include vue_address component #
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with { {% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdParty.id }, targetEntity: { name: 'thirdparty', id: thirdParty.id },
mode: 'new', mode: 'new',
@ -56,6 +59,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
#}
{{ form_row(form.comment) }} {{ form_row(form.comment) }}
{{ form_row(form.centers) }} {{ form_row(form.centers) }}

View File

@ -19,3 +19,11 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block js %}
{{ encore_entry_script_tags('mod_input_address') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_input_address') }}
{% endblock %}

View File

@ -36,87 +36,10 @@
</div> </div>
{% endblock %} {% endblock %}
{% block js %}
{% block content_not %} {{ encore_entry_script_tags('mod_input_address') }}
<div class="thirdparty-edit my-5"> {% endblock %}
<div class="row justify-content-center">
<div class="col-md-10"> {% block css %}
{{ encore_entry_link_tags('mod_input_address') }}
{{ form_start(form) }}
{% if form.civility is defined %}
{{ form_row(form.civility) }}
{% endif %}
{{ form_row(form.name) }}
{% if form.nameCompany is defined %}
{{ form_row(form.nameCompany) }}
{{ form_row(form.acronym) }}
{% endif %}
{% if form.profession is defined %}
{{ form_row(form.profession) }}
{% endif %}
{{ form_row(form.types) }}
{{ form_row(form.categories) }}
{{ form_row(form.telephone) }}
{{ form_row(form.email) }}
<div class="mb-3 row">
{{ form_label(form.address) }}
{{ form_widget(form.address) }}
<div class="col-sm-8">
{% if thirdParty.address %}
{# include vue_address component #}
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdParty.id },
mode: 'edit',
addressId: thirdParty.address.id,
buttonSize: 'btn-sm',
} %}
{#
backUrl: path('chill_3party_3party_update', { thirdparty_id: thirdParty.id }),
#}
{% else %}
{# include vue_address component #}
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdParty.id },
mode: 'new',
buttonSize: 'btn-sm',
buttonText: 'Create a new address',
modalTitle: 'Create a new address',
} %}
{% endif %}
</div>
</div>
{{ form_row(form.comment) }}
{{ form_row(form.centers) }}
{{ form_row(form.active) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_path_forward_return_path('chill_3party_3party_index') }}">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
</li>
<li>
{{ form_widget(form.submit, {'label': 'Update', 'attr': {'class': 'btn btn-update' }}) }}
</li>
</ul>
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -48,13 +48,20 @@
</dd> </dd>
{% endif %} {% endif %}
<dt>{{ 'Type'|trans }}</dt> <dt>{{ 'thirdparty.Categories'|trans }}</dt>
{% set types = [] %} {% set types = [] %}
{% for t in thirdParty.types %} {% for t in thirdParty.types %}
{% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %} {% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %}
{% endfor %} {% endfor %}
{% for c in thirdParty.categories %}
{% set types = types|merge([ c.name|localize_translatable_string ]) %}
{% endfor %}
<dd> <dd>
{{ types|join(', ') }} {% if types|length > 0 %}
{{ types|join(', ') }}
{% else %}
<p class="chill-no-data-statement">{{ 'thirdParty.Any categories' }}</p>
{% endif %}
</dd> </dd>
<dt>{{ 'Phonenumber'|trans }}</dt> <dt>{{ 'Phonenumber'|trans }}</dt>

View File

@ -5,7 +5,36 @@ namespace Chill\ThirdPartyBundle\Search;
use Chill\MainBundle\Search\SearchApiInterface; use Chill\MainBundle\Search\SearchApiInterface;
use Chill\MainBundle\Search\SearchApiQuery; use Chill\MainBundle\Search\SearchApiQuery;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use function explode;
/*
* Internal note: test query for parametrizing / testing:
*
WITH rows AS (
SELECT 'aide a domicile en milieu rural admr' AS c, 'la roche sur yon' AS l
UNION
SELECT 'aide a domicile en milieu rural admr' AS c, 'fontenay-le-comte' AS l
), searches AS (
SELECT 'admr roche' AS s, 'admr' AS s1, 'roche' As s2
UNION
SELECT 'admr font' AS s, 'admr' AS s1, 'font' AS s2
)
SELECT
c, l, s, s1, s2,
strict_word_similarity(s, c)
+ (c LIKE '%' || s1 || '%')::int
+ (c LIKE '%' || s2 || '%')::int
+ (l LIKE '%' || s1 || '%')::int
+ (l LIKE '%' || s2 || '%')::int,
l LIKE '%' || s1 || '%',
l LIKE '%' || s2 || '%'
FROM rows, searches
*/
/**
* Generate query for searching amongst third parties
*/
class ThirdPartyApiSearch implements SearchApiInterface class ThirdPartyApiSearch implements SearchApiInterface
{ {
private ThirdPartyRepository $thirdPartyRepository; private ThirdPartyRepository $thirdPartyRepository;
@ -17,18 +46,45 @@ class ThirdPartyApiSearch implements SearchApiInterface
public function provideQuery(string $pattern, array $parameters): SearchApiQuery public function provideQuery(string $pattern, array $parameters): SearchApiQuery
{ {
return (new SearchApiQuery) $query = (new SearchApiQuery)
->setSelectKey('tparty') ->setSelectKey('tparty')
->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)") ->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)")
->setSelectPertinence("GREATEST(". ->setFromClause('chill_3party.third_party AS tparty
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), tparty.canonicalized),". LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id
"(tparty.canonicalized LIKE '%' || LOWER(UNACCENT(?)) || '%')::int". LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id
")", [ $pattern, $pattern ]) LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id
->setFromClause('chill_3party.third_party AS tparty') LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id
->setWhereClause("tparty.active IS TRUE ". LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id')
"AND (LOWER(UNACCENT(?)) <<% tparty.canonicalized OR ". ->andWhereClause("tparty.active IS TRUE")
"tparty.canonicalized LIKE '%' || LOWER(UNACCENT(?)) || '%')", [ $pattern, $pattern ]) ;
;
$strs = explode(' ', $pattern);
$wheres = [];
$whereArgs = [];
$pertinence = [];
$pertinenceArgs = [];
foreach ($strs as $str) {
if (!empty($str)) {
$wheres[] = "(LOWER(UNACCENT(?)) <<% tparty.canonicalized OR
tparty.canonicalized LIKE '%' || LOWER(UNACCENT(?)) || '%')";
$whereArgs[] = [$str, $str];
$pertinence[] = "STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), tparty.canonicalized) + ".
"(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT(?)) || '%')::int + ".
// take postcode label into account, but lower than the canonicalized field
"COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0) + ".
"COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)";
$pertinenceArgs[] = [$str, $str, $str, $str];
}
}
$query
->setSelectPertinence(\implode(' + ', $pertinence), \array_merge([],
...$pertinenceArgs))
->andWhereClause(\implode(' OR ', $wheres), \array_merge([],
...$whereArgs));
return $query;
} }
public function supportsTypes(string $pattern, array $types, array $parameters): bool public function supportsTypes(string $pattern, array $types, array $parameters): bool

View File

@ -61,7 +61,8 @@ class ThirdPartyRender extends AbstractChillEntityRender
'hLevel' => $options['hLevel'] ?? 3, 'hLevel' => $options['hLevel'] ?? 3,
'customButtons' => $options['customButtons'] ?? [], 'customButtons' => $options['customButtons'] ?? [],
'customArea' => $options['customArea'] ?? [], 'customArea' => $options['customArea'] ?? [],
'showContacts' => $options['showContacts'] ?? [], 'showContacts' => $options['showContacts'] ?? false,
'showParent' => $options['showParent'] ?? true,
]; ];
return return

View File

@ -0,0 +1,92 @@
<?php
namespace Chill\ThirdParty\Tests\Entity;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use PHPUnit\Framework\TestCase;
class ThirdPartyTest extends TestCase
{
public function testAddingRemovingActivityTypes()
{
$tp = new ThirdParty();
$cat1 = new ThirdPartyCategory();
$cat2 = new ThirdPartyCategory();
$tp->addTypesAndCategories('type');
$tp->addTypesAndCategories($cat1);
$tp->addTypesAndCategories($cat2);
$this->assertTrue($tp->getCategories()->contains($cat1));
$this->assertTrue($tp->getCategories()->contains($cat2));
$this->assertCount(2, $tp->getCategories());
$this->assertCount(1, $tp->getTypes());
$this->assertContains('type', $tp->getTypes());
$this->assertCount(3, $tp->getTypesAndCategories());
$this->assertContains($cat1, $tp->getTypesAndCategories());
$this->assertContains($cat2, $tp->getTypesAndCategories());
$this->assertContains('type', $tp->getTypesAndCategories());
// remove type
$tp->removeTypesAndCategories('type');
$tp->removeTypesAndCategories($cat2);
$this->assertTrue($tp->getCategories()->contains($cat1),
"test that cat1 is still present");
$this->assertFalse($tp->getCategories()->contains($cat2));
$this->assertCount(1, $tp->getCategories());
$this->assertCount(0, $tp->getTypes());
$this->assertNotContains('type', $tp->getTypes());
$this->assertCount(1, $tp->getTypesAndCategories());
$this->assertContains($cat1, $tp->getTypesAndCategories());
$this->assertNotContains($cat2, $tp->getTypesAndCategories());
$this->assertNotContains('type', $tp->getTypesAndCategories());
}
public function testSyncingActivityTypes()
{
$tp = new ThirdParty();
$tp->setTypesAndCategories([
'type1',
'type2',
$cat1 = new ThirdPartyCategory(),
$cat2 = new ThirdPartyCategory()
]);
$this->assertTrue($tp->getCategories()->contains($cat1));
$this->assertTrue($tp->getCategories()->contains($cat2));
$this->assertCount(2, $tp->getCategories());
$this->assertCount(2, $tp->getTypes());
$this->assertContains('type1', $tp->getTypes());
$this->assertContains('type2', $tp->getTypes());
$this->assertCount(4, $tp->getTypesAndCategories());
$this->assertContains($cat1, $tp->getTypesAndCategories());
$this->assertContains($cat2, $tp->getTypesAndCategories());
$this->assertContains('type1', $tp->getTypesAndCategories());
$this->assertContains('type2', $tp->getTypesAndCategories());
$tp->setTypesAndCategories([$cat1, 'type1']);
$this->assertTrue($tp->getCategories()->contains($cat1));
$this->assertFalse($tp->getCategories()->contains($cat2));
$this->assertCount(1, $tp->getCategories());
$this->assertCount(1, $tp->getTypes());
$this->assertContains('type1', $tp->getTypes());
$this->assertNotContains('type2', $tp->getTypes());
$this->assertCount(2, $tp->getTypesAndCategories());
$this->assertContains($cat1, $tp->getTypesAndCategories());
$this->assertNotContains($cat2, $tp->getTypesAndCategories());
$this->assertContains('type1', $tp->getTypesAndCategories());
$this->assertNotContains('type2', $tp->getTypesAndCategories());
}
}

View File

@ -5,6 +5,8 @@ module.exports = function(encore, entries)
ChillThirdPartyAssets: __dirname + '/Resources/public' ChillThirdPartyAssets: __dirname + '/Resources/public'
}); });
entries.push(__dirname + '/Resources/public/chill/index.js');
encore.addEntry( encore.addEntry(
'page_3party_3party_index', 'page_3party_3party_index',
__dirname + '/Resources/public/page/index/index.js' __dirname + '/Resources/public/page/index/index.js'

View File

@ -1,19 +1,5 @@
services: services:
Chill\ThirdPartyBundle\Form\ThirdPartyType: Chill\ThirdPartyBundle\Form\:
arguments: resource: '../../Form/'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' autowire: true
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' autoconfigure: true
$typesManager: '@Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager'
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
$om: '@doctrine.orm.entity_manager'
tags:
- { name: form.type }
Chill\ThirdPartyBundle\Form\Type\PickThirdPartyType:
arguments:
$em: '@Doctrine\ORM\EntityManagerInterface'
$urlGenerator: '@Symfony\Component\Routing\Generator\UrlGeneratorInterface'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
$typesManager: '@Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager'
tags:
- { name: form.type }

View File

@ -16,7 +16,9 @@ thirdparty.NameCompany: Service/Département
thirdparty.Acronym: Sigle thirdparty.Acronym: Sigle
thirdparty.Categories: Catégories thirdparty.Categories: Catégories
thirdparty.Child: Personne de contact thirdparty.Child: Personne de contact
thirdparty.child: Personne de contact
thirdparty.Children: Personnes de contact thirdparty.Children: Personnes de contact
thirdparty.children: Personnes de contact
thirdparty.Parent: Tiers institutionnel thirdparty.Parent: Tiers institutionnel
thirdparty.Parents: Tiers institutionnels thirdparty.Parents: Tiers institutionnels
thirdparty.Civility: Civilité thirdparty.Civility: Civilité
@ -29,15 +31,17 @@ thirdparty.UpdateBy.short: ' par '
thirdparty.CreatedAt.long: Date de création thirdparty.CreatedAt.long: Date de création
thirdparty.UpdatedAt.long: Date de la dernière modification thirdparty.UpdatedAt.long: Date de la dernière modification
thirdparty.UpdateBy.long: Utilisateur qui a effectué la dernière modification thirdparty.UpdateBy.long: Utilisateur qui a effectué la dernière modification
thirdparty.A company: Une institution thirdparty.A company: Une personne morale
thirdparty.company: Institution thirdparty.company: Personne morale
thirdparty.A contact: Une personne physique thirdparty.A contact: Une personne physique
thirdparty.contact: Personne physique thirdparty.contact: Personne physique
thirdparty.Contact of: Contact de
thirdparty.a_company_explanation: >- thirdparty.a_company_explanation: >-
Les institutions peuvent compter un ou plusieurs contacts, interne à l'instution. Il est également possible de Les personnes morales peuvent compter un ou plusieurs contacts, interne à l'instution. Il est également possible de
leur associer un acronyme, et le nom d'un service. leur associer un acronyme, et le nom d'un service.
thirdparty.a_contact_explanation: >- thirdparty.a_contact_explanation: >-
Les personnes physiques ne disposent pas d'acronyme, de service, ou de contacts sous-jacents. Les personnes physiques ne disposent pas d'acronyme, de service, ou de contacts sous-jacents. Il est possible de leur
indiquer une civilité et un métier.
thirdparty.Which kind of third party ?: Quel type de tiers souhaitez-vous créer ? thirdparty.Which kind of third party ?: Quel type de tiers souhaitez-vous créer ?
thirdparty.Contact data are confidential: Données de contact confidentielles thirdparty.Contact data are confidential: Données de contact confidentielles
@ -65,6 +69,7 @@ No nameCompany given: Aucune raison sociale renseignée
No acronym given: Aucun sigle renseigné No acronym given: Aucun sigle renseigné
No phone given: Aucun téléphone renseigné No phone given: Aucun téléphone renseigné
No email given: Aucune adresse courriel renseignée No email given: Aucune adresse courriel renseignée
thirdparty.Any categories: Aucune catégorie
The party is visible in those centers: Le tiers est visible dans ces centres The party is visible in those centers: Le tiers est visible dans ces centres
The party is not visible in any center: Le tiers n'est associé à aucun centre The party is not visible in any center: Le tiers n'est associé à aucun centre