diff --git a/.editorconfig b/.editorconfig index d769b46a4..fe115d4c0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ charset = utf-8 end_of_line = LF insert_final_newline = true trim_trailing_whitespace = true +indent_size = 4 [*.{php,html,twig}] indent_style = space diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..7fb7d4680 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,108 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to + +* [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for stable releases; +* date versioning for test releases + +## Unreleased + + + + + + +## Test releases + +### Test release 2021-10-18 + +* [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 release 2021-10-11 + +* Address: zoom on postal code geometry + fix origin of manually entered postal code + +* in the Address vue component, order the postal code and street address by alphabetic and numeric order + +* add 3 new fields to PostalCode and adapt postal code command and fixtures + +* [Aside activity] Fixes for aside activity + + * categories with child + * fast creation buttons + * add ordering for types + +* [AccompanyingCourse Resume page] badge-title for AccompanyingCourseWork and for Activities; +* Improve badges behaviour with small screens; + +* [ThirdParty]: + + * third party list + * create a kind contact/institution when create a new thirdparty, and set contact embedded as kind=child; + * filter thirdparties in list + +* [FilterOrder]: add development kit for generating filter and ordering in list +* [Capitalization of names] person names are capitalized on creation, on prePersist event +* [On-The-Fly] modale works for showing, editing and creating person or thirdparty ; +* [AccompanyingCourse Resume page] associated persons list, can see household when hover, and with show on-the-fly modale when clicking person ; + +### test release 2021-10-04 + +* [Household editor][UI] Update how household suggestion and addresses are picked; + + See https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/80 +* [AddAddress] Handle address suggestion; +* [CenterType][Create a person] when overriding the ACL rules, allow to show a PickCenterType + when no centers are reachable by the default ACL. +* [Household] Show comment event if no address are associated with the household; +* [Person results] Add requestor into search results: + + * a badge "requestor" is shown into search results; + * periods where the person is only requestor (without participating) are also shown; + + Issues: + + * https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/13 + * https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/199 +* [Person form] "accept sms" not required: + + 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 + +* [Household editor] suggest only temporarily addresses; + See https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/82 +* On-The-Fly modale works for showing, editing and creating person and thirdparty ; +* AccompanyingCourse Resume page: list associated persons by household, see household when hover, and show on-the-fly modale when clicking on person ; +* [AddAddress] Handle address suggestion; +* [AddAddress][Entity address]: add a link between address and address reference; +* [Household editor] suggest household by comparing the temporary addresses from courses; + + 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 + + + +## Stable releases + +No stable releases for v2+ + diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100644 index 000000000..4dcdb08ea --- /dev/null +++ b/CONVENTIONS.md @@ -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 +
+

mon titre

+ +
+``` + +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 + } +} + +``` + +## 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 %} +

+ {{ block('title')}} +

+{% 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 l’option 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 + +``` + +``` + +### 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" => ? + diff --git a/composer.json b/composer.json index 747747290..0d134cbf8 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "league/csv": "^9.7.1", "nyholm/psr7": "^1.4", "phpoffice/phpspreadsheet": "^1.16", + "ramsey/uuid-doctrine": "^1.7", "sensio/framework-extra-bundle": "^5.5", "symfony/asset": "4.*", "symfony/browser-kit": "^5.2", @@ -29,6 +30,7 @@ "symfony/expression-language": "4.*", "symfony/form": "4.*", "symfony/intl": "4.*", + "symfony/mime": "^4 || ^5", "symfony/monolog-bundle": "^3.5", "symfony/security-bundle": "4.*", "symfony/serializer": "^5.2", diff --git a/src/Bundle/ChillActivityBundle/CHANGELOG.md b/src/Bundle/ChillActivityBundle/CHANGELOG.md index 871ead4bd..6261aa1db 100644 --- a/src/Bundle/ChillActivityBundle/CHANGELOG.md +++ b/src/Bundle/ChillActivityBundle/CHANGELOG.md @@ -27,3 +27,4 @@ Version 1.5.5 ============= - [activity] replace dropdown for selecting reasons and use chillEntity for reason rendering +- fix bug: error when trying to edit activity of which the type has been deactivated diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index a454626b7..8a4a228eb 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -174,6 +174,10 @@ class ActivityController extends AbstractController $activityType = $em->getRepository(\Chill\ActivityBundle\Entity\ActivityType::class) ->find($activityType_id); + if (isset($activityType) && !$activityType->isActive()) { + throw new \InvalidArgumentException('Activity type must be active'); + } + $activityData = null; if ($request->query->has('activityData')) { $activityData = $request->query->get('activityData'); diff --git a/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php b/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php index ce32143f5..a23b2d73f 100644 --- a/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php +++ b/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php @@ -141,7 +141,6 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface, C $ref = 'activity_'.$person->getFullnameCanonical(); for($i = 0; $i < $activityNbr; $i ++) { - print "Creating an activity type for : ".$person." (ref: ".$ref.") \n"; $activity = $this->newRandomActivity($person); $manager->persist($activity); } diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php index e37dcbc73..ae310f775 100644 --- a/src/Bundle/ChillActivityBundle/Entity/Activity.php +++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php @@ -23,6 +23,7 @@ namespace Chill\ActivityBundle\Entity; use Chill\DocStoreBundle\Entity\Document; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; +use Chill\MainBundle\Entity\Location; use Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodLinkedWithSocialIssuesEntityInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\SocialWork\SocialAction; @@ -177,6 +178,13 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer */ private string $sentReceived = ''; + /** + * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Location") + * @groups({"read"}) + */ + private ?Location $location = null; + + public function __construct() { $this->reasons = new ArrayCollection(); @@ -555,4 +563,22 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer return $this; } + + /** + * @return Location|null + */ + public function getLocation(): ?Location + { + return $this->location; + } + + /** + * @param Location|null $location + * @return Activity + */ + public function setLocation(?Location $location): Activity + { + $this->location = $location; + return $this; + } } diff --git a/src/Bundle/ChillActivityBundle/Entity/ActivityType.php b/src/Bundle/ChillActivityBundle/Entity/ActivityType.php index 3446fa6dd..14ede1335 100644 --- a/src/Bundle/ChillActivityBundle/Entity/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Entity/ActivityType.php @@ -248,6 +248,16 @@ class ActivityType */ private string $socialActionsLabel = ''; + /** + * @ORM\Column(type="smallint", nullable=false, options={"default"=1}) + */ + private int $locationVisible = self::FIELD_INVISIBLE; + + /** + * @ORM\Column(type="string", nullable=false, options={"default"=""}) + */ + private string $locationLabel = ''; + /** * @ORM\Column(type="float", options={"default"="0.0"}) */ @@ -820,4 +830,29 @@ class ActivityType return $this; } + + public function getLocationVisible(): ?int + { + return $this->locationVisible; + } + + public function setLocationVisible(int $locationVisible): self + { + $this->locationVisible = $locationVisible; + + return $this; + } + + public function getLocationLabel(): ?string + { + return $this->locationLabel; + } + + public function setLocationLabel(string $locationLabel): self + { + $this->locationLabel = $locationLabel; + + return $this; + } + } diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php index 8c8fb45a9..19d53c262 100644 --- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php @@ -6,6 +6,7 @@ use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\ActivityPresence; use Chill\ActivityBundle\Entity\ActivityReason; use Chill\DocStoreBundle\Form\StoredObjectType; +use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\CommentType; use Chill\PersonBundle\Entity\Person; @@ -93,15 +94,13 @@ class ActivityType extends AbstractType /** @var \Chill\ActivityBundle\Entity\ActivityType $activityType */ $activityType = $options['activityType']; - if (!$activityType->isActive()) { - throw new \InvalidArgumentException('Activity type must be active'); - } - // TODO revoir la gestion des center au niveau du form des activité. if ($options['center']) { $builder->add('scope', ScopePickerType::class, [ 'center' => $options['center'], - 'role' => $options['role'] + 'role' => $options['role'], + // TODO make required again once scope and rights are fixed + 'required' => false ]); } @@ -224,7 +223,7 @@ class ActivityType extends AbstractType if ($activityType->isVisible('comment')) { $builder->add('comment', CommentType::class, [ - 'label' => empty($activityType->getLabel('comment')) + 'label' => empty($activityType->getLabel('comment')) ? 'activity.comment' : $activityType->getLabel('comment'), 'required' => $activityType->isRequired('comment'), ]); @@ -304,6 +303,23 @@ class ActivityType extends AbstractType ; } + if ($activityType->isVisible('location')) { + $builder->add('location', HiddenType::class) + ->get('location') + ->addModelTransformer(new CallbackTransformer( + function (?Location $location): string { + if (null === $location) { + return ''; + } + return $location->getId(); + }, + function (?string $id): Location { + return $this->om->getRepository(Location::class)->findOneBy(['id' => (int) $id]); + } + )) + ; + } + if ($activityType->isVisible('emergency')) { $builder->add('emergency', CheckboxType::class, [ 'label' => $activityType->getLabel('emergency'), diff --git a/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss b/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss index 528d75ded..28c02e23e 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss +++ b/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss @@ -1,7 +1,9 @@ // Access to Bootstrap variables and mixins @import '~ChillMainAssets/module/bootstrap/shared'; -// activity creation first step: select type page +//// ACTIVITY CREATION +// first step: select type page + div.new-activity-select-type { div.activity-row { display: flex; @@ -21,91 +23,36 @@ div.new-activity-select-type { } } -// exceptions for flex-table in list-records -div.activity-list { - div.flex-table { - div.item-bloc { - div.item-row.main { - div.item-col { - &:first-child { - flex-basis: 15%; - } - ul.list-content { - li.social-issues, li.social-actions { - .badge-primary { - font-variant: small-caps; - font-weight: bold; - font-size: 88%; - margin-bottom: 0.2em; - } - } - li.social-issues .badge-primary { - background-color: $orange; - } - li.social-actions .badge-primary { - background-color: $green; - } - } - } - } - div.item-row.comment { - margin-left: 15%; - blockquote.chill-user-quote { - margin-top: 0.5em; - margin-bottom: 0.5em; - } - } - div.item-row.details { - margin-left: 15%; +//// ACTIVITY LIST PAGE +// precise badge-title specific details - // override flex-bloc to adapt in list - // TODO refund this - div.accompanyingCourse.flex-bloc.concerned-groups { - margin: 0; - width: 100%; - justify-content: space-around; - div.item-bloc { - box-shadow: unset; - padding: 0; - flex-basis: 25%; - div.item-row { - flex-direction: column; - div.item-col { - &:first-child { - width: unset; - } - &:last-child { - border-top: 0; - margin-top: 0; - padding-top: 0; - ul.list-content { - padding: 0; - } - } - } - } - } +h2.badge-title { + div.duration { + font-size: smaller; + padding-left: 1em; + margin-top: 1em; + } + ul.list-content { + font-size: 70%; + list-style-type: none; + padding-left: 0; + margin: 0; + li { + margin-bottom: 0.2em; + // exception: change bg color for action badges above badge-title + .bg-light { + background-color: $chill-light-gray !important; } - - } - ul.list-content { - list-style-type: none; - padding-left: 1em; - margin: 0 0; - li { - margin-bottom: 0.2em; - } - } - } - div.duration { - font-size: smaller; - padding-left: 1em; - margin-top: 1em; - } - } + } + } +} +div.main { + padding: 1em; } -// exceptions for flex-bloc in concerned-groups +//// ACTIVITY SHOW AND FORM PAGES +// Exceptions for flex-bloc in concerned-groups + div.flex-bloc.concerned-groups { margin-top: 1em; div.item-bloc { @@ -130,7 +77,6 @@ div.flex-bloc.concerned-groups { } } - /// CHILL ENTITY RENDER BOX .chill-entity { diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/App.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/App.vue index 07fe84319..52454c2f7 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/App.vue +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/App.vue @@ -1,17 +1,20 @@ diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/api.js b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/api.js index 79b4ec8fc..9dab14ef1 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/api.js +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/api.js @@ -3,16 +3,61 @@ import { getSocialIssues } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api. /* * Load socialActions by socialIssue (id) */ -const getSocialActionByIssue = (id) => { +const getSocialActionByIssue = (id) => { const url = `/api/1.0/person/social/social-action/by-social-issue/${id}.json`; return fetch(url) .then(response => { if (response.ok) { return response.json(); } - throw Error('Error with request resource response'); + throw Error('Error with request resource response'); }); }; +/* +* Load Locations + */ +const getLocations = () => { + const url = `/api/1.0/main/location.json`; + return fetch(url) + .then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); +}; + +/* +* Load Location Types + */ +const getLocationTypes = () => { + const url = `/api/1.0/main/location-type.json`; + return fetch(url) + .then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); +}; + +/* +* Post a Location + */ +const postLocation = (body) => { + const url = `/api/1.0/main/location.json`; + return fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + body: JSON.stringify(body) + }) + .then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); +}; + export { getSocialIssues, - getSocialActionByIssue + getSocialActionByIssue, + getLocations, + getLocationTypes, + postLocation }; diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/ConcernedGroups.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/ConcernedGroups.vue index 1cbd299dc..108e1e493 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/ConcernedGroups.vue +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/ConcernedGroups.vue @@ -73,9 +73,12 @@ export default { addPersons: { key: 'activity', options: { - type: ['person', 'thirdparty', 'user'], // TODO add 'user' + type: ['person', 'thirdparty', 'user'], priority: null, uniq: false, + button: { + size: 'btn-sm' + } } } } diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location.vue new file mode 100644 index 000000000..0dbf2652a --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location.vue @@ -0,0 +1,97 @@ + + + diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue new file mode 100644 index 000000000..3ffb2a33f --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue @@ -0,0 +1,181 @@ + + + diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc.vue index 14a3b9526..06e0b197c 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc.vue +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/SocialIssuesAcc.vue @@ -1,12 +1,12 @@ - diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/i18n.js new file mode 100644 index 000000000..3cba67149 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/i18n.js @@ -0,0 +1,24 @@ +const ontheflyMessages = { + fr: { + onthefly: { + show: { + person: "Détails de l'usager", + thirdparty: "Détails du tiers", + file_person: "Ouvrir la fiche de l'usager", + file_thirdparty: "Voir le Tiers", + }, + edit: { + person: "Modifier un usager", + thirdparty: "Modifier un tiers" + }, + create: { + button: "Créer \"{q}\"", + title: "Création d'un nouvel usager ou d'un tiers professionnel", + person: "un nouvel usager", + thirdparty: "un nouveau tiers professionnel" + }, + } + } +} + +export { ontheflyMessages }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/index.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/index.js new file mode 100644 index 000000000..88e6aad5e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/index.js @@ -0,0 +1,35 @@ +import { createApp } from "vue"; +import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'; +import { ontheflyMessages } from './i18n.js'; +import App from "./App.vue"; + +const i18n = _createI18n( ontheflyMessages ); + +let containers = document.querySelectorAll('.onthefly-container'); + +containers.forEach((container) => { + + const app = createApp({ + template: ``, + data() { + return { + onTheFly: { + context: { + action: container.dataset.action, + type: container.dataset.targetName, + id: parseInt(container.dataset.targetId), + }, + options: { + buttonText: container.dataset.buttonText || null, + displayBadge: container.dataset.displayBadge || false + } + } + } + } + }) + .use(i18n) + .component('app', App) + .mount(container); + + //console.log('container dataset', container.dataset); +}); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js index 5e001a21b..c16b4c65d 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js @@ -53,24 +53,6 @@ const messages = { top: "Haut", bottom: "Bas", }, - onthefly: { - show: { - person: "Détails de l'usager", - thirdparty: "Détails du tiers", - file_person: "Ouvrir la fiche de l'usager", - file_thirdparty: "Voir le Tiers", - }, - edit: { - person: "Modifier un usager", - thirdparty: "Modifier un tiers" - }, - create: { - button: "Créer \"{q}\"", - title: "Création d'un nouvel usager ou d'un tiers professionnel", - person: "un nouvel usager", - thirdparty: "un nouveau tiers professionnel" - }, - }, renderbox: { person: "Usager", birthday: { diff --git a/src/Bundle/ChillMainBundle/Resources/views/Address/_insert_vue_address.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Address/_insert_vue_address.html.twig index f4f63c4eb..571f4257f 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Address/_insert_vue_address.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Address/_insert_vue_address.html.twig @@ -18,7 +18,7 @@ * stickyActions bool (default: false) * useValidFrom bool (default: false) * useValidTo bool (default: false) - * hideAddress bool (default: false) + * onlyButton bool (default: false) #}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_edit_content.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_edit_content.html.twig index 5e338d5fd..a26eb2e4a 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_edit_content.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_edit_content.html.twig @@ -1,5 +1,5 @@ {% set formId = crudMainFormId|default('crud_main_form') %} -
+
{% block crud_content_header %}

{{ ('crud.'~crud_name~'.title_edit')|trans }}

{% endblock crud_content_header %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_index.html.twig index 46828a49e..15774bdf6 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_index.html.twig @@ -1,9 +1,15 @@
- + {% block index_header %}

{{ ('crud.' ~ crud_name ~ '.index.title')|trans({'%crud_name%': crud_name}) }}

{% endblock index_header %} +{% block filter_order %} + {% if filter_order is not null %} + {{ filter_order|chill_render_filter_order_helper }} + {% endif %} +{% endblock %} + {% if entities|length == 0 %} {% block no_existing_entities %}

{{ no_existing_entities_sentences|default('No entities')|trans }}

@@ -32,17 +38,20 @@ {% endif %} -
- {{ chill_pagination(paginator) }} -
+{% block pagination %} +
+ {{ chill_pagination(paginator) }} +
+{% endblock %} {% block list_actions %} {% endblock list_actions %}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_new_content.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_new_content.html.twig index 4c2003617..906a77d0d 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_new_content.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_new_content.html.twig @@ -1,10 +1,11 @@ +{% set formId = crudMainFormId|default('crud_main_form') %}
{% block crud_content_header %}

{{ ('crud.' ~ crud_name ~ '.title_new')|trans({'%crud_name%' : crud_name }) }}

{% endblock crud_content_header %} {% block crud_content_form %} - {{ form_start(form) }} + {{ form_start(form, { 'attr' : { 'id': formId } }) }} {% block crud_content_form_rows %} {% for f in form %}{% if f.vars.name != 'submit' %} @@ -14,6 +15,8 @@ {% block crud_content_after_form %}{% endblock %} + {{ form_end(form) }} + {% block crud_content_form_actions %}
    {% block content_form_actions_back %} @@ -25,21 +28,21 @@ {% endblock %} {% block content_form_actions_save_and_close %}
  • -
  • {% endblock %} {% block content_form_actions_save_and_show %}
  • -
  • {% endblock %} {% block content_form_actions_save_and_new %}
  • -
  • diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig new file mode 100644 index 000000000..074f3bb94 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -0,0 +1,12 @@ +{{ form_start(form) }} +
    +
    +
    +
    + {{ form_widget(form.q)}} + +
    +
    +
    +
    +{{ form_end(form) }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig index ce406286b..1426841bb 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig @@ -166,8 +166,13 @@
  • {{ form_widget(entry) }} + {{ form_errors(entry) }}
  • + {% else %} +
  • + {{ form.vars.empty_collection_explain|default('No item')|trans }} +
  • {% endfor %}
@@ -198,3 +203,15 @@ {{ form_widget(entry) }} {% endfor %} {% endblock comment_widget %} + +{% block pick_center_widget %} + {{ form_widget(form.center) }} +{% endblock pick_center_widget %} + +{% block pick_center_row %} + {% if (not form.vars.is_hidden) %} + {{ block('form_row') }} + {% else %} + {{ form_widget(form.center) }} + {% endif %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Menu/defaultMenu.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Menu/defaultMenu.html.twig index 67a32b98b..6eada4dd6 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Menu/defaultMenu.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Menu/defaultMenu.html.twig @@ -1,5 +1,5 @@ {# - * Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS, + * Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS, / * * This program is free software: you can redistribute it and/or modify @@ -17,7 +17,7 @@ #} \ No newline at end of file + diff --git a/src/Bundle/ChillMainBundle/Resources/views/OnTheFly/_insert_vue_onthefly.html.twig b/src/Bundle/ChillMainBundle/Resources/views/OnTheFly/_insert_vue_onthefly.html.twig new file mode 100644 index 000000000..e63fd9ee0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/OnTheFly/_insert_vue_onthefly.html.twig @@ -0,0 +1,37 @@ +{# + This Twig template include load vue_onthefly component. + It push all variables from context in OnTheFly/App.vue. + + OPTIONS + * targetEntity { + name: string 'person', 'thirdparty' + id: integer + } + * action string 'show', 'edit', 'create' + * buttonText string + * displayBadge boolean (default: false) replace button by badge, need to define buttonText for content + +#} + + +{{ encore_entry_script_tags('vue_onthefly') }} +{{ encore_entry_link_tags('vue_onthefly') }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/User/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/User/index.html.twig index c57d9ed5f..53bd89482 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/User/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/User/index.html.twig @@ -10,7 +10,7 @@ {% endblock %} {% block table_entities_tbody %} {% for entity in entities %} - + {% if entity.isEnabled %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig index 27fc4430b..8bee2983e 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig @@ -70,6 +70,9 @@
+ {{ chill_widget('homepage', {} ) }} {% endblock %} diff --git a/src/Bundle/ChillMainBundle/Search/Entity/SearchUserApiProvider.php b/src/Bundle/ChillMainBundle/Search/Entity/SearchUserApiProvider.php index e4061cea1..6b8139419 100644 --- a/src/Bundle/ChillMainBundle/Search/Entity/SearchUserApiProvider.php +++ b/src/Bundle/ChillMainBundle/Search/Entity/SearchUserApiProvider.php @@ -27,7 +27,7 @@ class SearchUserApiProvider implements SearchApiInterface ->setSelectPertinence("GREATEST(SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical), SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical))", [ $pattern, $pattern ]) ->setFromClause("users AS u") - ->setWhereClause("SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15 + ->setWhereClauses("SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15 OR SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical) > 0.15 ", [ $pattern, $pattern ]); diff --git a/src/Bundle/ChillMainBundle/Search/SearchApiQuery.php b/src/Bundle/ChillMainBundle/Search/SearchApiQuery.php index 2c986ee6c..9188c0b5c 100644 --- a/src/Bundle/ChillMainBundle/Search/SearchApiQuery.php +++ b/src/Bundle/ChillMainBundle/Search/SearchApiQuery.php @@ -12,8 +12,8 @@ class SearchApiQuery private array $pertinenceParams = []; private ?string $fromClause = null; private array $fromClauseParams = []; - private ?string $whereClause = null; - private array $whereClauseParams = []; + private array $whereClauses = []; + private array $whereClausesParams = []; public function setSelectKey(string $selectKey, array $params = []): self { @@ -47,16 +47,39 @@ class SearchApiQuery 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->whereClauseParams = $params; + $this->whereClauses = [$whereClause]; + $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; } public function buildQuery(): string { + $where = \implode(' AND ', $this->whereClauses); + return \strtr("SELECT '{key}' AS key, {metadata} AS metadata, @@ -68,7 +91,7 @@ class SearchApiQuery '{metadata}' => $this->jsonbMetadata, '{pertinence}' => $this->pertinence, '{from}' => $this->fromClause, - '{where}' => $this->whereClause, + '{where}' => $where, ]); } @@ -79,7 +102,7 @@ class SearchApiQuery $this->jsonbMetadataParams, $this->pertinenceParams, $this->fromClauseParams, - $this->whereClauseParams, + \array_merge([], ...$this->whereClausesParams), ); } } diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php index 2b223a0fe..afff931ed 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php @@ -41,9 +41,8 @@ use Chill\MainBundle\Entity\RoleScope; * * Provides methods for user and entities information. * - * @author Julien Fastré */ -class AuthorizationHelper +class AuthorizationHelper implements AuthorizationHelperInterface { protected RoleHierarchyInterface $roleHierarchy; @@ -203,9 +202,9 @@ class AuthorizationHelper * @param User $user * @param string|Role $role * @param null|Scope $scope - * @return Center[] + * @return Center[]|array */ - public function getReachableCenters(User $user, $role, Scope $scope = null) + public function getReachableCenters(User $user, string $role, ?Scope $scope = null): array { if ($role instanceof Role) { $role = $role->getRole(); @@ -267,9 +266,9 @@ class AuthorizationHelper * @param User $user * @param string role * @param Center|Center[] $center - * @return Scope[] + * @return Scope[]|array */ - public function getReachableScopes(User $user, $role, $center) + public function getReachableScopes(User $user, string $role, $center): array { if ($role instanceof Role) { $role = $role->getRole(); diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php new file mode 100644 index 000000000..31709bcd5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php @@ -0,0 +1,32 @@ +getExtra(); $data['validFrom'] = $address->getValidFrom(); $data['validTo'] = $address->getValidTo(); + $data['addressReference'] = $this->normalizer->normalize($address->getAddressReference(), $format, [ + AbstractNormalizer::GROUPS => ['read'] + ]); return $data; } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php new file mode 100644 index 000000000..0b9eb60bb --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -0,0 +1,60 @@ +formFactory = $formFactory; + $this->requestStack = $requestStack; + } + + public function setSearchBox($searchBoxFields = null): self + { + $this->searchBoxFields = $searchBoxFields; + + return $this; + } + + public function hasSearchBox(): bool + { + return $this->searchBoxFields !== null; + } + + private function getFormData(): array + { + return [ + 'q' => $this->getQueryString() + ]; + } + + public function getQueryString(): ?string + { + $q = $this->requestStack->getCurrentRequest() + ->query->get('q', null); + + return empty($q) ? NULL : $q; + } + + public function buildForm($name = null, string $type = FilterOrderType::class, array $options = []): FormInterface + { + return $this->formFactory + ->createNamed($name, $type, $this->getFormData(), \array_merge([ + 'helper' => $this, + 'method' => 'GET', + 'csrf_protection' => false, + ], $options)); + } +} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php new file mode 100644 index 000000000..ec0f6b60b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -0,0 +1,40 @@ +formFactory = $formFactory; + $this->requestStack = $requestStack; + } + + public function addSearchBox(?array $fields = [], ?array $options = []): self + { + $this->searchBoxFields = $fields; + + return $this; + } + + public function build(): FilterOrderHelper + { + $helper = new FilterOrderHelper( + $this->formFactory, + $this->requestStack + ); + + $helper->setSearchBox($this->searchBoxFields); + + return $helper; + } +} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php new file mode 100644 index 000000000..1b1c4c983 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php @@ -0,0 +1,26 @@ +formFactory = $formFactory; + $this->requestStack = $requestStack; + } + + public function create(string $context, ?array $options = []): FilterOrderHelperBuilder + { + return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack); + } +} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactoryInterface.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactoryInterface.php new file mode 100644 index 000000000..a222adf7a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactoryInterface.php @@ -0,0 +1,8 @@ + true, 'is_safe' => ['html'], + ]) + ]; + } + + public function renderFilterOrderHelper( + Environment $environment, + FilterOrderHelper $helper, + ?string $template = '@ChillMain/FilterOrder/base.html.twig', + ?array $options = [] + ) { + return $environment->render($template, [ + 'helper' => $helper, + 'form' => $helper->buildForm()->createView(), + 'options' => $options + ]); + } + +} diff --git a/src/Bundle/ChillMainBundle/Test/PrepareClientTrait.php b/src/Bundle/ChillMainBundle/Test/PrepareClientTrait.php index 0ae7c235a..5df0b57dd 100644 --- a/src/Bundle/ChillMainBundle/Test/PrepareClientTrait.php +++ b/src/Bundle/ChillMainBundle/Test/PrepareClientTrait.php @@ -17,32 +17,31 @@ */ namespace Chill\MainBundle\Test; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** - * Prepare a client authenticated with a user - * - * @author Julien Fastré + * Prepare a client authenticated with a user */ trait PrepareClientTrait { /** * Create a new client with authentication information. - * + * * @param string $username the username (default 'center a_social') * @param string $password the password (default 'password') * @return \Symfony\Component\BrowserKit\Client * @throws \LogicException */ public function getClientAuthenticated( - $username = 'center a_social', + $username = 'center a_social', $password = 'password' - ) { + ): KernelBrowser { if (!$this instanceof WebTestCase) { throw new \LogicException(sprintf("The current class does not " . "implements %s", WebTestCase::class)); } - + return static::createClient(array(), array( 'PHP_AUTH_USER' => $username, 'PHP_AUTH_PW' => $password, diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/AddressControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/AddressControllerTest.php new file mode 100644 index 000000000..2000a05bf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/AddressControllerTest.php @@ -0,0 +1,49 @@ +client = $this->getClientAuthenticated(); + } + + /** + * @dataProvider generateAddressIds + * @param int $addressId + */ + public function testDuplicate(int $addressId) + { + $this->client->request('POST', "/api/1.0/main/address/$addressId/duplicate.json"); + + $this->assertResponseIsSuccessful('test that duplicate is successful'); + } + + public function generateAddressIds() + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $qb = $em->createQueryBuilder(); + $addresses = $qb->select('a')->from(Address::class, 'a') + ->setMaxResults(2) + ->getQuery() + ->getResult(); + + foreach ($addresses as $a) { + yield [ $a->getId() ]; + } + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/UserControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/UserControllerTest.php index 1953439dd..0017fbb4e 100644 --- a/src/Bundle/ChillMainBundle/Tests/Controller/UserControllerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Controller/UserControllerTest.php @@ -2,12 +2,17 @@ namespace Chill\MainBundle\Tests\Controller; +use Chill\MainBundle\Entity\User; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; class UserControllerTest extends WebTestCase { private $client; + private array $toDelete = []; + public function setUp() { self::bootKernel(); @@ -22,18 +27,14 @@ class UserControllerTest extends WebTestCase public function testList() { // get the list - $crawler = $this->client->request('GET', '/fr/admin/user/'); + $crawler = $this->client->request('GET', '/fr/admin/main/user'); $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), - "Unexpected HTTP status code for GET /admin/user/"); - - $link = $crawler->selectLink('Ajouter un nouvel utilisateur')->link(); - $this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $link); - $this->assertRegExp('|/fr/admin/user/new$|', $link->getUri()); + "Unexpected HTTP status code for GET /admin/main/user"); } public function testNew() { - $crawler = $this->client->request('GET', '/fr/admin/user/new'); + $crawler = $this->client->request('GET', '/fr/admin/main/user/new'); $username = 'Test_user'. uniqid(); $password = 'Password1234!'; @@ -54,22 +55,15 @@ class UserControllerTest extends WebTestCase $this->assertGreaterThan(0, $crawler->filter('td:contains("Test_user")')->count(), 'Missing element td:contains("Test user")'); - $update = $crawler->selectLink('Modifier')->link(); - - $this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $update); - $this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit$|', $update->getUri()); - //test the auth of the new client $this->isPasswordValid($username, $password); - - return $update; } protected function isPasswordValid($username, $password) { /* @var $passwordEncoder \Symfony\Component\Security\Core\Encoder\UserPasswordEncoder */ - $passwordEncoder = self::$kernel->getContainer() - ->get('security.password_encoder'); + $passwordEncoder = self::$container + ->get(UserPasswordEncoderInterface::class); $user = self::$kernel->getContainer() ->get('doctrine.orm.entity_manager') @@ -81,46 +75,33 @@ class UserControllerTest extends WebTestCase /** * - * @param \Symfony\Component\DomCrawler\Link $update - * @depends testNew + * @dataProvider dataGenerateUserId */ - public function testUpdate(\Symfony\Component\DomCrawler\Link $update) + public function testUpdate(int $userId, string $username) { - $crawler = $this->client->click($update); + $crawler = $this->client->request('GET', "/fr/admin/main/user/$userId/edit"); $username = 'Foo bar '.uniqid(); - $form = $crawler->selectButton('Mettre à jour')->form(array( + $form = $crawler->selectButton('Enregistrer & fermer')->form(array( 'chill_mainbundle_user[username]' => $username, )); $this->client->submit($form); $crawler = $this->client->followRedirect(); // Check the element contains an attribute with value equals "Foo" - $this->assertGreaterThan(0, $crawler->filter('[value="'.$username.'"]')->count(), - 'Missing element [value="Foo bar"]'); - - $updatePassword = $crawler->selectLink('Modifier le mot de passe')->link(); - - $this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $updatePassword); - $this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit_password$|', - $updatePassword->getUri()); - - return array('link' => $updatePassword, 'username' => $username); + $this->assertGreaterThan(0, $crawler->filter('[data-username="'.$username.'"]')->count(), + 'Missing element [data-username="Foo bar"]'); } /** * - * @param \Symfony\Component\DomCrawler\Link $updatePassword - * @depends testUpdate + * @dataProvider dataGenerateUserId */ - public function testUpdatePassword(array $params) + public function testUpdatePassword(int $userId, $username) { - $link = $params['link']; - $username = $params['username']; + $crawler = $this->client->request('GET', "/fr/admin/user/$userId/edit_password"); $newPassword = '1234Password!'; - $crawler = $this->client->click($link); - $form = $crawler->selectButton('Changer le mot de passe')->form(array( 'chill_mainbundle_user_password[new_password][first]' => $newPassword, 'chill_mainbundle_user_password[new_password][second]' => $newPassword, @@ -130,10 +111,38 @@ class UserControllerTest extends WebTestCase $this->assertTrue($this->client->getResponse()->isRedirect(), "the response is a redirection"); - $this->client->followRedirect(); $this->isPasswordValid($username, $newPassword); } + 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 dataGenerateUserId() + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $user = new User(); + $user->setUsername('Test_user '.uniqid()); + $user->setPassword(self::$container->get(UserPasswordEncoderInterface::class)->encodePassword($user, + 'password')); + + $em->persist($user); + $em->flush(); + + $this->toDelete[] = [User::class, $user->getId()]; + + yield [ $user->getId(), $user->getUsername() ]; + } } diff --git a/src/Bundle/ChillMainBundle/Tests/Form/Type/CenterTypeTest.php b/src/Bundle/ChillMainBundle/Tests/Form/Type/PickCenterTypeTest.php similarity index 91% rename from src/Bundle/ChillMainBundle/Tests/Form/Type/CenterTypeTest.php rename to src/Bundle/ChillMainBundle/Tests/Form/Type/PickCenterTypeTest.php index 48b96375e..a72360301 100644 --- a/src/Bundle/ChillMainBundle/Tests/Form/Type/CenterTypeTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Form/Type/PickCenterTypeTest.php @@ -28,18 +28,19 @@ use Symfony\Bridge\Doctrine\Form\Type\EntityType; /** - * + * * * @author Julien Fastré */ class CenterTypeTest extends TypeTestCase { /** - * Test that a user which can reach only one center + * Test that a user which can reach only one center * render as an hidden field */ public function testUserCanReachSingleCenter() { + $this->markTestSkipped(); //prepare user $center = $this->prepareCenter(1, 'center'); $groupCenter = (new GroupCenter()) @@ -47,18 +48,19 @@ class CenterTypeTest extends TypeTestCase ; $user = (new User()) ->addGroupCenter($groupCenter); - + $type = $this->prepareType($user); - + $this->assertEquals(HiddenType::class, $type->getParent()); } - + /** - * Test that a user which can reach only one center + * Test that a user which can reach only one center * render as an hidden field */ public function testUserCanReachMultipleSameCenter() { + $this->markTestSkipped(); //prepare user $center = $this->prepareCenter(1, 'center'); $groupCenterA = (new GroupCenter()) @@ -70,18 +72,19 @@ class CenterTypeTest extends TypeTestCase $user = (new User()) ->addGroupCenter($groupCenterA) ->addGroupCenter($groupCenterB); - + $type = $this->prepareType($user); - + $this->assertEquals(HiddenType::class, $type->getParent()); } - + /** - * Test that a user which can reach multiple center + * Test that a user which can reach multiple center * make CenterType render as "entity" type. */ public function testUserCanReachMultipleCenters() { + $this->markTestSkipped(); //prepare user $centerA = $this->prepareCenter(1, 'centerA'); $centerB = $this->prepareCenter(2, 'centerB'); @@ -95,61 +98,61 @@ class CenterTypeTest extends TypeTestCase ->addGroupCenter($groupCenterA) ->addGroupCenter($groupCenterB) ; - + $type = $this->prepareType($user); - + $this->assertEquals(EntityType::class, $type->getParent()); } - + /** * prepare a mocked center, with and id and name given - * + * * @param int $id * @param string $name - * @return \Chill\MainBundle\Entity\Center + * @return \Chill\MainBundle\Entity\Center */ private function prepareCenter($id, $name) { $prophet = new \Prophecy\Prophet; - + $prophecyCenter = $prophet->prophesize(); $prophecyCenter->willExtend('\Chill\MainBundle\Entity\Center'); $prophecyCenter->getId()->willReturn($id); $prophecyCenter->getName()->willReturn($name); - + return $prophecyCenter->reveal(); } - - + + /** * prepare the type with mocked center transformer and token storage - * + * * @param User $user the user for wich the form will be prepared * @return CenterType */ private function prepareType(User $user) { - $prophet = new \Prophecy\Prophet; - + $prophet = new \Prophecy\Prophet; + //create a center transformer $centerTransformerProphecy = $prophet->prophesize(); $centerTransformerProphecy ->willExtend('Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer'); $transformer = $centerTransformerProphecy->reveal(); - + $tokenProphecy = $prophet->prophesize(); $tokenProphecy ->willImplement('\Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $tokenProphecy->getUser()->willReturn($user); $token = $tokenProphecy->reveal(); - + $tokenStorageProphecy = $prophet->prophesize(); $tokenStorageProphecy ->willExtend('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage'); $tokenStorageProphecy->getToken()->willReturn($token); $tokenStorage = $tokenStorageProphecy->reveal(); - + return new CenterType($tokenStorage, $transformer); } - + } diff --git a/src/Bundle/ChillMainBundle/Tests/Search/SearchApiQueryTest.php b/src/Bundle/ChillMainBundle/Tests/Search/SearchApiQueryTest.php new file mode 100644 index 000000000..2e63f24e0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Search/SearchApiQueryTest.php @@ -0,0 +1,40 @@ +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()); + } + +} diff --git a/src/Bundle/ChillMainBundle/Tests/Services/ChillMenuTwigFunctionTest.php b/src/Bundle/ChillMainBundle/Tests/Services/ChillMenuTwigFunctionTest.php deleted file mode 100644 index a85ff9710..000000000 --- a/src/Bundle/ChillMainBundle/Tests/Services/ChillMenuTwigFunctionTest.php +++ /dev/null @@ -1,54 +0,0 @@ - - * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace Chill\MainBundle\Tests\Services; - -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\DomCrawler\Crawler; - -/** - * Test the Twig function 'chill_menu' - * - * @author Julien Fastré - */ -class ChillMenuTwigFunctionTest extends KernelTestCase -{ - - private static $templating; - - public static function setUpBeforeClass() - { - self::bootKernel(array('environment' => 'test')); - static::$templating = static::$container - ->get('templating'); - $pathToBundle = static::$container->getParameter('kernel.bundles_metadata')['ChillMainBundle']['path']; - //load templates in Tests/Resources/views - static::$container->get('twig.loader') - ->addPath($pathToBundle.'/Resources/test/views/', $namespace = 'tests'); - } - - public function testNormalMenu() - { - $content = static::$templating->render('@tests/menus/normalMenu.html.twig'); - $this->assertContains('ul', $content, - "test that the file contains an ul tag" - ); - } -} diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index 8ee69e96d..8252502ba 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -293,6 +293,32 @@ paths: 401: description: "Unauthorized" + /1.0/main/address/{id}/duplicate.json: + post: + tags: + - address + summary: Duplicate an existing address + parameters: + - name: id + in: path + required: true + description: The address id that will be duplicated + schema: + type: integer + format: integer + minimum: 1 + responses: + 200: + description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/Address' + 404: + description: "not found" + 401: + description: "Unauthorized" + /1.0/main/address-reference.json: get: tags: @@ -511,3 +537,89 @@ paths: description: "ok" 401: description: "Unauthorized" + + /1.0/main/location.json: + get: + tags: + - location + summary: Return a list of locations + responses: + 200: + description: "ok" + 401: + description: "Unauthorized" + post: + tags: + - location + summary: create a new location + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + type: + type: string + name: + type: string + phonenumber1: + type: string + phonenumber2: + type: string + email: + type: string + address: + type: object + properties: + id: + type: integer + locationType: + type: object + properties: + id: + type: integer + type: + type: string + responses: + 401: + description: "Unauthorized" + 404: + description: "Not found" + 200: + description: "OK" + 422: + description: "Unprocessable entity (validation errors)" + 400: + description: "transition cannot be applyed" + + /1.0/main/location/{id}.json: + get: + tags: + - location + summary: Return the given location + parameters: + - name: id + in: path + required: true + description: The location id + schema: + type: integer + format: integer + minimum: 1 + responses: + 200: + description: "ok" + 401: + description: "Unauthorized" + + /1.0/main/location-type.json: + get: + tags: + - location + summary: Return a list of location types + responses: + 200: + description: "ok" + 401: + description: "Unauthorized" diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js index 7ba42ed97..733899c19 100644 --- a/src/Bundle/ChillMainBundle/chill.webpack.config.js +++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js @@ -58,8 +58,11 @@ module.exports = function(encore, entries) encore.addEntry('mod_forkawesome', __dirname + '/Resources/public/module/forkawesome/index.js'); encore.addEntry('mod_bootstrap', __dirname + '/Resources/public/module/bootstrap/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_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js'); // Vue entrypoints encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); + encore.addEntry('vue_onthefly', __dirname + '/Resources/public/vuejs/OnTheFly/index.js'); }; diff --git a/src/Bundle/ChillMainBundle/config/services.yaml b/src/Bundle/ChillMainBundle/config/services.yaml index 53e602998..6540069fa 100644 --- a/src/Bundle/ChillMainBundle/config/services.yaml +++ b/src/Bundle/ChillMainBundle/config/services.yaml @@ -60,11 +60,6 @@ services: tags: - { name: twig.extension } - chill.main.form.data_transformer.center_transformer: - class: Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer - arguments: - - "@doctrine.orm.entity_manager" - chill.main.validator.role_scope_scope_presence: class: Chill\MainBundle\Validation\Validator\RoleScopeScopePresence arguments: diff --git a/src/Bundle/ChillMainBundle/config/services/form.yaml b/src/Bundle/ChillMainBundle/config/services/form.yaml index f719edb55..f2e5b1fc4 100644 --- a/src/Bundle/ChillMainBundle/config/services/form.yaml +++ b/src/Bundle/ChillMainBundle/config/services/form.yaml @@ -1,4 +1,5 @@ services: + chill.main.form.type.translatable.string: class: Chill\MainBundle\Form\Type\TranslatableStringFormType arguments: @@ -36,13 +37,9 @@ services: tags: - { name: form.type, alias: select2_chill_language } - chill.main.form.type.center: - class: Chill\MainBundle\Form\Type\CenterType - arguments: - - "@security.token_storage" - - "@chill.main.form.data_transformer.center_transformer" - tags: - - { name: form.type, alias: center } + Chill\MainBundle\Form\Type\PickCenterType: + autowire: true + autoconfigure: true chill.main.form.type.composed_role_scope: class: Chill\MainBundle\Form\Type\ComposedRoleScopeType @@ -97,6 +94,10 @@ services: arguments: - '@Chill\MainBundle\Export\ExportManager' + Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer: + autowire: true + autoconfigure: true + chill.main.form.advanced_search_type: class: Chill\MainBundle\Form\AdvancedSearchType autowire: true @@ -128,3 +129,11 @@ services: tags: - { name: form.type } + + Chill\MainBundle\Form\Type\PickAddressType: + autoconfigure: true + autowire: true + + Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer: + autoconfigure: true + autowire: true diff --git a/src/Bundle/ChillMainBundle/config/services/security.yaml b/src/Bundle/ChillMainBundle/config/services/security.yaml index 0d820220b..15d6f7da5 100644 --- a/src/Bundle/ChillMainBundle/config/services/security.yaml +++ b/src/Bundle/ChillMainBundle/config/services/security.yaml @@ -38,6 +38,7 @@ services: autowire: true autoconfigure: true Chill\MainBundle\Security\Authorization\AuthorizationHelper: '@chill.main.security.authorization.helper' + Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface: '@chill.main.security.authorization.helper' chill.main.role_provider: class: Chill\MainBundle\Security\RoleProvider diff --git a/src/Bundle/ChillMainBundle/config/services/templating.yaml b/src/Bundle/ChillMainBundle/config/services/templating.yaml index cd35a6466..e264f3a99 100644 --- a/src/Bundle/ChillMainBundle/config/services/templating.yaml +++ b/src/Bundle/ChillMainBundle/config/services/templating.yaml @@ -36,7 +36,7 @@ services: autowire: true tags: - { name: 'chill.render_entity' } - + Chill\MainBundle\Templating\ChillMarkdownRenderExtension: tags: - { name: twig.extension } @@ -46,4 +46,10 @@ services: - '@Symfony\Component\Templating\EngineInterface' tags: - { name: 'chill.render_entity' } - + + Chill\MainBundle\Templating\Listing\: + resource: './../../Templating/Listing' + autoconfigure: true + autowire: true + + Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface: '@Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory' diff --git a/src/Bundle/ChillMainBundle/migrations/Version20210929192242.php b/src/Bundle/ChillMainBundle/migrations/Version20210929192242.php new file mode 100644 index 000000000..a2fbefa69 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20210929192242.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20211006151653.php b/src/Bundle/ChillMainBundle/migrations/Version20211006151653.php new file mode 100644 index 000000000..9f4508ba8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20211006151653.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE chill_main_postal_code ADD refPostalCodeId VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_postal_code ADD postalCodeSource VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_postal_code ADD center geometry(POINT,4326) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_postal_code DROP refPostalCodeId'); + $this->addSql('ALTER TABLE chill_main_postal_code DROP postalCodeSource'); + $this->addSql('ALTER TABLE chill_main_postal_code DROP center'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20211007150019.php b/src/Bundle/ChillMainBundle/migrations/Version20211007150019.php new file mode 100644 index 000000000..b81c7f74e --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20211007150019.php @@ -0,0 +1,31 @@ +addSql('CREATE SEQUENCE chill_main_civility_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_civility (id INT NOT NULL, name JSON NOT NULL, abbreviation JSON NOT NULL, active BOOLEAN NOT NULL, PRIMARY KEY(id))'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_civility_id_seq'); + $this->addSql('DROP TABLE chill_main_civility'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20211012141336.php b/src/Bundle/ChillMainBundle/migrations/Version20211012141336.php new file mode 100644 index 000000000..4ed8da1dc --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20211012141336.php @@ -0,0 +1,66 @@ +addSql('CREATE SEQUENCE chill_main_location_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE chill_main_location_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_location (id INT NOT NULL, address_id INT DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, phonenumber1 VARCHAR(64) DEFAULT NULL, phonenumber2 VARCHAR(64) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, availableForUsers BOOLEAN NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, locationType_id INT NOT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + + $this->addSql('CREATE INDEX IDX_90E4736AB8B0DA8E ON chill_main_location (locationType_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_90E4736AF5B7AF75 ON chill_main_location (address_id)'); + $this->addSql('CREATE INDEX IDX_90E4736A3174800F ON chill_main_location (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_90E4736A65FF1AEC ON chill_main_location (updatedBy_id)'); + + $this->addSql('COMMENT ON COLUMN chill_main_location.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_location.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('CREATE TABLE chill_main_location_type (id INT NOT NULL, title JSON NOT NULL, availableForUsers BOOLEAN NOT NULL, addressRequired VARCHAR(32) DEFAULT \'optional\' NOT NULL, contactData VARCHAR(32) DEFAULT \'optional\' NOT NULL, PRIMARY KEY(id))'); + + $this->addSql('ALTER TABLE chill_main_location ADD CONSTRAINT FK_90E4736AB8B0DA8E FOREIGN KEY (locationType_id) REFERENCES chill_main_location_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_location ADD CONSTRAINT FK_90E4736AF5B7AF75 FOREIGN KEY (address_id) REFERENCES chill_main_address (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_location ADD CONSTRAINT FK_90E4736A3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_location ADD CONSTRAINT FK_90E4736A65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + + $this->addSql('ALTER TABLE activity ADD location_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A64D218E FOREIGN KEY (location_id) REFERENCES chill_main_location (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_AC74095A64D218E ON activity (location_id)'); + + $this->addSql('ALTER TABLE chill_calendar.calendar ADD location_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_calendar.calendar ADD CONSTRAINT FK_712315AC64D218E FOREIGN KEY (location_id) REFERENCES chill_main_location (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_712315AC64D218E ON chill_calendar.calendar (location_id)'); + } + + public function down(Schema $schema): void + { + + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095A64D218E'); + $this->addSql('ALTER TABLE activity DROP location_id'); + + $this->addSql('ALTER TABLE chill_calendar.calendar DROP CONSTRAINT FK_712315AC64D218E'); + $this->addSql('ALTER TABLE chill_calendar.calendar DROP location_id'); + + $this->addSql('ALTER TABLE chill_main_location DROP CONSTRAINT FK_90E4736AB8B0DA8E'); + + $this->addSql('DROP SEQUENCE chill_main_location_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE chill_main_location_type_id_seq CASCADE'); + + $this->addSql('DROP TABLE chill_main_location'); + $this->addSql('DROP TABLE chill_main_location_type'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20211013124455.php b/src/Bundle/ChillMainBundle/migrations/Version20211013124455.php new file mode 100644 index 000000000..0d30cece5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20211013124455.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE activitytype ADD locationVisible SMALLINT DEFAULT 1 NOT NULL'); + $this->addSql('ALTER TABLE activitytype ADD locationLabel VARCHAR(255) DEFAULT \'\' NOT NULL'); + + // fix old migration !? + $this->addSql('ALTER TABLE activitytype ALTER category_id DROP DEFAULT'); + + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE activitytype DROP locationVisible'); + $this->addSql('ALTER TABLE activitytype DROP locationLabel'); + + // fix old migration !? + $this->addSql('ALTER TABLE activitytype ALTER category_id SET DEFAULT 1'); + + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20211015084653.php b/src/Bundle/ChillMainBundle/migrations/Version20211015084653.php new file mode 100644 index 000000000..12b230f10 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20211015084653.php @@ -0,0 +1,28 @@ +addSql('DROP INDEX uniq_90e4736af5b7af75'); + $this->addSql('CREATE INDEX IDX_90E4736AF5B7AF75 ON chill_main_location (address_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX IDX_90E4736AF5B7AF75'); + $this->addSql('CREATE UNIQUE INDEX uniq_90e4736af5b7af75 ON chill_main_location (address_id)'); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index d99a565c1..c026571d1 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -1,7 +1,7 @@ "This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License": "Ce programme est un logiciel libre: vous pouvez le redistribuer et/ou le modifier selon les termes de la licence GNU Affero GPL" User manual: Manuel d'utilisation Search: Rechercher -'Search persons, ...': 'Recherche des personnes, ...' +"Search persons, ...": "Recherche des personnes, ..." Person name: Nom / Prénom de la personne Login: Connexion Logout: Se déconnecter @@ -33,7 +33,7 @@ Cancel: Annuler Save: Enregistrer This form contains errors: Ce formulaire contient des erreurs Choose an user: Choisir un utilisateur -'You are going to leave a page with unsubmitted data. Are you sure you want to leave ?': "Vous allez quitter la page alors que des données n'ont pas été enregistrées. Êtes vous sûr de vouloir partir ?" +"You are going to leave a page with unsubmitted data. Are you sure you want to leave ?": "Vous allez quitter la page alors que des données n'ont pas été enregistrées. Êtes vous sûr de vouloir partir ?" No value: Aucune information Last updated by: Dernière mise à jour par Last updated on: Dernière mise à jour le @@ -56,6 +56,7 @@ centers: centres Centers: Centres comment: commentaire Comment: Commentaire +Any comment: Aucun commentaire # comment embeddable No comment associated: Aucun commentaire @@ -71,24 +72,26 @@ Postal code: Code postal Valid from: Valide à partir du Choose a postal code: Choisir un code postal address: - address_homeless: L'adresse est-elle celle d'un domicile fixe ? - real address: Adresse d'un domicile - consider homeless: N'est pas l'adresse d'un domicile (SDF) + address_homeless: L'adresse est-elle celle d'un domicile fixe ? + real address: Adresse d'un domicile + consider homeless: N'est pas l'adresse d'un domicile (SDF) address more: - floor: ét - corridor: coul - steps: esc - flat: appart - buildingName: résidence - extra: '' - distribution: cedex + floor: ét + corridor: coul + steps: esc + flat: appart + buildingName: résidence + extra: "" + distribution: cedex Create a new address: Créer une nouvelle adresse +Create an address: Créer une adresse +Update address: Modifier l'adresse #serach Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche. The domain %domain% is unknow. Please check your search.: Le domaine de recherche "%domain%" est inconnu. Merci de vérifier votre recherche. -Invalid terms : Recherche invalide -You should not have more than one domain. : Vous ne devriez pas avoir plus d'un domaine de recherche. +Invalid terms: Recherche invalide +You should not have more than one domain.: Vous ne devriez pas avoir plus d'un domaine de recherche. #used for page title Search %pattern%: Recherche de "%pattern%" Results %start%-%end% of %total%: Résultats %start%-%end% sur %total% @@ -113,9 +116,9 @@ Permissions Menu: Gestion des droits Permissions management of your chill installation: Gestion des permissions de votre instance #admin section -'Administration interface': Interface d'administration +"Administration interface": Interface d'administration Welcome to the admin section !: > - Bienvenue dans l'interface d'administration ! + Bienvenue dans l'interface d'administration ! #admin section for center's administration Create a new center: Créer un nouveau centre @@ -126,7 +129,7 @@ New center: Nouveau centre Center: Centre #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 Permission group "%name%": Groupe de permissions "%name%" Grant those permissions: Attribue ces permissions @@ -143,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 #admin section for users -user list: Liste des utilisateurs User edit: Modification d'un 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 @@ -166,8 +168,12 @@ Back to the user edition: Retour au formulaire d'édition Password successfully updated!: Mot de passe mis à jour Flags: Drapeaux +# admin section for users jobs +User jobs: Métiers + + #admin section for circles (old: scopes) -List circles: Liste des cercles +List circles: Cercles New circle: Nouveau cercle Circle: Cercle Circle edit: Modification du cercle @@ -210,7 +216,6 @@ Problem during download: Problème durant le téléchargement # sans valeur without data: sans valeur - #CSV List Formatter Add a number on first column: La première colonne est un numéro Number: Numéro @@ -228,9 +233,9 @@ Comma separated values (CSV): Valeurs séparées par des virgules (CSV - tableur Choose the format: Choisir le format # select2 -'select2.no_results': Aucun résultat -'select2.error_loading': Erreur de chargement des résultats -'select2.searching': Recherche en cours... +"select2.no_results": Aucun résultat +"select2.error_loading": Erreur de chargement des résultats +"select2.searching": Recherche en cours... # change password Change my password: Modification du mot de passe @@ -258,38 +263,46 @@ Impersonate: Incarner l'utilisateur Impersonate mode: Mode fantôme crud: - # general items - new: - button_action_form: Créer - link_edit: Modifier - save_and_close: Créer & fermer - save_and_show: Créer & voir - save_and_new: Créer & nouveau - success: Les données ont été créées - edit: - button_action_form: Enregistrer - back_to_view: Voir - save_and_close: Enregistrer & fermer - save_and_show: Enregistrer & voir - success: Les données ont été modifiées - delete: - success: Les données ont été supprimées - link_to_form: Supprimer - default: - success: Les données ont été enregistrées - view: - link_duplicate: Dupliquer - ## admin for users - admin_user: - index: - title: Utilisateurs - add_new: "Créer" - is_active: "Actif ?" - usernames: "Identifiants" - mains: "Champs principaux" - title_new: "Nouvel utilisateur" - title_edit: Modifier un utilisateur + # general items + new: + button_action_form: Créer + link_edit: Modifier + save_and_close: Créer & fermer + save_and_show: Créer & voir + save_and_new: Créer & nouveau + success: Les données ont été créées + edit: + button_action_form: Enregistrer + back_to_view: Voir + save_and_close: Enregistrer & fermer + save_and_show: Enregistrer & voir + success: Les données ont été modifiées + delete: + success: Les données ont été supprimées + link_to_form: Supprimer + default: + success: Les données ont été enregistrées + view: + 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 CHILL_FOO_SEE: Voir un élément CHILL_FOO_EDIT: Modifier un élément + +#Show templates +Date: Date +By: Par +For: Pour +Created for: Créé pour +Created by: Créé par diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php index 1bf40d146..5c11f4802 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php @@ -118,6 +118,7 @@ class AccompanyingCourseController extends Controller return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [ 'accompanyingCourse' => $accompanyingCourse, 'withoutHousehold' => $withoutHousehold, + 'participationsByHousehold' => $accompanyingCourse->actualParticipationsByHousehold(), 'works' => $works, 'activities' => $activities ]); diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdApiController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdApiController.php index 0bb804689..11e41ba29 100644 --- a/src/Bundle/ChillPersonBundle/Controller/HouseholdApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdApiController.php @@ -4,24 +4,31 @@ namespace Chill\PersonBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\AddressReference; use Chill\MainBundle\Serializer\Model\Collection; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Household\Household; +use Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepositoryInterface; use Chill\PersonBundle\Repository\Household\HouseholdRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; class HouseholdApiController extends ApiController { private HouseholdRepository $householdRepository; - public function __construct(HouseholdRepository $householdRepository) - { + private HouseholdACLAwareRepositoryInterface $householdACLAwareRepository; + + public function __construct( + HouseholdRepository $householdRepository, + HouseholdACLAwareRepositoryInterface $householdACLAwareRepository + ) { $this->householdRepository = $householdRepository; + $this->householdACLAwareRepository = $householdACLAwareRepository; } - public function householdAddressApi($id, Request $request, string $_format): Response { @@ -37,7 +44,7 @@ class HouseholdApiController extends ApiController { // TODO add acl - $count = $this->householdRepository->countByAccompanyingPeriodParticipation($person); + $count = $this->householdRepository->countByAccompanyingPeriodParticipation($person); $paginator = $this->getPaginatorFactory()->create($count); if ($count === 0) { @@ -93,4 +100,27 @@ class HouseholdApiController extends ApiController return $this->json(\array_values($addresses), Response::HTTP_OK, [], [ 'groups' => [ 'read' ] ]); } + + /** + * + * @Route("/api/1.0/person/household/by-address-reference/{id}.json", + * name="chill_api_person_household_by_address_reference") + * @param AddressReference $addressReference + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getHouseholdByAddressReference(AddressReference $addressReference): Response + { + // TODO ACL + $this->denyAccessUnlessGranted('ROLE_USER'); + + $total = $this->householdACLAwareRepository->countByAddressReference($addressReference); + $paginator = $this->getPaginatorFactory()->create($total); + $households = $this->householdACLAwareRepository->findByAddressReference($addressReference, + $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage()); + $collection = new Collection($households, $paginator); + + return $this->json($collection, Response::HTTP_OK, [], [ + AbstractNormalizer::GROUPS => ['read'] + ]); + } } diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdController.php index 79391e904..1b9824184 100644 --- a/src/Bundle/ChillPersonBundle/Controller/HouseholdController.php +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdController.php @@ -167,6 +167,23 @@ class HouseholdController extends AbstractController ); } + /** + * @Route( + * "/{household_id}/relationship", + * name="chill_person_household_relationship", + * methods={"GET", "HEAD"} + * ) + * @ParamConverter("household", options={"id" = "household_id"}) + */ + public function showRelationship(Request $request, Household $household) + { + return $this->render('@ChillPerson/Household/relationship.html.twig', + [ + 'household' => $household + ] + ); + } + /** * @Route( * "/{household_id}/members/metadata/edit", diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonApiController.php b/src/Bundle/ChillPersonBundle/Controller/PersonApiController.php index b3f4087eb..ab39c13bb 100644 --- a/src/Bundle/ChillPersonBundle/Controller/PersonApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/PersonApiController.php @@ -45,9 +45,9 @@ class PersonApiController extends ApiController $person = parent::createEntity($action, $request); // TODO temporary hack to allow creation of person with fake center - $centers = $this->authorizationHelper->getReachableCenters($this->getUser(), + /* $centers = $this->authorizationHelper->getReachableCenters($this->getUser(), new Role(PersonVoter::CREATE)); - $person->setCenter($centers[0]); + $person->setCenter($centers[0]); */ return $person; } @@ -77,13 +77,6 @@ class PersonApiController extends ApiController $a = $participation->getAccompanyingPeriod()->getAddressLocation(); $addresses[$a->getId()] = $a; } - if (null !== $personLocation = $participation - ->getAccompanyingPeriod()->getPersonLocation()) { - $a = $personLocation->getCurrentHouseholdAddress(); - if (null !== $a) { - $addresses[$a->getId()] = $a; - } - } } // remove the actual address diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonController.php b/src/Bundle/ChillPersonBundle/Controller/PersonController.php index 03da97d5f..f9190857c 100644 --- a/src/Bundle/ChillPersonBundle/Controller/PersonController.php +++ b/src/Bundle/ChillPersonBundle/Controller/PersonController.php @@ -155,7 +155,7 @@ final class PersonController extends AbstractController $person = $this->_getPerson($person_id); if ($person === null) { - return $this->createNotFoundException(); + throw $this->createNotFoundException(); } $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php index 848e8f874..adc37d5b5 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php @@ -247,7 +247,6 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con if (\random_int(0, 10) > 3) { // always add social scope: $accompanyingPeriod->addScope($this->getReference('scope_social')); - var_dump(count($accompanyingPeriod->getScopes())); $accompanyingPeriod->setAddressLocation($this->createAddress()); $manager->persist($accompanyingPeriod->getAddressLocation()); diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 485185ce7..bc3b798fa 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -84,6 +84,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac $loader->load('services/repository.yaml'); $loader->load('services/serializer.yaml'); $loader->load('services/security.yaml'); + $loader->load('services/doctrineEventListener.yaml'); // load service advanced search only if configure if ($config['search']['search_by_phone'] != 'never') { @@ -640,12 +641,13 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac Request::METHOD_GET => true, Request::METHOD_HEAD => true, Request::METHOD_POST=> true, + Request::METHOD_PATCH => true ], 'roles' => [ Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE, Request::METHOD_HEAD => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE, Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE, - + Request::METHOD_PATCH => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE, ], ], 'address' => [ diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 82b2ee10c..296acde19 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -214,7 +214,7 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface private $scopes; /** - * @ORM\ManyToOne(targetEntity=Person::class) + * @ORM\ManyToOne(targetEntity=Person::class, inversedBy="accompanyingPeriodRequested") * @ORM\JoinColumn(nullable=true) */ private $requestorPerson; @@ -514,6 +514,44 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface ); } + /** + * Return an array with open participations sorted by household + * [ + * [ + * "household" => Household x, + * "members" => [ + * Participation y , Participation z, ... + * ] + * ], + * ] + * + */ + public function actualParticipationsByHousehold(): array + { + $participations = $this->getOPenParticipations()->toArray(); + + $households = []; + foreach ($participations as $p) { + $households[] = $p->getPerson()->getCurrentHousehold(); + } + $households = array_unique($households, SORT_REGULAR); + + $array = []; + foreach ($households as $household) { + $members = []; + foreach ($participations as $p) { + if ($household === $p->getPerson()->getCurrentHousehold()) { + $members[] = array_shift($participations); + } else { + $participations[] = array_shift($participations); + } + } + $array[] = [ 'household' => $household, 'members' => $members ]; + } + + return $array; + } + /** * Return true if the accompanying period contains a person. * diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriodParticipation.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriodParticipation.php index 70c5a0505..c873e527a 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriodParticipation.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriodParticipation.php @@ -47,86 +47,91 @@ class AccompanyingPeriodParticipation * @Groups({"read"}) */ private $id; - + /** * @ORM\ManyToOne(targetEntity=Person::class, inversedBy="accompanyingPeriodParticipations") * @ORM\JoinColumn(name="person_id", referencedColumnName="id", nullable=false) * @Groups({"read"}) */ private $person; - + /** * @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class, inversedBy="participations", cascade={"persist"}) * @ORM\JoinColumn(name="accompanyingperiod_id", referencedColumnName="id", nullable=false) */ private $accompanyingPeriod; - + /** * @ORM\Column(type="date", nullable=false) * @Groups({"read"}) */ private $startDate; - + /** * @ORM\Column(type="date", nullable=true) * @Groups({"read"}) */ private $endDate = null; - + public function __construct(AccompanyingPeriod $accompanyingPeriod, Person $person) { $this->startDate = new \DateTimeImmutable('now'); $this->accompanyingPeriod = $accompanyingPeriod; $this->person = $person; } - + public function getId(): ?int { return $this->id; } - + public function getPerson(): ?Person { return $this->person; } - + public function setPerson(?Person $person): self { $this->person = $person; - + return $this; } - + public function getAccompanyingPeriod(): ?AccompanyingPeriod { return $this->accompanyingPeriod; } - + public function setAccompanyingPeriod(?AccompanyingPeriod $accompanyingPeriod): self { $this->accompanyingPeriod = $accompanyingPeriod; - + return $this; } - + public function getStartDate(): ?\DateTimeInterface { return $this->startDate; } - + /* * public function setStartDate(\DateTimeInterface $startDate): self { $this->startDate = $startDate; return $this; } */ - + public function getEndDate(): ?\DateTimeInterface { return $this->endDate; } - + public function setEndDate(?\DateTimeInterface $endDate): self { $this->endDate = $endDate; - + return $this; } + + public function isOpen(): bool + { + return $this->endDate === null; + } } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php index edf94bd67..46ae02ce0 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php @@ -229,7 +229,7 @@ class Household )) ->andWhere($expr->orX( $expr->isNull('endDate'), - $expr->gte('endDate', $date) + $expr->gt('endDate', $date) )); return $criteria; @@ -306,7 +306,7 @@ class Household ) ->orWhere( $expr->andX( - $expr->lt('endDate', $date), + $expr->lte('endDate', $date), $expr->neq('endDate', null) ) ); diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index 69429219f..9aaad8c04 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -28,9 +28,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Country; use Chill\MainBundle\Entity\User; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\MaritalStatus; -use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\MainBundle\Entity\HasCenterInterface; use Chill\MainBundle\Entity\Address; @@ -329,6 +329,15 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI */ private $accompanyingPeriodParticipations; + /** + * The accompanying period requested by the Person + * + * @ORM\OneToMany(targetEntity=AccompanyingPeriod::class, + * mappedBy="requestorPerson") + * @var Collection|AccompanyingPeriod[] + */ + private Collection $accompanyingPeriodRequested; + /** * A remark over the person * @var string @@ -478,6 +487,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI $this->genderComment = new CommentEmbeddable(); $this->maritalStatusComment = new CommentEmbeddable(); $this->periodLocatedOn = new ArrayCollection(); + $this->accompanyingPeriodRequested = new ArrayCollection(); } /** @@ -605,6 +615,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI /** * Get AccompanyingPeriodParticipations Collection + * + * @return AccompanyingPeriodParticipation[]|Collection */ public function getAccompanyingPeriodParticipations(): Collection { @@ -614,6 +626,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI /** * Return a collection of participation, where the participation * is still opened, not a draft, and the period is still opened + * + * @return AccompanyingPeriodParticipation[]|Collection */ public function getOpenedParticipations(): Collection { @@ -1650,6 +1664,84 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI return $this; } + /** + * @return AccompanyingPeriod[]|Collection + */ + public function getAccompanyingPeriodRequested(): Collection + { + return $this->accompanyingPeriodRequested; + } + + /** + * Return a list of all accompanying period where the person is involved: + * + * * as requestor; + * * as participant, only for opened participation; + * + * @param bool $asParticipantOpen add participation which are still opened + * @param bool $asRequestor add accompanying period where the person is requestor + * @return AccompanyingPeriod[]|Collection + */ + public function getAccompanyingPeriodInvolved( + bool $asParticipantOpen = true, + bool $asRequestor = true + ): Collection + { + $result = new ArrayCollection(); + + if ($asParticipantOpen) { + foreach ($this->getOpenedParticipations() + ->map(fn (AccompanyingPeriodParticipation $app) => + $app->getAccompanyingPeriod()) + as $period + ) { + if (!$result->contains($period)) { + $result->add($period); + } + } + } + + if ($asRequestor) { + foreach ($this->accompanyingPeriodRequested as $period) { + if (!$result->contains($period)) { + $result->add($period); + } + } + } + + return $result; + } + + /** + * Handy method to get the AccompanyingPeriodParticipation + * matching a given AccompanyingPeriod. + * + * Used in template, to find the participation when iterating on a list + * of period. + * + * @param \Chill\PersonBundle\Entity\AccompanyingPeriod $period + * @return AccompanyingPeriodParticipation + */ + public function findParticipationForPeriod(AccompanyingPeriod $period): ?AccompanyingPeriodParticipation + { + $closeCandidates = []; + + foreach ($this->getAccompanyingPeriodParticipations() as $participation) { + if ($participation->getAccompanyingPeriod() === $period) { + if ($participation->isOpen()) { + return $participation; + } + $closeCandidates[] = $participation; + } + } + + if (0 < count($closeCandidates)) { + return $closeCandidates[0]; + } + + return null; + } + public function getCreatedBy(): ?User { return $this->createdBy; diff --git a/src/Bundle/ChillPersonBundle/EventListener/PersonEventListener.php b/src/Bundle/ChillPersonBundle/EventListener/PersonEventListener.php new file mode 100644 index 000000000..20855a166 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/EventListener/PersonEventListener.php @@ -0,0 +1,30 @@ +getFirstName()), MB_CASE_TITLE, 'UTF-8'); + $firstnameCaps = ucwords(strtolower($firstnameCaps), " \t\r\n\f\v'-"); + $person->setFirstName($firstnameCaps); + + $lastnameCaps = mb_strtoupper($person->getLastName(), 'UTF-8'); + $person->setLastName($lastnameCaps); + } + + public function prePersistAltName(PersonAltName $altname) + { + $altnameCaps = mb_strtoupper($altname->getLabel(), 'UTF-8'); + $altname->setLabel($altnameCaps); + } +} diff --git a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php index 36d088c25..8fa95fe4f 100644 --- a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php @@ -22,7 +22,9 @@ namespace Chill\PersonBundle\Form; use Chill\MainBundle\Form\Event\CustomizeFormEvent; +use Chill\MainBundle\Repository\CenterRepository; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Security\Authorization\PersonVoter; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -30,12 +32,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Chill\MainBundle\Form\Type\ChillDateType; -use Chill\MainBundle\Form\Type\CenterType; +use Chill\MainBundle\Form\Type\PickCenterType; use Chill\PersonBundle\Form\Type\GenderType; use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer; use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; use Chill\PersonBundle\Form\Type\PersonAltNameType; -use Chill\MainBundle\Form\Type\Export\PickCenterType; final class CreationPersonType extends AbstractType { @@ -43,11 +44,7 @@ final class CreationPersonType extends AbstractType // TODO: See if this is still valid and update accordingly. const NAME = 'chill_personbundle_person_creation'; - /** - * - * @var CenterTransformer - */ - private $centerTransformer; + private CenterRepository $centerRepository; /** * @@ -58,11 +55,11 @@ final class CreationPersonType extends AbstractType private EventDispatcherInterface $dispatcher; public function __construct( - CenterTransformer $centerTransformer, + CenterRepository $centerRepository, ConfigPersonAltNamesHelper $configPersonAltNamesHelper, EventDispatcherInterface $dispatcher ) { - $this->centerTransformer = $centerTransformer; + $this->centerTransformer = $centerRepository; $this->configPersonAltNamesHelper = $configPersonAltNamesHelper; $this->dispatcher = $dispatcher; } @@ -82,8 +79,9 @@ final class CreationPersonType extends AbstractType ->add('gender', GenderType::class, array( 'required' => true, 'placeholder' => null )) - ->add('center', CenterType::class, [ - 'required' => false + ->add('center', PickCenterType::class, [ + 'required' => false, + 'role' => PersonVoter::CREATE, ]) ; diff --git a/src/Bundle/ChillPersonBundle/Form/PersonType.php b/src/Bundle/ChillPersonBundle/Form/PersonType.php index 376310965..8bc982713 100644 --- a/src/Bundle/ChillPersonBundle/Form/PersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/PersonType.php @@ -129,8 +129,7 @@ class PersonType extends AbstractType $builder ->add('mobilenumber', TelType::class, array('required' => false)) ->add('acceptSMS', CheckboxType::class, array( - 'value' => false, - 'required' => true + 'required' => false )); } diff --git a/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php b/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php index c9eade17a..c6a76445d 100644 --- a/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php +++ b/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php @@ -50,6 +50,13 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface ]]) ->setExtras(['order' => 30]); + $menu->addChild($this->translator->trans('household.Relationship'), [ + 'route' => 'chill_person_household_relationship', + 'routeParameters' => [ + 'household_id' => $household->getId() + ]]) + ->setExtras(['order' => 40]); + } diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php new file mode 100644 index 000000000..38236df14 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php @@ -0,0 +1,103 @@ +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; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepositoryInterface.php new file mode 100644 index 000000000..56a927ae5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepositoryInterface.php @@ -0,0 +1,23 @@ +andWhere($qb->expr()->eq('s.birthdate', ':birthdate')) + $qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate')) ->setParameter('birthdate', $birthdate); } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/chillperson.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/chillperson.scss index 071d0f9a5..2e5821659 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/chillperson.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/chillperson.scss @@ -1,10 +1,13 @@ // Access to Bootstrap variables and mixins @import '~ChillMainAssets/module/bootstrap/shared'; +@import 'ChillMainAssets/chill/scss/chill_variables'; + // Complete/override Main generic assets @import './scss/mixins'; @import './scss/render_box.scss'; @import './scss/flex_table.scss'; +@import './scss/badge.scss'; // Specific templates styles @import './scss/accompanying_period_work.scss'; @@ -19,13 +22,13 @@ div.banner { div#header-person-name { - background: none repeat scroll 0 0 $chill-green-dark; + background: none repeat scroll 0 0 shade-color($chill-person-context, 20%); color: $white; padding-top: 1em; padding-bottom: 1em; } div#header-person-details { - background: none repeat scroll 0 0 $chill-green; + background: none repeat scroll 0 0 $chill-person-context; color: $white; padding-top: 1em; padding-bottom: 1em; @@ -92,7 +95,6 @@ div.person-view { * Header custom for Accompanying Course */ -$chill-accourse-context: #718596; div.banner { div#header-accompanying_course-name { @@ -104,9 +106,9 @@ div.banner { span { a { color: $white; - } - a:hover { - text-decoration: underline; + &:hover { + text-decoration: underline; + } } } } @@ -118,19 +120,11 @@ div.banner { } } -abbr.referrer { - font-size: 70%; - padding-right: 0.4em; - align-self: center; // in flex context -} - /* * HOUSEHOLD CONTEXT * Header custom for Household */ -$chill-household-context: #929d69; - div.banner { div#header-household-name { background: none repeat scroll 0 0 $chill-household-context; @@ -142,9 +136,9 @@ div.banner { span { a { color: $white; - } - a:hover { - text-decoration: underline; + &:hover { + text-decoration: underline; + } } } div.household-members { @@ -185,6 +179,7 @@ div.banner { } } +/// div.household-resume { display: flex; flex-direction: row; @@ -208,42 +203,42 @@ div.household-resume { } } -/* -* BADGES, MARKS, PINS -* for chill person theme -*/ - -// chill person badges -span.badge-person, -span.badge-thirdparty { - display: inline-block; - padding: 0 0.5em !important; - background-color: $white; - color: $dark; - border: 1px solid $chill-ll-gray; - border-bottom-width: 2px; - border-bottom-style: solid; - border-radius: 6px; - a { - text-decoration: none; +/// Horizontal list of persons (Accourse resume page) +div.accompanyingcourse-resume { + div.associated-persons { + font-size: 110%; + span.household { + display: inline-block; + border-radius: 8px; + border: 1px solid $white; + &:hover { + border: 1px solid $chill-beige; + i { + display: inline-block; + } + } + &.no-household:hover { + border: 1px solid $white; + } + i { + color: $chill-beige; + display: none; + } + padding: 0.3em; + margin-right: 2px; + } } } -span.badge-person { - border-bottom-color: $chill-green; -} -// todo: move in thirdparty -span.badge-thirdparty { - border-bottom-color: shade-color($chill-pink, 10%); + +/// +abbr.referrer { // still used ? + font-size: 70%; + padding-right: 0.4em; + align-self: center; // in flex context } -// household holder mark -span.fa-holder { - width: 1em; - margin: -10px 0.3em -8px 0; - i:last-child { - font-weight: 900; - color: white; - font-size: 70%; - font-family: "Open Sans Extrabold"; - } +.updatedBy { + margin-top: 0.3rem; + font-size: 0.9rem; + font-style: italic; } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/accompanying_period_work.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/accompanying_period_work.scss index f43e37f29..e6bdb174b 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/accompanying_period_work.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/accompanying_period_work.scss @@ -1,31 +1,6 @@ /// AccompanyingCourse Work list Page div.accompanying_course_work-list { - h2.title { - display: flex; - flex-direction: row; - width: 100%; - color: $dark; - height: 40px; - - span.title_label { - border-radius: 0.35rem 0 0 0.35rem; - background-color: $social-action-label-color; - color: $white; - font-size: 80%; - padding: 0.5em; - padding-right: 0; - } - span.title_action { - flex-grow: 1; - margin: 0 0 0 auto; - border-radius: 0 0.35rem 0.35rem 0; - background-color: $light; - padding: 0.2em 0.7em; - @include badge_social_action; - } - } - div.timeline { width: 100%; ul { @@ -125,7 +100,7 @@ div.accompanying_course_work-list { } } &.goal_title li::marker { - color: $sky-blue; + color: $social-issue-color; } &.result_list li::marker { color: $pink; @@ -133,11 +108,4 @@ div.accompanying_course_work-list { } } - .updatedBy { - margin-top: 0.3rem; - font-size: 0.9rem; - font-style: italic; - } - } - diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss new file mode 100644 index 000000000..7e9f13149 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/badge.scss @@ -0,0 +1,119 @@ +/* +* BADGES PERSON AND THIRDPARTY +*/ + +span.badge-person, +span.badge-thirdparty { + display: inline-block; + padding: 0 0.5em !important; + background-color: $white; + color: $dark; + border: 1px solid $chill-ll-gray; + border-bottom-width: 2px; + border-bottom-style: solid; + border-radius: 6px; + a { + text-decoration: none; + } +} +span.badge-person { + border-bottom-color: $chill-green; +} +span.badge-thirdparty { + border-bottom-color: shade-color($chill-pink, 10%); +} + +/* +* HOUSEHOLD HOLDER MARK +*/ + +span.fa-holder { + width: 1em; + margin: -10px 0.3em -8px 0; + i:last-child { + font-family: "Open Sans Extrabold"; + font-weight: 900; + font-size: 70%; + color: $white; + } +} + +/* +* BADGE_TITLE +* Display Title like a badge (with background-colored label) +*/ + +h2.badge-title { + display: flex; + flex-direction: row; + width: 100%; + color: $dark; + + a & { text-decoration: none; } // ?!? keep it ? + + span.title_label { + border-radius: 0.35rem 0 0 0.35rem; + color: $white; + font-size: 80%; + padding: 0.5em; + padding-right: 0; + h3 { + margin-bottom: 0.5rem; + } + } + span.title_action { + flex-grow: 1; + margin: 0 0 0 auto; + border-radius: 0 0.35rem 0.35rem 0; + background-color: $light; + padding: 0.2em 1em; + + ul.small_in_title { + margin-top: 0.5em; + font-size: 70%; + padding-left: 1rem; + } + } +} + +/// badge_title in AccompanyingCourse Work list Page +div.accompanying_course_work-list { + h2.badge-title { + span.title_label { + // Calculate same color then border:groove + background-color: shade-color($social-action-color, 34%); + } + span.title_action { + @include badge_title($social-action-color); + } + } +} + +/// badge_title in Activities on resume page +div.activity-list { + h2.badge-title { + span.title_label { + // Calculate same color then border:groove + background-color: shade-color($activity-color, 34%); + h3 { + color: $white; + } + } + span.title_action { + @include badge_title($activity-color); + } + span.title_label { + div.duration { + font-size: 70%; + font-weight: 500; + p { + margin-bottom: 0; + text-align: right; + abbr { + text-decoration: none; + } + } + } + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/mixins.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/mixins.scss index 4ff1d72bc..e995f97eb 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/mixins.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/mixins.scss @@ -1,44 +1,39 @@ -// Additionnal colors -$sky-blue: #4bafe8; - -// mixins variables -$social-issue-color: $sky-blue; -$social-action-color: $orange; - -// Calculate same color then border:groove -// origin: $orange, computed: #965028 -$social-action-label-color: shade-color($orange, 34%); - /// -/// Social Issue mixin -// define visual badge for all social issues +/// Chill badge mixin +// define chill visual badge /// -@mixin badge_social_issue { +@mixin chill_badge($color) { text-transform: capitalize !important; font-weight: 500 !important; - border-left: 20px groove $social-issue-color; + border-left: 20px groove $color; &:before { - content: '\f04b'; font-family: ForkAwesome; - color: $social-issue-color; + content: '\f04b'; + color: $color; + } +} + +/// +/// Social badge mixin +// define visual badge for social issues or social action +/// + +@mixin badge_social($color) { + @include chill_badge($color); + &:before { margin: 0 0.3em 0 -0.75em; } } /// -/// Social Action mixin -// define visual badge for all social actions +/// Generic mixin for titles like badge +// define visual badge used in title area /// -@mixin badge_social_action { - text-transform: capitalize !important; - font-weight: 500 !important; - border-left: 20px groove $social-action-color; +@mixin badge_title($color) { + @include chill_badge($color); &:before { - content: '\f04b'; - font-family: ForkAwesome; - color: $social-action-color; - margin: 0 0.3em 0 -0.75em; + margin: 0 0.3em 0 -1.05em; } } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/render_box.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/render_box.scss index 1aa1c214b..230640bbd 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/render_box.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/render_box.scss @@ -8,21 +8,28 @@ span.altname {} } - /// SOCIAL-ISSUE - &.entity-social-issue { - margin-right: 0.3em; - font-size: 120%; - span.badge { - @include badge_social_issue; - } - } - - /// SOCIAL-ACTION + /// SOCIAL-ISSUE AND SOCIAL-ACTION + &.entity-social-issue, &.entity-social-action { margin-right: 0.3em; font-size: 120%; span.badge { - @include badge_social_action; + // for too long badge in a narrow inside block + text-align: initial; + margin-bottom: 0.2em; + > span { + white-space: normal; + } + } + } + &.entity-social-issue { + span.badge { + @include badge_social($social-issue-color); + } + } + &.entity-social-action { + span.badge { + @include badge_social($social-action-color); } } } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/lib/household.js b/src/Bundle/ChillPersonBundle/Resources/public/lib/household.js new file mode 100644 index 000000000..52754fa97 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/lib/household.js @@ -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 +}; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/page/vis/index.js b/src/Bundle/ChillPersonBundle/Resources/public/page/vis/index.js new file mode 100644 index 000000000..f11e930c0 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/page/vis/index.js @@ -0,0 +1,32 @@ +import vis from 'vis-network/dist/vis-network.min'; + +require('./scss/vis.scss'); + +// create an array with nodes +let nodes = new vis.DataSet([ + { id: 1, label: "Node 1" }, + { id: 2, label: "Node 2" }, + { id: 3, label: "Node 3" }, + { id: 4, label: "Node 4" }, + { id: 5, label: "Node 5", cid: 1 }, +]); + +// create an array with edges +let edges = new vis.DataSet([ + { from: 1, to: 3 }, + { from: 1, to: 2 }, + { from: 2, to: 4 }, + { from: 2, to: 5 }, + { from: 3, to: 3 }, +]); + +// create a network +let container = document.getElementById("graph-relationship"); +let data = { + nodes: nodes, + edges: edges, +}; +let options = {}; + +// +let network = new vis.Network(container, data, options); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/page/vis/scss/vis.scss b/src/Bundle/ChillPersonBundle/Resources/public/page/vis/scss/vis.scss new file mode 100644 index 000000000..3d29c47ce --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/page/vis/scss/vis.scss @@ -0,0 +1,5 @@ +div#graph-relationship { + margin: 2em auto; + height: 500px; + border: 1px solid lightgray; +} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/App.vue index e4baa082b..bd7705ca4 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/App.vue @@ -87,8 +87,8 @@ export default { } padding: 0em 0em; margin: 1em 0; - border: 1px dotted tint-color($chill-accourse-context, 10%); border-radius: 5px; + border: 1px dotted tint-color($chill-accourse-context, 10%); border-left: 1px dotted tint-color($chill-accourse-context, 10%); border-right: 1px dotted tint-color($chill-accourse-context, 10%); dd { @@ -96,10 +96,15 @@ export default { } & > div { margin: 1em 3em 0; + &.flex-table, &.flex-bloc { margin: 1em 0 0; } + &.alert.to-confirm { + margin: 1em 0 0; + padding: 1em 3em; + } } div.flex-table { diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue index 9fe6fe87c..84e7fb2c6 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue @@ -22,7 +22,7 @@ {{ $t('course.open_at') }}{{ $d(accompanyingCourse.openingDate.datetime, 'text') }} - {{ $t('course.by') }}{{ accompanyingCourse.user.username }} + {{ $t('course.referrer') }}: {{ accompanyingCourse.user.username }} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner/SocialIssue.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner/SocialIssue.vue index d2cfe5da0..57d185820 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner/SocialIssue.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner/SocialIssue.vue @@ -12,9 +12,9 @@ export default { diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue index 07761b31b..52b538284 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated.vue @@ -26,7 +26,7 @@ :id="p.person.id" :value="p.person.id" /> -
@@ -58,11 +58,14 @@
+
+ {{ $t('persons_associated.participation_not_valid') }} +
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Scopes.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Scopes.vue index 903f85a86..3217afd25 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Scopes.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Scopes.vue @@ -1,18 +1,20 @@ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/SocialIssue.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/SocialIssue.vue index 6faaa5083..41fd85f0e 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/SocialIssue.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/SocialIssue.vue @@ -21,13 +21,17 @@ +
+ {{ $t('social_issue.not_valid') }} +
+ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/CurrentHousehold.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/CurrentHousehold.vue new file mode 100644 index 000000000..1f76c8a5b --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/CurrentHousehold.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue index be332523d..c40cc9cbc 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue @@ -1,5 +1,8 @@ @@ -141,9 +146,6 @@ export default { }, props: ['person', 'options', 'render', 'returnPath'], computed: { - getGenderTranslation: function() { - return this.person.gender == 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man'; - }, isMultiline: function() { if(this.options.isMultiline){ return this.options.isMultiline @@ -152,7 +154,13 @@ export default { } }, getGenderIcon: function() { - return this.person.gender == 'woman' ? 'fa-venus' : this.person.gender == 'man' ? 'fa-mars' : 'fa-neuter'; + return this.person.gender === 'woman' ? 'fa-venus' : this.person.gender === 'man' ? 'fa-mars' : 'fa-neuter'; + }, + getGenderTranslation: function() { + return this.person.gender === 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man'; + }, + getGender() { + return this.person.gender === 'woman' ? 'person.gender.woman' : this.person.gender === 'man' ? 'person.gender.man' : 'person.gender.neuter'; }, birthdate: function(){ if(this.person.birthdate !== null){ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue index 5261b2c81..d0627753a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue @@ -40,7 +40,7 @@ - +
@@ -75,7 +75,7 @@ @@ -102,6 +112,10 @@ export default { &:before{ content: " " } + &.tparty-parent { + font-weight: bold; + font-variant: all-small-caps; + } } diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/OnTheFly/ThirdParty.vue b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/OnTheFly/ThirdParty.vue index 1b1207131..5b605c748 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/OnTheFly/ThirdParty.vue +++ b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/OnTheFly/ThirdParty.vue @@ -1,5 +1,5 @@