Merge branch 'master' into HEAD

This commit is contained in:
Julien Fastré 2021-10-18 13:49:00 +02:00
commit 8be11314c3
86 changed files with 2566 additions and 789 deletions

View File

@ -10,6 +10,24 @@ and this project adheres to
## Unreleased ## Unreleased
<!-- write down unreleased development here -->
* [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
* [AccompanyingCourse banner]: replace translation referrer (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/70)
* [Location]: add location system in activity and RV (calendar). User can choose in location list or create a new location.
## Test releases
### Test release 2021-10-11
* Address: zoom on postal code geometry + fix origin of manually entered postal code
* add 3 new fields to PostalCode and adapt postal code command and fixtures * add 3 new fields to PostalCode and adapt postal code command and fixtures
* [Aside activity] Fixes for aside activity * [Aside activity] Fixes for aside activity
@ -28,14 +46,15 @@ and this project adheres to
* filter thirdparties in list * filter thirdparties in list
* [FilterOrder]: add development kit for generating filter and ordering in list * [FilterOrder]: add development kit for generating filter and ordering in list
* [AccompanyingCourse banner]: replace translation referrer (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/70) * [Capitalization of names] person names are capitalized on creation, on prePersist event
* [Location]: add location system in activity and RV (calendar). User can choose in location list or create a new location. * [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 releases
### 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.
@ -54,8 +73,31 @@ 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...
DO NOT ADD unreleased items here. Add them under "Unreleased" title
### Test release yyyy-mm-dd
-->
## Stable releases
No stable releases for v2+

383
CONVENTIONS.md Normal file
View File

@ -0,0 +1,383 @@
# Conventions Chill
en cours de rédaction
## Assets: nommage des entrypoints
Trois types d'entrypoint:
* application vue (souvent spécifique à une page) -> préfixé par `vue_`;
* code js/css qui est réutilisé à plusieurs endroits:
* ckeditor
* async_upload (utilisé pour un formulaire)
* bootstrap
* chill.js
* ...
=> on préfixe `mod_`
* code css ou js pour une seule page
* ré-utilise parfois des "foncitionnalités": ShowHide, ...
=> on préfixe `page_`
Arborescence:
```
# Sous Resources/public
- chill/ => theme (chill)
- chillmain.scss -> push dans l'entrypoint chill
- lib/ => ne vont jamais dans un entrypoint, mais sont ré-utilisés par d'autres
- ShowHide
- Collection
- Select2
- module/ => termine dans des entrypoints ré-utilisables (mod_)
- bootstrap
- custom.scss
- custom/
- variables.scss
- ..
- forkawesome
- AsyncUpload
- vue/ => uniquement application vue (vue_)
- _components
- app
- page/ => uniquement pour une seule page (page_)
- login
- person
- personvendee
- household_edit_metadata
- index.js
```
## Organisation des feuilles de styles
Comment s'échaffaudent les styles dans Chill ?
1. l'entrypoint **mod_bootstrap** (module bootstrap) est le premier niveau. Toutes les parties(modules) de bootstrap sont convoquées dans le fichier ```bootstrap.js``` situé dans ```ChillMainBundle/Resources/public/module/bootstrap```.
* Au début, ce fichier importe le fichier ```variables.scss``` qui détermine la plupart des réglages bootstrap tels qu'on les a personnalisés. Ce fichier surcharge l'original, et de nombreuses variables y sont adaptées pour Chill.
* On veillera à ce qu'on puisse toujours comparer ce fichier à l'original de bootstrap. En cas de mise à jour de bootstrap, il faudra générer un diff, et adapter ce diff sur le fichier variable de la nouvelle version.
* A la fin on importe le fichier ```custom.scss```, qui comprends des adaptations de bootstrap pour le préparer à notre thème Chill.
* ce ```custom.scss``` peut être splitté en plus petits fichiers avec des ```@import 'custom/...'```
* L'idée est que cette première couche bootstrap règle un partie importante des styles de l'application, en particulier ce qui touche aux position du layout, aux points de bascules responsive, aux marges et écarts appliqués par défauts aux éléments qu'on manipule.
2. l'entrypoint **chill** est le second niveau. Il contient le thème Chill qui est reconnaissable à l'application.
* Chaque bundle a un dossier ```Resources/public/chill``` dans lequel on peut trouver une feuille sass principale, qui est éventuellement splittée avec des ```@imports```. Toutes ces feuilles sont compilées dans un unique entrypoint Chill, c'est le thème de l'application. Celui-ci surcharge bootstrap.
* La feuille chillmain.scss devrait contenir les cascades de styles les plus générales, celles qui sont appliquées à de nombreux endroits de l'application.
* La feuille chillperson.scss va aussi retrouver des styles propres aux différents contextes des personnes: person, household et accompanyingcourse.
* Certains bundles plus secondaires ne contiennent que des styles spécifiques à leur fonctionnement.
3. les entrypoints **vue_** sont utilisés pour des composants vue. Les fichiers vue peuvent contenir un bloc de styles scss. Ce sont des styles qui ne concernent que le composant et son héritage, le tag ```scoped``` précise justement sa portée (voir la doc).
4. les entrypoints **page_** sont utilisés pour ajouter des assets spécifiques à certaines pages, le plus souvent des scripts et des styles.
## Taguer du code html et construire la cascade de styles
L'exemple suivant montre comment taguer sans excès un élément de code. On remarque que:
* il n'est pas nécessaire de taguer toutes les classes intérieures,
* il ne faut pas répéter la classe parent dans toutes les classes enfants. La cascade sass va permettre de saisir le html avec souplesse sans alourdir la structure des balises.
* souvent la première classe sera déclinée par plusieurs classes qui commencent de la même manière: ```bloc-dark``` ajoute juste la version sombre de ```bloc```, on ne met pas ```bloc dark```, car on ne souhaite pas que la classe ```dark``` de ```bloc``` interagisse avec la même classe ```dark``` de ```table```. On aura donc un élément ```bloc bloc-dark``` et un élément ```table table-dark```.
```html
<div class="bloc bloc-dark mon-bloc">
<h3>mon titre</h3>
<ul class="record_actions">
<li>
<a class="btn btn-edit"></a>
</li>
<li></li>
<li></li>
</ul>
</div>
```
Finalement, il importe ici de définir ce qu'est un bloc, ce qu'est une zone d'actions et ce qu'est un bouton. Ces 3 éléments existent de manière autonome, ce sont les seuls qu'on tagge.
Par exemple pour mettre un style au titre on précise juste h3 dans la cascade bloc.
```sass
div.bloc {
// un bloc générique, utilisé à plusieurs endroits
&.bloc-dark {
// la version sombre du bloc
}
h3 {}
ul {
// une liste standard dans bloc
li {
// des items de liste standard dans bloc
}
}
}
div.mon-bloc {
// des exceptions spécifiques à mon-bloc,
// qui sont des adaptations de bloc
}
ul.record_actions {
// va uniformiser tous les record_actions de l'application
li {
//...
}
}
.btn {
// les boutons de bootstrap
.btn-edit {
// chill étends les boutons bootstrap pour ses propres besoins
}
}
</style>
```
## Render box
## URL
### Nommage des routes
:::warning
Ces règles n'ont pas toujours été utilisées par le passé. Elles sont souhaitées pour le futur.
:::
Les routes sont nommées de cette manière:
`chill_bundle_entite_action`
1. d'abord chill_ (pour tous les modules chill)
2. ensuite une string qui est identique, par bundle
3. si le point est un point d'api (json), alors ajouter la string `api`
4. ensuite une string qui indique sur quelle entité porte la route, voire également les sous-entités
5. ensuite une action (`list`, `view`, `edit`, `new`, ...)
Le fait d'indiquer `api` en 3 permet de distinguer les routes d'api qui sont générées par la configuration (qui sont toutes préfixées par `chill_api`, de celles générées manuellement. (Exemple: `chill_api_household__index`, et `chill_person_api_household_members_move`)
Si les points 4 et 5 sont inexistants, alors ils sont remplacés par d'autres éléments de manière à garantir l'unicité de la route, et sa bonne compréhension.
### URL
Les URL respectent également une convention:
#### Pour les pages html
:::warning
Ces règles n'ont pas toujours été utilisées par le passé. Elles sont souhaitées pour le futur.
:::
Syntaxe:
```
/{_locale}/bundle/entity/{id}/action
/{_locale}/bundle/entity/sub-entity/{id}/action
```
Les éléments suivants devraient se trouver dans la liste:
1. la locale;
2. un identifiant du bundle
3. l'entité auquel il se rapporte
4. les éventuelles sous-entités auxquelles l'url se rapport
5. l'action
Ces éléments peuvent être entrecoupés de l'identifiant d'une entité. Dans ce cas, cet identifiant se place juste après l'entité auquel il se rapporte.
Exemple:
```
# liste des échanges pour une personne
/fr/activity/person/25/activity/list
# nouvelle activité
/fr/activity/activity/new?person_id=25
```
#### Pour les API
:::info
Les routes générées automatiquement sont préfixées par chill_api
:::
Syntaxe:
```
/api/1.0/bundle/entity/{id}/action
/api/1.0/bundle/entity/sub-entity/{id}/action
```
Les éléments suivants devraient se trouver dans la liste:
1. la string `/api/` et puis la version (1.0)
2. un identifiant du bundle
3. l'entité auquel il se rapporte
4. les éventuelles sous-entités auxquelles l'url se rapport
5. l'action
Ces éléments peuvent être entrecoupés de l'identifiant d'une entité. Dans ce cas, cet identifiant se place juste après l'entité auquel il se rapporte.
## Règles UI chill
### Titre des pages
#### Chaque page contient un titre
Chaque page contient un titre dans la balise head. Ce titre est normalement identique à celui de l'entête de la page.
Astuce: il est possible d'utiliser la fonction `block` de twig pour cela:
```htmlmixed=
{% block title "Titre de la page" %}
{% block content %}
<h1>
{{ block('title')}}
</h1>
{% endblock %}
```
### Utilisation des `entity_render`
#### En twig
Les templates twig doivent toujours utiliser la fonction chill_entity_render_box pour effectuer le rendu des éléments suivants:
* User
* Person
* SocialIssue
* SocialAction
* Address
* ThirdParty
* ...
Exemple:
```
address|chill_entity_render_box
```
Justification:
* des éléments sont parfois personnalisés par installation (par exemple, le nom de chaque utilisateur sera suivi par le nom du service)
* pour rationaliser et rendre semblable les affichages
* pour simplifier le code twig
A prevoir:
* toujours trois positions:
* inline
* block
* item (dans un tableau, une ligne)
> block et item sont en fait la même option passée au render_box: render: bloc. Il y a aussi raw pour le inline, et label pour une titraille configurable avec des options.
> quand on passe loption render: bloc, on peut placer le render_box dans une boucle for plus large qui fonctionne avec la classe flex-table ou la classe flex-bloc, ce qui donnera un affichage en rangée (table) ou en blocs. [name=Mathieu]
#### En vue
Il existe systématiquement une "box" équivalente en vue.
#### Lien vers des sections
A chaque fois qu'on indique le nom d'une personne, un parcours, un ménage, il y a toujours:
* un lien pour accéder à son dossier (pour autant que l'utilisateur ait les droits d'accès);
* à moins qu'il ne soit indiqué dans une phrase, l'icône de son dossier avant ou après (donc un bonhomme pour la personne, une maison pour le ménage, un fa-random pour les parcours);
Ces éléments sont toujours proposé par des `render_box` par défaut. Des options permettent de les désactiver dans des cas particuliers
> à discuter, quelques réflexion:
> quelle est la logique qui domine pour les boutons ? on a symbolisé les 4 actions du crud par des couleurs: bleu(show) orange(edit) vert(create) et rouge(delete).
> Est-ce que c'est ça qui prime, et comment ça s'articule avec la logique des pictos ?
> Par exemple, il pourrait être logique d'utiliser l'oeil bleu pour voir l'objet, qu'il s'agisse d'une personne ou d'un parcours, ce serait plutôt le contexte, et l'infobulle (title) qui préciserait le contexte.
> Je pense que les pictos de boutons doivent faire référence à l'action, mais pas à l'objet. Autrement dit je n'utiliserais jamais l'icone du ménage ou du parcours dans les boutons.
> Pour représenter les ménages et les parcours, je pense qu'il faudrait trouver autre chose que forkawesome. Si c'est des pictos, trouver un motif différents et de tailles différente. Réfléchir à un couplage picto-couleur-forme différent, qui exprime le contexte et qui se distingue bien des boutons.
> Idem pour les badges, il faut une palette de badge qui couvre tous les besoins: socialIssue, socialActions, socialReason, members, etc. [name=Mathieu]
### Formulaires
#### Vocabulaire:
Utiliser toujours:
* `Créer` dans un `bt bt-create` pour les **liens** vers le formulairep pour créer une entité (pour parvenir au formulaire);
* `Enregistrer` dans un `bt bt-save` pour les boutons "Enregistrer" (dans un formulaire édition **ou** création);
* `Enregistrer et nouveau`
* `Enregistrer et voir`
* `Modifier` dans un `bt bt-edit` pour les **liens** vers le formulaire d'édition
* `Dupliquer` (préciser là où on peut le voir)
* `Annuler` pour quitter une page d'édition avec un lien vers la liste, ou le `returnPath`
#### Retour après un enregistrement
Après avoir cliqué sur "Créer" ou "Sauver", la page devrait revenir:
* vers le returnPath, s'il existe;
* sinon, vers la page "vue".
### Bandeaux contenant les boutons d'actions
Les boutons sont toujours dans un bandeau "sticky-form" dans le bas du formulaire ou de la page de liste.
Si pertinent:
* Le bandeau contient un bouton "Annuler" qui retourne à la page précédente. Il est obligatoire pour les formulaires, optionnel pour les listes ou les pages "résumés"
* Ce bouton "annuler" est toujours à gauche
```
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_entity_return_path('route_name' { 'route': 'option' } )}}">{{ return_path_label }}</a>
</li>
<li>
<!-- action 1 -->
</li>
</ul>
```
### Messages flash
#### A la création d'une entité
A chaque fois qu'un élément est créé par un formulaire, un message flash doit apparaitre. Il indique:
> "L'élément a été créé"
Le nom de l'élément peut être remplacé par quelque chose de plus pertinent:
> * L'activité a été créée
> * Le rendez-vous a été créé
> * ...
#### A l'enregistrement d'une entité
A chaque fois qu'un élément est enregistré, un message flash doit apparaitre:
> * Les données ont été modifiées
>
#### Erreur sur un formulaire (erreur de validation)
En tête d'un formulaire, un message flash doit indiquer que des validations n'ont pas réussi:
> Ce formulaire contient des erreurs
Les erreurs doivent apparaitre attachée au champ qui les concerne. Toutefois, il est acceptable d'afficher les erreurs à la racine du formulaire s'il était complexe, techniquement, d'attacher les erreurs.
### Liens de retour
A chaque fois qu'un lien est indiqué, vérifier si on ne doit pas utiliser la fonction `chill_return_path`, `chill_forward_return_path` ou `chill_return_path_or`.
* depuis la page liste, vers l'ouverture d'un élément, ou le bouton création => utiliser `chill_path_add_return_path`
* dans ces pages d'éditions,
* utiliser `chill_return_path_or` dans le bouton "Cancel";
* pour les boutons "enregistrer et voir" et "Enregistrer et fermer" => ?

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

@ -32,7 +32,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) { foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) {
switch($key) { switch($key) {
case 'q': case 'q':
continue; break;
case 'page': case 'page':
$builder->add($key, HiddenType::class, [ $builder->add($key, HiddenType::class, [
'data' => 1 'data' => 1

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

@ -166,6 +166,7 @@
<li class="entry" data-collection-is-persisted="1"> <li class="entry" data-collection-is-persisted="1">
<div> <div>
{{ form_widget(entry) }} {{ form_widget(entry) }}
{{ form_errors(entry) }}
</div> </div>
</li> </li>
{% else %} {% else %}

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.
@ -127,7 +129,7 @@ New center: Nouveau centre
Center: Centre Center: Centre
#admin section for permissions group #admin section for permissions group
Permissions group list: Liste des groupes de permissions Permissions group list: Groupes de permissions
Create a new permissions group: Créer un nouveau groupe de permissions Create a new permissions group: Créer un nouveau groupe de permissions
Permission group "%name%": Groupe de permissions "%name%" Permission group "%name%": Groupe de permissions "%name%"
Grant those permissions: Attribue ces permissions Grant those permissions: Attribue ces permissions
@ -144,7 +146,6 @@ The role '%role%' has been removed: Le rôle "%role%" a été enlevé de ce grou
The role '%role%' on circle '%scope%' has been removed: Le rôle "%role%" sur le cercle "%scope%" a été enlevé de ce groupe de permission The role '%role%' on circle '%scope%' has been removed: Le rôle "%role%" sur le cercle "%scope%" a été enlevé de ce groupe de permission
#admin section for users #admin section for users
user list: Liste des utilisateurs
User edit: Modification d'un utilisateur User edit: Modification d'un utilisateur
User'status: Statut de l'utilisateur User'status: Statut de l'utilisateur
Disabled, the user is not allowed to login: Désactivé, l'utilisateur n'est pas autorisé à se connecter Disabled, the user is not allowed to login: Désactivé, l'utilisateur n'est pas autorisé à se connecter
@ -167,8 +168,12 @@ Back to the user edition: Retour au formulaire d'édition
Password successfully updated!: Mot de passe mis à jour Password successfully updated!: Mot de passe mis à jour
Flags: Drapeaux Flags: Drapeaux
# admin section for users jobs
User jobs: Métiers
#admin section for circles (old: scopes) #admin section for circles (old: scopes)
List circles: Liste des cercles List circles: Cercles
New circle: Nouveau cercle New circle: Nouveau cercle
Circle: Cercle Circle: Cercle
Circle edit: Modification du cercle Circle edit: Modification du cercle
@ -279,6 +284,17 @@ crud:
success: Les données ont été enregistrées success: Les données ont été enregistrées
view: view:
link_duplicate: Dupliquer link_duplicate: Dupliquer
admin_user:
index:
title: Utilisateurs
add_new: Créer
admin_user_job:
index:
title: Métiers
add_new: Créer
title_new: Nouveau métier
title_edit: Modifier un métier
No entities: Aucun élément No entities: Aucun élément
CHILL_FOO_SEE: Voir un élément CHILL_FOO_SEE: Voir un élément

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

@ -268,7 +268,7 @@ final class PersonController extends AbstractController
) { ) {
$this->em->persist($person); $this->em->persist($person);
// $this->em->flush(); $this->em->flush();
$this->lastPostDataReset(); $this->lastPostDataReset();
if ($form->get('createPeriod')->isClicked()) { if ($form->get('createPeriod')->isClicked()) {

View File

@ -12,28 +12,19 @@ use Symfony\Component\Validator\Exception\LogicException as ExceptionLogicExcept
class PersonEventListener class PersonEventListener
{ {
public function onPrePersist(LifecycleEventArgs $event): void public function prePersistPerson(Person $person): void
{ {
if($event->getObject() instanceof Person){ $firstnameCaps = mb_convert_case(mb_strtolower($person->getFirstName()), MB_CASE_TITLE, 'UTF-8');
$firstnameCaps = ucwords(strtolower($firstnameCaps), " \t\r\n\f\v'-");
$person->setFirstName($firstnameCaps);
$person = $event->getObject(); $lastnameCaps = mb_strtoupper($person->getLastName(), 'UTF-8');
$firstnameCaps = mb_convert_case(mb_strtolower($person->getFirstName()), MB_CASE_TITLE, 'UTF-8'); $person->setLastName($lastnameCaps);
$firstnameCaps = ucwords(strtolower($firstnameCaps), " \t\r\n\f\v'-");
$person->setFirstName($firstnameCaps);
$lastnameCaps = mb_strtoupper($person->getLastName(), 'UTF-8');
$person->setLastName($lastnameCaps);
} elseif ($event->getObject() instanceof PersonAltName){
$altname = $event->getObject();
$altnameCaps = mb_strtoupper($altname->getLabel(), 'UTF-8');
$altname->setLabel($altnameCaps);
} else {
throw new LogicException('Entity must be a person or an altname');
}
} }
}
public function prePersistAltName(PersonAltName $altname)
{
$altnameCaps = mb_strtoupper($altname->getLabel(), 'UTF-8');
$altname->setLabel($altnameCaps);
}
}

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

@ -130,6 +130,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

@ -18,12 +18,12 @@
{% set activeRouteKey = '' %} {% set activeRouteKey = '' %}
{% block title %}{{ 'Update details for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName } )|capitalize }}{% endblock %} {% block title %}{{ 'Update details for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %}
{% block personcontent %} {% block personcontent %}
<div class="person-edit"> <div class="person-edit">
<h1>{{ 'Update details for %name%'|trans({ '%name%': person.firstName|capitalize ~ ' ' ~ person.lastName|capitalize } ) }}</h1> <h1>{{ block('title') }}</h1>
{% form_theme form '@ChillMain/Form/fields.html.twig' %} {% form_theme form '@ChillMain/Form/fields.html.twig' %}
{{ form_start(form) }} {{ form_start(form) }}

View File

@ -23,8 +23,7 @@ This view should receive those arguments:
- person - person
#} #}
{% block title %}{{ 'Person details'|trans|capitalize ~ ' ' ~ person.firstName|capitalize ~ {% block title %}{{ 'Person details'|trans|capitalize ~ ' ' ~ person|chill_entity_render_string }}{% endblock %}
' ' ~ person.lastName }}{% endblock %}
{# {#
we define variables to include an edit form repeated multiple time across we define variables to include an edit form repeated multiple time across
the page the page

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

@ -20,11 +20,7 @@ class PersonCreateEventTest extends TestCase
$person->setFirstName($firstname); $person->setFirstName($firstname);
$person->setLastName($lastname); $person->setLastName($lastname);
$args = $this->createMock(LifecycleEventArgs::class); $listener->prePersistPerson($person);
$args->method('getObject')
->willReturn($person);
$listener->onPrePersist($args);
$this->assertEquals($firstnameExpected, $person->getFirstName()); $this->assertEquals($firstnameExpected, $person->getFirstName());
$this->assertEquals($lastnameExpected, $person->getLastName()); $this->assertEquals($lastnameExpected, $person->getLastName());
@ -41,11 +37,7 @@ class PersonCreateEventTest extends TestCase
$personAltname->setLabel($altname); $personAltname->setLabel($altname);
$args = $this->createMock(LifecycleEventArgs::class); $listener->prePersistAltName($personAltname);
$args->method('getObject')
->willReturn($personAltname);
$listener->onPrePersist($args);
$this->assertEquals($altnameExpected, $personAltname->getLabel()); $this->assertEquals($altnameExpected, $personAltname->getLabel());
} }
@ -66,4 +58,4 @@ class PersonCreateEventTest extends TestCase
yield ['fastré', 'FASTRÉ']; yield ['fastré', 'FASTRÉ'];
yield ['émile', 'ÉMILE']; yield ['émile', 'ÉMILE'];
} }
} }

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

@ -1,12 +1,14 @@
services: services:
Chill\PersonBundle\EventListener\PersonEventListener: Chill\PersonBundle\EventListener\PersonEventListener:
autoconfigure: true autoconfigure: true
tags: tags:
-
name: 'doctrine.orm.entity_listener'
event: 'onPrePersist'
entity: 'Chill\PersonBundle\Entity\Person'
- -
name: 'doctrine.orm.entity_listener' name: 'doctrine.orm.entity_listener'
event: 'onPrePersist' event: 'prePersist'
entity: 'Chill\PersonBundle\Entity\PersonAltName' entity: 'Chill\PersonBundle\Entity\Person'
method: 'prePersistPerson'
-
name: 'doctrine.orm.entity_listener'
event: 'prePersist'
entity: 'Chill\PersonBundle\Entity\PersonAltName'
method: 'prePersistAltName'

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