mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'master' into 38-gestion-doublon
This commit is contained in:
commit
92b1b17679
39
.changes/v2.5.0.md
Normal file
39
.changes/v2.5.0.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
## v2.5.0 - 2023-07-14
|
||||||
|
### Feature
|
||||||
|
* Allow filtering on the basis of a user within general tasks lists
|
||||||
|
* ([#120](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/120)) Adding OrderFilter to the list of social actions.
|
||||||
|
* ([#125](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/125)) [export] Add a list for people with their associated course
|
||||||
|
* [export] Add ordering by person's lastname or course opening date in list which concerns accompanying course or peoples
|
||||||
|
* ([#128](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/128)) [Export] allow to group activities by localisation
|
||||||
|
* ([#129](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/129)) [export] Add a filter "filter course having an activity between two dates"
|
||||||
|
* ([#112](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/112)) [addresses] Add a cronjob to re-associate addresses with addresses reference every 6 hours
|
||||||
|
* Improve filtering layout
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* reimplement the visualization of all calculator results
|
||||||
|
* ([#117](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/117)) Repair my unread notification list with actions and evaluations documents
|
||||||
|
* ([#126](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/126)) Correct bug in thirdparty API search query: simplify address joins clause for child and parent kind
|
||||||
|
|
||||||
|
### DX
|
||||||
|
* Documentation for database principles
|
||||||
|
* [cronjob] when a cronjob is executed, it may return an array of data that will be passed as argument on the next execution
|
||||||
|
|
||||||
|
### UX
|
||||||
|
* ([#93](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/93)) Better integration of address details button: look, position, title tag
|
||||||
|
* ([#93](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/93)) Show address detail button on person and household banners
|
||||||
|
* Improve residential address position on show onthefly modale
|
||||||
|
|
||||||
|
### Traduction francophone des principaux changements
|
||||||
|
|
||||||
|
* Ajout d'un filtre "par utilisateur" aux pages de tâche
|
||||||
|
* Filtre des actions d'accompagnement par date, type, intervenant
|
||||||
|
* export: liste des usagers concernés avec détail de leurs parcours
|
||||||
|
* export: ajout d'un regroupement des échanges par localisation
|
||||||
|
* export: ajout d'un filtre "parcours ayant reçu un échange entre deux dates"
|
||||||
|
* ajout d'une tâche cron pour associer les adresses à une adresse de référence
|
||||||
|
* correction: réparation de la liste des notifications sur la page d'accueil, dans le cas où une notification concerne une action ou un document dans une évaluation
|
||||||
|
* correction: réparation de la recherche des tiers ayant des codes postaux similaires entre les parents et enfants
|
||||||
|
* meilleure intégration du bouton "détail d'une adresse": améliration de la taille et de la position
|
||||||
|
* bouton permettant de visualiser les détails d'une adresse (modale avec carte) dans la bannière "Usager" et "Ménage"
|
||||||
|
* amélioration de la modale permettant de voir les détails d'un usager: les adresses de résidence sont dans la continuité des autres adresses, et non plus dans une colonne séparée
|
||||||
|
* améliore le design et l'expérience utilisateur des filtres
|
3
.changes/v2.5.1.md
Normal file
3
.changes/v2.5.1.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## v2.5.1 - 2023-07-14
|
||||||
|
### Fixed
|
||||||
|
* [collate addresses] block collating addresses to another address reference where the address reference is already the best match
|
3
.changes/v2.5.2.md
Normal file
3
.changes/v2.5.2.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## v2.5.2 - 2023-07-15
|
||||||
|
### Fixed
|
||||||
|
* [Collate Address] when updating address point, do not use the point's address reference if the similarity is below the requirement for associating the address reference and the address (it uses the postcode's center instead)
|
3
.changes/v2.5.3.md
Normal file
3
.changes/v2.5.3.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## v2.5.3 - 2023-07-20
|
||||||
|
### Fixed
|
||||||
|
* ([#132](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/132)) Rendez-vous documents created would appear in all documents lists of all persons with an accompanying period. Or statements are now added to the where clause to filter out documents that come from unrelated accompanying period/ or person rendez-vous.
|
@ -7,7 +7,7 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
|
|||||||
kindFormat: '### {{.Kind}}'
|
kindFormat: '### {{.Kind}}'
|
||||||
# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description.
|
# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description.
|
||||||
changeFormat: >-
|
changeFormat: >-
|
||||||
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}} {{ if not (eq .Custom.Long "") }}
|
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}} {{ if and (.Custom.Long) (not (eq .Custom.Long "")) }}
|
||||||
|
|
||||||
{{ .Custom.Long }}{{ end }}
|
{{ .Custom.Long }}{{ end }}
|
||||||
custom:
|
custom:
|
||||||
@ -30,6 +30,8 @@ kinds:
|
|||||||
auto: patch
|
auto: patch
|
||||||
- label: DX
|
- label: DX
|
||||||
auto: patch
|
auto: patch
|
||||||
|
- label: UX
|
||||||
|
auto: patch
|
||||||
newlines:
|
newlines:
|
||||||
afterChangelogHeader: 1
|
afterChangelogHeader: 1
|
||||||
beforeChangelogVersion: 1
|
beforeChangelogVersion: 1
|
||||||
|
52
CHANGELOG.md
52
CHANGELOG.md
@ -6,6 +6,58 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||||
|
|
||||||
|
|
||||||
|
## v2.5.3 - 2023-07-20
|
||||||
|
### Fixed
|
||||||
|
* ([#132](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/132)) Rendez-vous documents created would appear in all documents lists of all persons with an accompanying period. Or statements are now added to the where clause to filter out documents that come from unrelated accompanying period/ or person rendez-vous.
|
||||||
|
|
||||||
|
## v2.5.2 - 2023-07-15
|
||||||
|
### Fixed
|
||||||
|
* [Collate Address] when updating address point, do not use the point's address reference if the similarity is below the requirement for associating the address reference and the address (it uses the postcode's center instead)
|
||||||
|
|
||||||
|
## v2.5.1 - 2023-07-14
|
||||||
|
### Fixed
|
||||||
|
* [collate addresses] block collating addresses to another address reference where the address reference is already the best match
|
||||||
|
|
||||||
|
## v2.5.0 - 2023-07-14
|
||||||
|
### Feature
|
||||||
|
* Allow filtering on the basis of a user within general tasks lists
|
||||||
|
* ([#120](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/120)) Adding OrderFilter to the list of social actions.
|
||||||
|
* ([#125](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/125)) [export] Add a list for people with their associated course
|
||||||
|
* [export] Add ordering by person's lastname or course opening date in list which concerns accompanying course or peoples
|
||||||
|
* ([#128](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/128)) [Export] allow to group activities by localisation
|
||||||
|
* ([#129](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/129)) [export] Add a filter "filter course having an activity between two dates"
|
||||||
|
* ([#112](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/112)) [addresses] Add a cronjob to re-associate addresses with addresses reference every 6 hours
|
||||||
|
* Improve filtering layout
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* reimplement the visualization of all calculator results
|
||||||
|
* ([#117](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/117)) Repair my unread notification list with actions and evaluations documents
|
||||||
|
* ([#126](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/126)) Correct bug in thirdparty API search query: simplify address joins clause for child and parent kind
|
||||||
|
|
||||||
|
### DX
|
||||||
|
* Documentation for database principles
|
||||||
|
* [cronjob] when a cronjob is executed, it may return an array of data that will be passed as argument on the next execution
|
||||||
|
|
||||||
|
### UX
|
||||||
|
* ([#93](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/93)) Better integration of address details button: look, position, title tag
|
||||||
|
* ([#93](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/93)) Show address detail button on person and household banners
|
||||||
|
* Improve residential address position on show onthefly modale
|
||||||
|
|
||||||
|
### Traduction francophone des principaux changements
|
||||||
|
|
||||||
|
* Ajout d'un filtre "par utilisateur" aux pages de tâche
|
||||||
|
* Filtre des actions d'accompagnement par date, type, intervenant
|
||||||
|
* export: liste des usagers concernés avec détail de leurs parcours
|
||||||
|
* export: ajout d'un regroupement des échanges par localisation
|
||||||
|
* export: ajout d'un filtre "parcours ayant reçu un échange entre deux dates"
|
||||||
|
* ajout d'une tâche cron pour associer les adresses à une adresse de référence
|
||||||
|
* correction: réparation de la liste des notifications sur la page d'accueil, dans le cas où une notification concerne une action ou un document dans une évaluation
|
||||||
|
* correction: réparation de la recherche des tiers ayant des codes postaux similaires entre les parents et enfants
|
||||||
|
* meilleure intégration du bouton "détail d'une adresse": améliration de la taille et de la position
|
||||||
|
* bouton permettant de visualiser les détails d'une adresse (modale avec carte) dans la bannière "Usager" et "Ménage"
|
||||||
|
* amélioration de la modale permettant de voir les détails d'un usager: les adresses de résidence sont dans la continuité des autres adresses, et non plus dans une colonne séparée
|
||||||
|
* améliore le design et l'expérience utilisateur des filtres
|
||||||
|
|
||||||
## v2.4.0 - 2023-07-07
|
## v2.4.0 - 2023-07-07
|
||||||
|
|
||||||
### Feature
|
### Feature
|
||||||
|
80
CONTRIBUTING.md
Normal file
80
CONTRIBUTING.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Chill is an open source, community-driven project.
|
||||||
|
|
||||||
|
If you'd like to contribute, please read the following.
|
||||||
|
|
||||||
|
## What can you do ?
|
||||||
|
|
||||||
|
Chill is an open-source project driven by a community of developers, users and social workers. If you don't feel ready to contribute code or patches, reviewing issues and pull requests (PRs) can be a great start to get involved and give back.
|
||||||
|
|
||||||
|
If you don't have your own instance or don't want to use it, you can try to reproduce bugs using the instance https://demo.chill.social
|
||||||
|
|
||||||
|
## Core team
|
||||||
|
|
||||||
|
The core team is the group of developers that determine the direction and evolution of the Chill project. Their votes rule if the features and patches proposed by the community are approved or rejected.
|
||||||
|
|
||||||
|
All the Chill Core members are long-time contributors with solid technical expertise and they have demonstrated a strong commitment to drive the project forward.
|
||||||
|
|
||||||
|
The core team:
|
||||||
|
|
||||||
|
- elects his own members;
|
||||||
|
- merge pull requests;
|
||||||
|
|
||||||
|
### members
|
||||||
|
|
||||||
|
Project leader: [julienfastre](https://gitlab.com/julienfastre)
|
||||||
|
|
||||||
|
Core members:
|
||||||
|
|
||||||
|
- [tchama](https://gitlab.com/tchama)
|
||||||
|
- [LenaertsJ](https://gitlab.com/LenaertsJ)
|
||||||
|
- [nobohan](https://gitlab.com/nobohan)
|
||||||
|
|
||||||
|
### Becoming a project member
|
||||||
|
|
||||||
|
About once a year, the core team discusses the opportunity to invite new members. To become a core team member, you must:
|
||||||
|
|
||||||
|
- take part on the development for at least 6 month: propose multiple merge requests and participate to the peer review process;
|
||||||
|
- through this participation, demonstrate your technical skills and your knowledge of the software and any of their dependencies;
|
||||||
|
|
||||||
|
### Core Membership Revocation
|
||||||
|
|
||||||
|
A Symfony Core membership can be revoked for any of the following reasons:
|
||||||
|
|
||||||
|
- Refusal to follow the rules and policies stated in this document;
|
||||||
|
- Lack of activity for the past six months;
|
||||||
|
- Willful negligence or intent to harm the Chill project;
|
||||||
|
|
||||||
|
The decision is taken by the majority of project members.
|
||||||
|
|
||||||
|
## Code development rules
|
||||||
|
|
||||||
|
### Merge requests
|
||||||
|
|
||||||
|
Every merge request must contains:
|
||||||
|
|
||||||
|
- one more entries suitable for generating a changelog. This is done using the [changie utility](https://changie.dev);
|
||||||
|
- a comprehensible description of the changes;
|
||||||
|
- if applicable, automated tests should be adapted or created;
|
||||||
|
- the code style must pass the project's rules, and non phpstan errors must be raised nor rector refactoring suggestion.
|
||||||
|
|
||||||
|
The pipelines must pass.
|
||||||
|
|
||||||
|
In case of emergency, some rules may be temporarily ignored.
|
||||||
|
|
||||||
|
### Merge Request Voting Policy
|
||||||
|
|
||||||
|
- -1 votes must always be justified by technical and objective reasons;
|
||||||
|
- +1 (technically: approbation on the merge request) votes do not require justification, unless there is at least one -1 vote;
|
||||||
|
- Core members can change their votes as many times as they desire during the course of a merge request discussion;
|
||||||
|
- Core members are not allowed to vote on their own merge requests.
|
||||||
|
|
||||||
|
### Merge Request Merging Process
|
||||||
|
|
||||||
|
All code must be committed to the repository through merge requests, except for minor changes which can be committed directly to the repository.
|
||||||
|
|
||||||
|
### Release Policy
|
||||||
|
|
||||||
|
The Core members are also the release manager for every Chill version.
|
||||||
|
|
84
docs/source/development/database-principles.rst
Normal file
84
docs/source/development/database-principles.rst
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
.. database-principles:
|
||||||
|
|
||||||
|
Principes de la base de données
|
||||||
|
###############################
|
||||||
|
|
||||||
|
Cette page donne une compréhension globale de la base de donnée de Chill, et explique quelques détails d'implémentations qui permettent d'accélérer les traitements à partir de la base de donnée, ou de l'exploiter plus aisément.
|
||||||
|
|
||||||
|
Cette page est rédigée en français.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
La stabilité du schéma de la base de donnée n'est pas garantie.
|
||||||
|
|
||||||
|
Toutefois, ce dernier évolue relativement peu. Il est rare que des tables ou des colonnes soient supprimées ou renommées. Mais il n'est pas garanti que cela puisse arriver.
|
||||||
|
|
||||||
|
Généralités
|
||||||
|
===========
|
||||||
|
|
||||||
|
Une liste commentée de toutes les tables :download:`est disponible au format CSV <./database/table_list.csv`.
|
||||||
|
|
||||||
|
Schéma et conventions de nommage
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
Au début de l'histoire de Chill, les schémas postgresql n'étaient pas exploités. Les données étaient stockées dans le schéma :code:`public`.
|
||||||
|
|
||||||
|
Par la suite, des nouveaux bundles sont apparus, et les tables ont été classées dans des schémas dédiés.
|
||||||
|
|
||||||
|
A l'heure actuelle:
|
||||||
|
|
||||||
|
- pour les anciens bundle, ceux qui ont déjà des tables dans le schéma public, les nouvelles tables sont ajoutées à ce schéma. Elles sont préfixées par :code:`chill_<nom du bundle>_`;
|
||||||
|
- pour les bundles plus récents, les tables sont créées dans le schéma dédié
|
||||||
|
|
||||||
|
Données avec de l'historicité
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Certaines données sont historisées:
|
||||||
|
|
||||||
|
- les référents d'un parcours;
|
||||||
|
- les statuts d'un parcours;
|
||||||
|
- la liaison entre les centres et les usagers;
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
Dans ces cas-là, Chill crée généralement deux colonnes, qui sont habituellement nommées :code:`startDate` et :code:`endDate`. Lorsque la colonne :code:`endDate` est à :code:`NULL`, cela signifie que la période n'est pas "fermée". La colonne :code:`startDate` n'est pas nullable.
|
||||||
|
|
||||||
|
Dans certains cas, la donnée actuelle (référent d'un parcours, par exemple) est également répétée au niveau de la table en elle-même. Par exemple, la table des parcours :code:`chill_person_accompanying_period` comporte une colonne :code:`step` (le statut du parcours) et :code:`user_id` (id du référent) en plus de l'historique. Bien que redondant, cela simplifie les traitements.
|
||||||
|
|
||||||
|
Relations particulières
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Usagers, ménages, adresses
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Les usagers ont une adresse au travers des ménages: dans l'interface, l'adresse est inscrite dans le dossier du ménage, et elle est "donnée" aux usagers membres du ménage, **et** qui partagent l'adresse de ce ménage. En effet, il est possible que des usagers "appartiennent" à un ménage sans y être domicilié: c'est le cas, par exemple, des enfants en garde alternée.
|
||||||
|
|
||||||
|
L'historique de l'appartenance des usagers au ménage est conservée, de même que l'historique des adresses pour un même ménage.
|
||||||
|
|
||||||
|
Les tables en jeu sont les suivantes:
|
||||||
|
|
||||||
|
- la table :code:`chill_person_person` liste les usagers;
|
||||||
|
- la table :code:`chill_person_household_members` liste les appartenances au ménage: il s'agit de la jointure entre les usagers et les ménages:
|
||||||
|
- les colonnes :code:`startDate` et :code:`endDate` indiquent la date de début et la date de fin de l'appartenance;
|
||||||
|
- la colonne :code:`shareHousehold` indique si l'utilisateur partage l'adresse du ménage (si oui, sa valeur est :code:`TRUE`)
|
||||||
|
- la table :code:`chill_person_household` liste les ménages
|
||||||
|
- la table :code:`chill_person_household_to_addresses` associe les ménages aux adresses;
|
||||||
|
- la table :code:`chill_main_address` contient les adresses, en indiquant la date de début de validité (:code:`validFrom`) et la fin de validité (:code:`validTo`).
|
||||||
|
|
||||||
|
Pour simplifier la résolution des adresses et des usagers, deux vues ont été mises en œuvre:
|
||||||
|
|
||||||
|
- la vue :code:`view_chill_person_household_address` reprend, pour chaque usager, l'historique des appartenances au ménage découpée par l'historique des adresses d'un ménage.
|
||||||
|
Autrement dit, une ligne est créée à chaque fois qu'un usager change de ménage, ou qu'un ménage change d'adresse. Il est donc possible de retrouver l'historique complet des adresses pour un usager donné via cette table.
|
||||||
|
- la vue :code:`view_chill_person_current_address` reprend l'adresse actuelle des usagers.
|
||||||
|
|
||||||
|
Adresses et unités géographiques
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
Chill propose des statistiques sur la localisation des adresses par rapport à des zones géographiques (:code:`chill_main_geographical_unit`).
|
||||||
|
|
||||||
|
Comme la résolution géographique des adresses est coûteuse en CPU et en temps de traitement, une vue matérialisée a été créée: :code:`view_chill_main_address_geographical_unit`. Elle est rafraichie quotidiennement dans la base de donnée de production.
|
||||||
|
|
||||||
|
Liste des tables et commentaires
|
||||||
|
================================
|
||||||
|
|
||||||
|
Une liste commentée de toutes les tables :download:`est disponible au format CSV <./database/table_list.csv`.
|
155
docs/source/development/database/table_list.csv
Normal file
155
docs/source/development/database/table_list.csv
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
order,table_schema,table_name,commentaire
|
||||||
|
1,chill_3party,party_category,Catégorie de tiers
|
||||||
|
2,chill_3party,party_center,Association entre les tiers et les centres (déprécié)
|
||||||
|
3,chill_3party,party_profession,Profession du tiers (déprécié)
|
||||||
|
4,chill_3party,third_party,Tiers
|
||||||
|
5,chill_3party,thirdparty_category,association tiers - catégories
|
||||||
|
6,chill_asideactivity,asideactivity,Activités annexes
|
||||||
|
7,chill_asideactivity,asideactivitycategory,Catégories d'activités annexes
|
||||||
|
8,chill_budget,charge,Charges du budget
|
||||||
|
9,chill_budget,charge_type,Types de charges
|
||||||
|
10,chill_budget,resource,Ressources du budget
|
||||||
|
11,chill_budget,resource_type,Types de ressources
|
||||||
|
12,chill_calendar,calendar,Rendez-vous
|
||||||
|
13,chill_calendar,calendar_doc,Document du rendez-vous
|
||||||
|
14,chill_calendar,calendar_range,Plage de disponibilité
|
||||||
|
15,chill_calendar,calendar_to_persons,association rendez-vous - usagers
|
||||||
|
16,chill_calendar,calendar_to_thirdparties,association rendez-vous - tiers
|
||||||
|
17,chill_calendar,cancel_reason,Motifs d'annulations
|
||||||
|
18,chill_calendar,invite,Invitation aux rendez-vous
|
||||||
|
19,chill_doc,accompanyingcourse_document,Documents associés aux parcours
|
||||||
|
20,chill_doc,document_category,Catégories de documents
|
||||||
|
21,chill_doc,person_document,Documents associés à l'usagers
|
||||||
|
22,chill_doc,stored_object,Documents
|
||||||
|
23,chill_task,recurring_task,Tâches récurrentes (non utilisé)
|
||||||
|
24,chill_task,single_task,Tâches
|
||||||
|
25,chill_task,single_task_place_event,Historique des transitions des tâches
|
||||||
|
26,chill_vendee,adressederelais,
|
||||||
|
27,chill_vendee,center_polygon
|
||||||
|
28,chill_vendee,entourage,
|
||||||
|
29,chill_vendee,geographical_unit
|
||||||
|
30,chill_vendee,geographical_unit_association
|
||||||
|
31,chill_vendee,mobilite
|
||||||
|
32,chill_vendee,niveauetude
|
||||||
|
33,chill_vendee,security_profile
|
||||||
|
34,chill_vendee,security_profile_action
|
||||||
|
35,chill_vendee,security_profile_jobs
|
||||||
|
36,chill_vendee,situationprofessionelle
|
||||||
|
37,chill_vendee,statutlogement
|
||||||
|
38,chill_vendee,tempsdetravail
|
||||||
|
39,chill_vendee,titredesejour
|
||||||
|
40,chill_vendee,vendee_person
|
||||||
|
41,chill_vendee,vendee_person_mineur
|
||||||
|
42,chill_vendee,vendeeperson_entourage
|
||||||
|
43,chill_vendee,vendeepersonmineur_adressederelais
|
||||||
|
44,public,accompanying_periods_scopes,Services associés aux parcours
|
||||||
|
45,public,activity,Échanges
|
||||||
|
46,public,activity_activityreason,s
|
||||||
|
47,public,activity_person,
|
||||||
|
48,public,activity_storedobject,
|
||||||
|
49,public,activity_thirdparty,
|
||||||
|
50,public,activity_user,
|
||||||
|
51,public,activityreason,Sujets d'échange
|
||||||
|
52,public,activityreasoncategory,Catégories de sujets
|
||||||
|
53,public,activitytpresence,Présence aux échanges
|
||||||
|
54,public,activitytype,Types d'échanges
|
||||||
|
55,public,activitytypecategory,Catégories de types d'échanges
|
||||||
|
56,public,centers,"Centres (territoires, agences, etc.)"
|
||||||
|
57,public,chill_activity_activity_chill_person_socialaction,
|
||||||
|
58,public,chill_activity_activity_chill_person_socialissue
|
||||||
|
59,public,chill_docgen_template,Gabarits de documents
|
||||||
|
60,public,chill_main_address,Adresses
|
||||||
|
61,public,chill_main_address_legacy,Anciennes adresses (dépréciés)
|
||||||
|
62,public,chill_main_address_reference,Adresses de référence
|
||||||
|
63,public,chill_main_civility,Civilités
|
||||||
|
64,public,chill_main_cronjob_execution,Dernière exécution des tâche cron
|
||||||
|
65,public,chill_main_geographical_unit,Unités géographiques
|
||||||
|
66,public,chill_main_geographical_unit_layer,Couches d'unités géographiques
|
||||||
|
67,public,chill_main_location,Localisations
|
||||||
|
68,public,chill_main_location_type,Types de localisations
|
||||||
|
69,public,chill_main_notification,Notifications
|
||||||
|
70,public,chill_main_notification_addresses_unread
|
||||||
|
71,public,chill_main_notification_addresses_user
|
||||||
|
72,public,chill_main_notification_comment,
|
||||||
|
73,public,chill_main_postal_code,Code postaux
|
||||||
|
74,public,chill_main_saved_export,Exports enregistrés
|
||||||
|
75,public,chill_main_user_job,Métiers
|
||||||
|
76,public,chill_main_workflow_entity,Workflows
|
||||||
|
77,public,chill_main_workflow_entity_comment
|
||||||
|
78,public,chill_main_workflow_entity_step,Etapes du workflow
|
||||||
|
79,public,chill_main_workflow_entity_step_cc_user,
|
||||||
|
80,public,chill_main_workflow_entity_step_user
|
||||||
|
81,public,chill_main_workflow_entity_step_user_by_accesskey,
|
||||||
|
82,public,chill_main_workflow_entity_subscriber_to_final,
|
||||||
|
83,public,chill_main_workflow_entity_subscriber_to_step
|
||||||
|
84,public,chill_person_accompanying_period,Parcours d'accompagnement
|
||||||
|
85,public,chill_person_accompanying_period_closingmotive,Motifs de cloture des parcours
|
||||||
|
86,public,chill_person_accompanying_period_comment,Commentaires des parcours
|
||||||
|
87,public,chill_person_accompanying_period_location_history,Historique de la localisatio ndes parcours
|
||||||
|
88,public,chill_person_accompanying_period_origin,Origine des parcours
|
||||||
|
89,public,chill_person_accompanying_period_participation,Appartenance des usagers au parcours
|
||||||
|
90,public,chill_person_accompanying_period_resource,Personnes ressources d'un parcours
|
||||||
|
91,public,chill_person_accompanying_period_social_issues,
|
||||||
|
92,public,chill_person_accompanying_period_step_history
|
||||||
|
93,public,chill_person_accompanying_period_user_history
|
||||||
|
94,public,chill_person_accompanying_period_work,Actions d'accompagnements
|
||||||
|
95,public,chill_person_accompanying_period_work_evaluation,Évaluations (dans les actions d'accompagnements)
|
||||||
|
96,public,chill_person_accompanying_period_work_evaluation_document,Documents des évaluations
|
||||||
|
97,public,chill_person_accompanying_period_work_goal,Objectifs d'une actions
|
||||||
|
98,public,chill_person_accompanying_period_work_goal_result,Objectifs et résultats d'une action
|
||||||
|
99,public,chill_person_accompanying_period_work_person,Usagers associés à une actions
|
||||||
|
100,public,chill_person_accompanying_period_work_referrer,Référents d'une actions
|
||||||
|
101,public,chill_person_accompanying_period_work_result,Résultats d'une action
|
||||||
|
102,public,chill_person_accompanying_period_work_third_party,Tiers traitants d'une action
|
||||||
|
103,public,chill_person_alt_name,"Noms supplémentaires d'un usager (nom marital, etc.)"
|
||||||
|
104,public,chill_person_household,Ménages
|
||||||
|
105,public,chill_person_household_composition,
|
||||||
|
106,public,chill_person_household_composition_type,Types de composition de ménage
|
||||||
|
107,public,chill_person_household_members,Membres du ménages
|
||||||
|
108,public,chill_person_household_position,Positions dans le ménage
|
||||||
|
109,public,chill_person_household_to_addresses,Association adresses - ménages
|
||||||
|
110,public,chill_person_marital_status,Etats civils
|
||||||
|
111,public,chill_person_not_duplicate,
|
||||||
|
112,public,chill_person_person,Usagers
|
||||||
|
113,public,chill_person_person_center_history,Historique des centres d'un usagers
|
||||||
|
114,public,chill_person_persons_to_addresses,Déprécié
|
||||||
|
115,public,chill_person_phone,Numéros d etéléphone supplémentaires d'un usager
|
||||||
|
116,public,chill_person_relations,Types de relations de filiation
|
||||||
|
117,public,chill_person_relationships,Relations de filiations
|
||||||
|
118,public,chill_person_residential_address,Adresses de résidences
|
||||||
|
119,public,chill_person_resource,Personnes ressources (pour les personnes)
|
||||||
|
120,public,chill_person_resource_kind,Type de personnes ressources
|
||||||
|
121,public,chill_person_social_action,Liste des actions d'accompagnement
|
||||||
|
122,public,chill_person_social_action_goal,Objectifs associés à une action
|
||||||
|
123,public,chill_person_social_action_result,Résultats associés à une action
|
||||||
|
124,public,chill_person_social_issue,Problématiques sociales
|
||||||
|
125,public,chill_person_social_work_evaluation,Evaluations disponibles
|
||||||
|
126,public,chill_person_social_work_evaluation_action,Associations entre les évaluations et les actions
|
||||||
|
127,public,chill_person_social_work_goal,Objectifs disponibles pour une actions
|
||||||
|
128,public,chill_person_social_work_goal_result,Objectifs et résultats disponible pour une action
|
||||||
|
129,public,chill_person_social_work_result,Résultats disponibles pour une action
|
||||||
|
130,public,country,Pays
|
||||||
|
131,public,custom_field_long_choice_options,
|
||||||
|
132,public,customfield
|
||||||
|
133,public,customfieldsdefaultgroup
|
||||||
|
134,public,customfieldsgroup
|
||||||
|
135,public,geography_columns,Table liée à postgis
|
||||||
|
136,public,geometry_columns,Table liée à postgis
|
||||||
|
137,public,group_centers,
|
||||||
|
138,public,language,Langues
|
||||||
|
139,public,messenger_messages,Table système
|
||||||
|
140,public,migration_versions,Table système
|
||||||
|
141,public,permission_groups
|
||||||
|
142,public,permissionsgroup_rolescope
|
||||||
|
143,public,persons_spoken_languages
|
||||||
|
144,public,regroupment,Regroupement de centres
|
||||||
|
145,public,regroupment_center,
|
||||||
|
146,public,role_scopes,
|
||||||
|
147,public,scopes,Services
|
||||||
|
148,public,spatial_ref_sys,Table système (postgis)
|
||||||
|
149,public,user_groupcenter,
|
||||||
|
150,public,users,Utilisateurs
|
||||||
|
151,public,view_chill_person_accompanying_period_info,
|
||||||
|
152,public,view_chill_person_current_address
|
||||||
|
153,public,view_chill_person_household_address
|
||||||
|
154,public,view_chill_person_person_center_history_current
|
Can't render this file because it has a wrong number of fields in line 28.
|
@ -36,6 +36,7 @@ As Chill rely on the `symfony <http://symfony.com>`_ framework, reading the fram
|
|||||||
Assets <assets.rst>
|
Assets <assets.rst>
|
||||||
Cron Jobs <cronjob.rst>
|
Cron Jobs <cronjob.rst>
|
||||||
Info about entities <entity-info.rst>
|
Info about entities <entity-info.rst>
|
||||||
|
Info about database (in French) <database-principles.rst>
|
||||||
|
|
||||||
Layout and UI
|
Layout and UI
|
||||||
**************
|
**************
|
||||||
|
@ -18,11 +18,17 @@ use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
|
|||||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||||
use Chill\ActivityBundle\Repository\ActivityTypeCategoryRepository;
|
use Chill\ActivityBundle\Repository\ActivityTypeCategoryRepository;
|
||||||
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
|
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
|
||||||
|
use Chill\ActivityBundle\Repository\ActivityUserJobRepository;
|
||||||
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
||||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||||
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Repository\LocationRepository;
|
use Chill\MainBundle\Repository\LocationRepository;
|
||||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||||
@ -47,68 +53,26 @@ use function array_key_exists;
|
|||||||
|
|
||||||
final class ActivityController extends AbstractController
|
final class ActivityController extends AbstractController
|
||||||
{
|
{
|
||||||
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
|
|
||||||
|
|
||||||
private ActivityACLAwareRepositoryInterface $activityACLAwareRepository;
|
|
||||||
|
|
||||||
private ActivityRepository $activityRepository;
|
|
||||||
|
|
||||||
private ActivityTypeCategoryRepository $activityTypeCategoryRepository;
|
|
||||||
|
|
||||||
private ActivityTypeRepositoryInterface $activityTypeRepository;
|
|
||||||
|
|
||||||
private CenterResolverManagerInterface $centerResolver;
|
|
||||||
|
|
||||||
private EntityManagerInterface $entityManager;
|
|
||||||
|
|
||||||
private EventDispatcherInterface $eventDispatcher;
|
|
||||||
|
|
||||||
private LocationRepository $locationRepository;
|
|
||||||
|
|
||||||
private LoggerInterface $logger;
|
|
||||||
|
|
||||||
private PersonRepository $personRepository;
|
|
||||||
|
|
||||||
private SerializerInterface $serializer;
|
|
||||||
|
|
||||||
private ThirdPartyRepository $thirdPartyRepository;
|
|
||||||
|
|
||||||
private TranslatorInterface $translator;
|
|
||||||
|
|
||||||
private UserRepositoryInterface $userRepository;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
|
private readonly ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
|
||||||
ActivityTypeRepositoryInterface $activityTypeRepository,
|
private readonly ActivityTypeRepositoryInterface $activityTypeRepository,
|
||||||
ActivityTypeCategoryRepository $activityTypeCategoryRepository,
|
private readonly ActivityTypeCategoryRepository $activityTypeCategoryRepository,
|
||||||
PersonRepository $personRepository,
|
private readonly PersonRepository $personRepository,
|
||||||
ThirdPartyRepository $thirdPartyRepository,
|
private readonly ThirdPartyRepository $thirdPartyRepository,
|
||||||
LocationRepository $locationRepository,
|
private readonly LocationRepository $locationRepository,
|
||||||
ActivityRepository $activityRepository,
|
private readonly ActivityRepository $activityRepository,
|
||||||
AccompanyingPeriodRepository $accompanyingPeriodRepository,
|
private readonly AccompanyingPeriodRepository $accompanyingPeriodRepository,
|
||||||
EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
EventDispatcherInterface $eventDispatcher,
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
SerializerInterface $serializer,
|
private readonly SerializerInterface $serializer,
|
||||||
UserRepositoryInterface $userRepository,
|
private readonly UserRepositoryInterface $userRepository,
|
||||||
CenterResolverManagerInterface $centerResolver,
|
private readonly CenterResolverManagerInterface $centerResolver,
|
||||||
TranslatorInterface $translator
|
private readonly TranslatorInterface $translator,
|
||||||
|
private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
|
||||||
|
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
private readonly PaginatorFactory $paginatorFactory,
|
||||||
) {
|
) {
|
||||||
$this->activityACLAwareRepository = $activityACLAwareRepository;
|
|
||||||
$this->activityTypeRepository = $activityTypeRepository;
|
|
||||||
$this->activityTypeCategoryRepository = $activityTypeCategoryRepository;
|
|
||||||
$this->personRepository = $personRepository;
|
|
||||||
$this->thirdPartyRepository = $thirdPartyRepository;
|
|
||||||
$this->locationRepository = $locationRepository;
|
|
||||||
$this->activityRepository = $activityRepository;
|
|
||||||
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
|
|
||||||
$this->entityManager = $entityManager;
|
|
||||||
$this->eventDispatcher = $eventDispatcher;
|
|
||||||
$this->logger = $logger;
|
|
||||||
$this->serializer = $serializer;
|
|
||||||
$this->userRepository = $userRepository;
|
|
||||||
$this->centerResolver = $centerResolver;
|
|
||||||
$this->translator = $translator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -289,14 +253,31 @@ final class ActivityController extends AbstractController
|
|||||||
{
|
{
|
||||||
$view = null;
|
$view = null;
|
||||||
$activities = [];
|
$activities = [];
|
||||||
// TODO: add pagination
|
|
||||||
|
|
||||||
[$person, $accompanyingPeriod] = $this->getEntity($request);
|
[$person, $accompanyingPeriod] = $this->getEntity($request);
|
||||||
|
$filter = $this->buildFilterOrder($person ?? $accompanyingPeriod);
|
||||||
|
|
||||||
|
$filterArgs = [
|
||||||
|
'my_activities' => $filter->getSingleCheckboxData('my_activities'),
|
||||||
|
'types' => $filter->hasEntityChoice('activity_types') ? $filter->getEntityChoiceData('activity_types') : [],
|
||||||
|
'jobs' => $filter->hasEntityChoice('jobs') ? $filter->getEntityChoiceData('jobs') : [],
|
||||||
|
'before' => $filter->getDateRangeData('activity_date')['to'],
|
||||||
|
'after' => $filter->getDateRangeData('activity_date')['from'],
|
||||||
|
];
|
||||||
|
|
||||||
if ($person instanceof Person) {
|
if ($person instanceof Person) {
|
||||||
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $person);
|
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $person);
|
||||||
|
$count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE, $filterArgs);
|
||||||
|
$paginator = $this->paginatorFactory->create($count);
|
||||||
$activities = $this->activityACLAwareRepository
|
$activities = $this->activityACLAwareRepository
|
||||||
->findByPerson($person, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']);
|
->findByPerson(
|
||||||
|
$person,
|
||||||
|
ActivityVoter::SEE,
|
||||||
|
$paginator->getCurrentPageFirstItemNumber(),
|
||||||
|
$paginator->getItemsPerPage(),
|
||||||
|
['date' => 'DESC', 'id' => 'DESC'],
|
||||||
|
$filterArgs
|
||||||
|
);
|
||||||
|
|
||||||
$event = new PrivacyEvent($person, [
|
$event = new PrivacyEvent($person, [
|
||||||
'element_class' => Activity::class,
|
'element_class' => Activity::class,
|
||||||
@ -308,10 +289,21 @@ final class ActivityController extends AbstractController
|
|||||||
} elseif ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
} elseif ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||||
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod);
|
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod);
|
||||||
|
|
||||||
|
$count = $this->activityACLAwareRepository->countByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, $filterArgs);
|
||||||
|
$paginator = $this->paginatorFactory->create($count);
|
||||||
$activities = $this->activityACLAwareRepository
|
$activities = $this->activityACLAwareRepository
|
||||||
->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']);
|
->findByAccompanyingPeriod(
|
||||||
|
$accompanyingPeriod,
|
||||||
|
ActivityVoter::SEE,
|
||||||
|
$paginator->getCurrentPageFirstItemNumber(),
|
||||||
|
$paginator->getItemsPerPage(),
|
||||||
|
['date' => 'DESC', 'id' => 'DESC'],
|
||||||
|
$filterArgs
|
||||||
|
);
|
||||||
|
|
||||||
$view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig';
|
$view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig';
|
||||||
|
} else {
|
||||||
|
throw new \LogicException("Unsupported");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
@ -320,10 +312,47 @@ final class ActivityController extends AbstractController
|
|||||||
'activities' => $activities,
|
'activities' => $activities,
|
||||||
'person' => $person,
|
'person' => $person,
|
||||||
'accompanyingCourse' => $accompanyingPeriod,
|
'accompanyingCourse' => $accompanyingPeriod,
|
||||||
|
'filter' => $filter,
|
||||||
|
'paginator' => $paginator,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildFilterOrder(AccompanyingPeriod|Person $associated): FilterOrderHelper
|
||||||
|
{
|
||||||
|
|
||||||
|
$filterBuilder = $this->filterOrderHelperFactory->create(self::class);
|
||||||
|
$types = $this->activityACLAwareRepository->findActivityTypeByAssociated($associated);
|
||||||
|
$jobs = $this->activityACLAwareRepository->findUserJobByAssociated($associated);
|
||||||
|
|
||||||
|
$filterBuilder
|
||||||
|
->addDateRange('activity_date', 'activity.date')
|
||||||
|
->addSingleCheckbox('my_activities', 'activity_filter.My activities');
|
||||||
|
|
||||||
|
if (1 < count($types)) {
|
||||||
|
$filterBuilder
|
||||||
|
->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [
|
||||||
|
'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) {
|
||||||
|
$text = match ($activityType->hasCategory()) {
|
||||||
|
true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ',
|
||||||
|
false => '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return $text . $this->translatableStringHelper->localize($activityType->getName());
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 < count($jobs)) {
|
||||||
|
$filterBuilder
|
||||||
|
->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [
|
||||||
|
'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filterBuilder->build();
|
||||||
|
}
|
||||||
|
|
||||||
public function newAction(Request $request): Response
|
public function newAction(Request $request): Response
|
||||||
{
|
{
|
||||||
$view = null;
|
$view = null;
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\ActivityBundle\Export\Aggregator;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Export\Declarations;
|
||||||
|
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
|
use Chill\MainBundle\Repository\LocationRepository;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Closure;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use function in_array;
|
||||||
|
|
||||||
|
final readonly class ActivityLocationAggregator implements AggregatorInterface
|
||||||
|
{
|
||||||
|
public const KEY = 'activity_location_aggregator';
|
||||||
|
|
||||||
|
public function addRole(): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function alterQuery(QueryBuilder $qb, $data)
|
||||||
|
{
|
||||||
|
if (!in_array('actloc', $qb->getAllAliases(), true)) {
|
||||||
|
$qb->leftJoin('activity.location', 'actloc');
|
||||||
|
}
|
||||||
|
$qb->addSelect(sprintf('actloc.name AS %s', self::KEY));
|
||||||
|
$qb->addGroupBy(self::KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyOn(): string
|
||||||
|
{
|
||||||
|
return Declarations::ACTIVITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
// no form required for this aggregator
|
||||||
|
}
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels($key, array $values, $data): Closure
|
||||||
|
{
|
||||||
|
return function ($value): string {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.aggregator.activity.by_location.Activity Location';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value || '' === $value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryKeys($data): array
|
||||||
|
{
|
||||||
|
return [self::KEY];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return 'export.aggregator.activity.by_location.Title';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\ActivityBundle\Export\Filter\ACPFilters;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return 'export.filter.activity.course_having_activity_between_date.Title';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('start_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity after'
|
||||||
|
])
|
||||||
|
->add('end_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity before'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||||
|
'end_date' => new RollingDate(RollingDate::T_TODAY)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function describeAction($data, $format = 'string')
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'export.filter.activity.course_having_activity_between_date.Only course having an activity between from and to',
|
||||||
|
[
|
||||||
|
'from' => $this->rollingDateConverter->convert($data['start_date']),
|
||||||
|
'to' => $this->rollingDateConverter->convert($data['end_date']),
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRole(): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function alterQuery(QueryBuilder $qb, $data)
|
||||||
|
{
|
||||||
|
$alias = 'act_period_having_act_betw_date_alias';
|
||||||
|
$from = 'act_period_having_act_betw_date_start';
|
||||||
|
$to = 'act_period_having_act_betw_date_end';
|
||||||
|
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->exists(
|
||||||
|
'SELECT 1 FROM ' . Activity::class . " {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->setParameter($from, $this->rollingDateConverter->convert($data['start_date']))
|
||||||
|
->setParameter($to, $this->rollingDateConverter->convert($data['end_date']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyOn()
|
||||||
|
{
|
||||||
|
return \Chill\PersonBundle\Export\Declarations::ACP_TYPE;
|
||||||
|
}
|
||||||
|
}
|
@ -18,67 +18,193 @@ use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
|||||||
use Chill\MainBundle\Entity\Location;
|
use Chill\MainBundle\Entity\Location;
|
||||||
use Chill\MainBundle\Entity\LocationType;
|
use Chill\MainBundle\Entity\LocationType;
|
||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||||
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\AbstractQuery;
|
use Doctrine\ORM\AbstractQuery;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\Security\Core\Role\Role;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
use function count;
|
use function count;
|
||||||
use function in_array;
|
use function in_array;
|
||||||
|
|
||||||
final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface
|
final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface
|
||||||
{
|
{
|
||||||
private AuthorizationHelper $authorizationHelper;
|
|
||||||
|
|
||||||
private CenterResolverDispatcherInterface $centerResolverDispatcher;
|
|
||||||
|
|
||||||
private EntityManagerInterface $em;
|
|
||||||
|
|
||||||
private ActivityRepository $repository;
|
|
||||||
|
|
||||||
private Security $security;
|
|
||||||
|
|
||||||
private TokenStorageInterface $tokenStorage;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
AuthorizationHelper $authorizationHelper,
|
private AuthorizationHelperForCurrentUserInterface $authorizationHelper,
|
||||||
CenterResolverDispatcherInterface $centerResolverDispatcher,
|
private CenterResolverManagerInterface $centerResolverManager,
|
||||||
TokenStorageInterface $tokenStorage,
|
private ActivityRepository $repository,
|
||||||
ActivityRepository $repository,
|
private EntityManagerInterface $em,
|
||||||
EntityManagerInterface $em,
|
private Security $security,
|
||||||
Security $security
|
private RequestStack $requestStack,
|
||||||
) {
|
) {
|
||||||
$this->authorizationHelper = $authorizationHelper;
|
|
||||||
$this->centerResolverDispatcher = $centerResolverDispatcher;
|
|
||||||
$this->tokenStorage = $tokenStorage;
|
|
||||||
$this->repository = $repository;
|
|
||||||
$this->em = $em;
|
|
||||||
$this->security = $security;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array
|
/**
|
||||||
|
* @throws NonUniqueResultException
|
||||||
|
* @throws NoResultException
|
||||||
|
*/
|
||||||
|
public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int
|
||||||
{
|
{
|
||||||
$user = $this->security->getUser();
|
$qb = $this->buildBaseQuery($filters);
|
||||||
$center = $this->centerResolverDispatcher->resolveCenter($period);
|
|
||||||
|
|
||||||
if (0 === count($orderBy)) {
|
$qb
|
||||||
$orderBy = ['date' => 'DESC'];
|
->select('COUNT(a)')
|
||||||
|
->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period);
|
||||||
|
|
||||||
|
return $qb->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countByPerson(Person $person, string $role, array $filters = []): int
|
||||||
|
{
|
||||||
|
$qb = $this->buildBaseQuery($filters);
|
||||||
|
|
||||||
|
$qb = $this->filterBaseQueryByPerson($qb, $person, $role);
|
||||||
|
|
||||||
|
$qb->select('COUNT(a)');
|
||||||
|
|
||||||
|
return $qb->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array
|
||||||
|
{
|
||||||
|
$qb = $this->buildBaseQuery($filters);
|
||||||
|
|
||||||
|
$qb->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period);
|
||||||
|
|
||||||
|
foreach ($orderBy as $field => $order) {
|
||||||
|
$qb->addOrderBy('a.' . $field, $order);
|
||||||
}
|
}
|
||||||
|
|
||||||
$scopes = $this->authorizationHelper
|
if (null !== $start) {
|
||||||
->getReachableCircles($user, $role, $center);
|
$qb->setFirstResult($start);
|
||||||
|
}
|
||||||
|
if (null !== $limit) {
|
||||||
|
$qb->setMaxResults($limit);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->em->getRepository(Activity::class)
|
return $qb->getQuery()->getResult();
|
||||||
->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function buildBaseQuery(array $filters): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->repository
|
||||||
|
->createQueryBuilder('a')
|
||||||
|
;
|
||||||
|
|
||||||
|
if (($filters['my_activities'] ?? false) and ($user = $this->security->getUser()) instanceof User) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
'a.createdBy = :user',
|
||||||
|
'a.user = :user',
|
||||||
|
':user MEMBER OF a.users'
|
||||||
|
)
|
||||||
|
)->setParameter('user', $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] !== ($types = $filters['types'] ?? [])) {
|
||||||
|
$qb->andWhere('a.activityType IN (:types)')->setParameter('types', $types);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] !== ($jobs = $filters['jobs'] ?? [])) {
|
||||||
|
$qb
|
||||||
|
->leftJoin('a.createdBy', 'creator')
|
||||||
|
->leftJoin('a.user', 'activity_u')
|
||||||
|
->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
'creator.userJob IN (:jobs)',
|
||||||
|
'activity_u.userJob IN (:jobs)',
|
||||||
|
'EXISTS (SELECT 1 FROM ' . User::class . ' activity_user WHERE activity_user MEMBER OF a.users AND activity_user.userJob IN (:jobs))'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->setParameter('jobs', $jobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== ($after = $filters['after'] ?? null)) {
|
||||||
|
$qb->andWhere('a.date >= :after')->setParameter('after', $after);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== ($before = $filters['before'] ?? null)) {
|
||||||
|
$qb->andWhere('a.date <= :before')->setParameter('before', $before);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param AccompanyingPeriod|Person $associated
|
||||||
|
* @return array<ActivityType>
|
||||||
|
*/
|
||||||
|
public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array
|
||||||
|
{
|
||||||
|
$in = $this->em->createQueryBuilder();
|
||||||
|
$in
|
||||||
|
->select('1')
|
||||||
|
->from(Activity::class, 'a');
|
||||||
|
|
||||||
|
if ($associated instanceof Person) {
|
||||||
|
$in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE);
|
||||||
|
} else {
|
||||||
|
$in->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $associated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// join between the embedded exist query and the main query
|
||||||
|
$in->andWhere('a.activityType = t');
|
||||||
|
|
||||||
|
$qb = $this->em->createQueryBuilder()->setParameters($in->getParameters());
|
||||||
|
$qb
|
||||||
|
->select('t')
|
||||||
|
->from(ActivityType::class, 't')
|
||||||
|
->where(
|
||||||
|
$qb->expr()->exists($in->getDQL())
|
||||||
|
);
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findUserJobByAssociated(Person|AccompanyingPeriod $associated): array
|
||||||
|
{
|
||||||
|
$in = $this->em->createQueryBuilder();
|
||||||
|
$in->select('IDENTITY(u.userJob)')
|
||||||
|
->from(User::class, 'u')
|
||||||
|
->join(
|
||||||
|
Activity::class,
|
||||||
|
'a',
|
||||||
|
Join::WITH,
|
||||||
|
'a.createdBy = u OR a.user = u OR u MEMBER OF a.users'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($associated instanceof Person) {
|
||||||
|
$in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE);
|
||||||
|
} else {
|
||||||
|
$in->andWhere('a.accompanyingPeriod = :associated');
|
||||||
|
$in->setParameter('associated', $associated);
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->em->createQueryBuilder()->setParameters($in->getParameters());
|
||||||
|
|
||||||
|
$qb->select('ub', 'JSON_EXTRACT(ub.label, :lang) AS HIDDEN lang')
|
||||||
|
->from(UserJob::class, 'ub')
|
||||||
|
->where($qb->expr()->in('ub.id', $in->getDQL()))
|
||||||
|
->setParameter('lang', $this->requestStack->getCurrentRequest()->getLocale())
|
||||||
|
->orderBy('lang')
|
||||||
|
;
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array
|
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array
|
||||||
{
|
{
|
||||||
$rsm = new ResultSetMappingBuilder($this->em);
|
$rsm = new ResultSetMappingBuilder($this->em);
|
||||||
@ -159,25 +285,73 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
|
|||||||
return $nq->getResult(AbstractQuery::HYDRATE_ARRAY);
|
return $nq->getResult(AbstractQuery::HYDRATE_ARRAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): array
|
||||||
* @param array $orderBy
|
|
||||||
*
|
|
||||||
* @return Activity[]|array
|
|
||||||
*/
|
|
||||||
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array
|
|
||||||
{
|
{
|
||||||
$user = $this->security->getUser();
|
$qb = $this->buildBaseQuery($filters);
|
||||||
$center = $this->centerResolverDispatcher->resolveCenter($person);
|
|
||||||
|
|
||||||
if (0 === count($orderBy)) {
|
$qb = $this->filterBaseQueryByPerson($qb, $person, $role);
|
||||||
$orderBy = ['date' => 'DESC'];
|
|
||||||
|
foreach ($orderBy as $field => $direction) {
|
||||||
|
$qb->addOrderBy('a.' . $field, $direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
$reachableScopes = $this->authorizationHelper
|
if (null !== $start) {
|
||||||
->getReachableCircles($user, $role, $center);
|
$qb->setFirstResult($start);
|
||||||
|
}
|
||||||
|
if (null !== $limit) {
|
||||||
|
$qb->setMaxResults($limit);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->em->getRepository(Activity::class)
|
return $qb->getQuery()->getResult();
|
||||||
->findByPersonImplied($person, $reachableScopes, $orderBy, $limit, $start);
|
}
|
||||||
|
|
||||||
|
private function filterBaseQueryByPerson(QueryBuilder $qb, Person $person, string $role): QueryBuilder
|
||||||
|
{
|
||||||
|
$orX = $qb->expr()->orX();
|
||||||
|
$counter = 0;
|
||||||
|
foreach ($this->centerResolverManager->resolveCenters($person) as $center) {
|
||||||
|
$scopes = $this->authorizationHelper->getReachableScopes($role, $center);
|
||||||
|
|
||||||
|
if ([] === $scopes) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orX->add(sprintf('a.person = :person AND a.scope IN (:scopes_%d)', $counter));
|
||||||
|
$qb->setParameter(sprintf('scopes_%d', $counter), $scopes);
|
||||||
|
$qb->setParameter('person', $person);
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
|
||||||
|
if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$and = $qb->expr()->andX(
|
||||||
|
sprintf('a.accompanyingPeriod = :period_%d', $counter),
|
||||||
|
sprintf('a.date >= :participation_start_%d', $counter)
|
||||||
|
);
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->setParameter(sprintf('period_%d', $counter), $participation->getAccompanyingPeriod())
|
||||||
|
->setParameter(sprintf('participation_start_%d', $counter), $participation->getStartDate());
|
||||||
|
|
||||||
|
if (null !== $participation->getEndDate()) {
|
||||||
|
$and->add(sprintf('a.date < :participation_end_%d', $counter));
|
||||||
|
$qb
|
||||||
|
->setParameter(sprintf('participation_end_%d', $counter), $participation->getEndDate());
|
||||||
|
}
|
||||||
|
$orX->add($and);
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === $orX->count()) {
|
||||||
|
$qb->andWhere('FALSE = TRUE');
|
||||||
|
} else {
|
||||||
|
$qb->andWhere($orX);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function queryTimelineIndexer(string $context, array $args = []): array
|
public function queryTimelineIndexer(string $context, array $args = []): array
|
||||||
@ -226,7 +400,6 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
|
|||||||
|
|
||||||
// acls:
|
// acls:
|
||||||
$reachableCenters = $this->authorizationHelper->getReachableCenters(
|
$reachableCenters = $this->authorizationHelper->getReachableCenters(
|
||||||
$this->tokenStorage->getToken()->getUser(),
|
|
||||||
ActivityVoter::SEE
|
ActivityVoter::SEE
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -251,7 +424,7 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// we get all the reachable scopes for this center
|
// we get all the reachable scopes for this center
|
||||||
$reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), ActivityVoter::SEE, $center);
|
$reachableScopes = $this->authorizationHelper->getReachableScopes(ActivityVoter::SEE, $center);
|
||||||
// we get the ids for those scopes
|
// we get the ids for those scopes
|
||||||
$reachablesScopesId = array_map(
|
$reachablesScopesId = array_map(
|
||||||
static fn (Scope $scope) => $scope->getId(),
|
static fn (Scope $scope) => $scope->getId(),
|
||||||
|
@ -11,15 +11,32 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\ActivityBundle\Repository;
|
namespace Chill\ActivityBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
|
use Chill\ActivityBundle\Entity\ActivityType;
|
||||||
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
|
||||||
interface ActivityACLAwareRepositoryInterface
|
interface ActivityACLAwareRepositoryInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return Activity[]|array
|
* Return all the activities associated to an accompanying period and that the user is allowed to apply the given role.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
|
||||||
|
* @return array<Activity>
|
||||||
*/
|
*/
|
||||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
|
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
|
||||||
|
*/
|
||||||
|
public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
|
||||||
|
*/
|
||||||
|
public function countByPerson(Person $person, string $role, array $filters = []): int;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a list of activities, simplified as array (not object).
|
* Return a list of activities, simplified as array (not object).
|
||||||
@ -31,7 +48,28 @@ interface ActivityACLAwareRepositoryInterface
|
|||||||
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array;
|
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Activity[]|array
|
* @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
|
||||||
|
* @return array<Activity>
|
||||||
*/
|
*/
|
||||||
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
|
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of the type for the activities associated to person or accompanying period
|
||||||
|
*
|
||||||
|
* @return array<ActivityType>
|
||||||
|
*/
|
||||||
|
public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of the user job for the activities associated to person or accompanying period
|
||||||
|
*
|
||||||
|
* Associated mean the job:
|
||||||
|
* - of the creator;
|
||||||
|
* - of the user (activity.user)
|
||||||
|
* - of all the users
|
||||||
|
*
|
||||||
|
* @return array<UserJob>
|
||||||
|
*/
|
||||||
|
public function findUserJobByAssociated(AccompanyingPeriod|Person $associated): array;
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\ActivityBundle\Repository;
|
namespace Chill\ActivityBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
use Chill\ActivityBundle\Entity\ActivityType;
|
use Chill\ActivityBundle\Entity\ActivityType;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
|
|
||||||
final class ActivityTypeRepository implements ActivityTypeRepositoryInterface
|
final class ActivityTypeRepository implements ActivityTypeRepositoryInterface
|
||||||
{
|
{
|
||||||
|
@ -12,12 +12,14 @@ declare(strict_types=1);
|
|||||||
namespace Chill\ActivityBundle\Repository;
|
namespace Chill\ActivityBundle\Repository;
|
||||||
|
|
||||||
use Chill\ActivityBundle\Entity\ActivityType;
|
use Chill\ActivityBundle\Entity\ActivityType;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
interface ActivityTypeRepositoryInterface extends ObjectRepository
|
interface ActivityTypeRepositoryInterface extends ObjectRepository
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return array|ActivityType[]
|
* @return array<ActivityType>
|
||||||
*/
|
*/
|
||||||
public function findAllActive(): array;
|
public function findAllActive(): array;
|
||||||
}
|
}
|
||||||
|
@ -80,12 +80,15 @@
|
|||||||
|
|
||||||
<div class="context-{{ context }}">
|
<div class="context-{{ context }}">
|
||||||
|
|
||||||
|
{{ filter|chill_render_filter_order_helper }}
|
||||||
|
|
||||||
{% if activities|length == 0 %}
|
{% if activities|length == 0 %}
|
||||||
<p class="chill-no-data-statement">
|
<p class="chill-no-data-statement">
|
||||||
{{ "There isn't any activities."|trans }}
|
{{ "There isn't any activities."|trans }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div class="flex-table activity-list">
|
<div class="flex-table activity-list">
|
||||||
{% for activity in activities %}
|
{% for activity in activities %}
|
||||||
{% include 'ChillActivityBundle:Activity:_list_item.html.twig' with {
|
{% include 'ChillActivityBundle:Activity:_list_item.html.twig' with {
|
||||||
@ -96,4 +99,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{{ chill_pagination(paginator) }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,325 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\ActivityBundle\Tests\Repository;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\ActivityType;
|
||||||
|
use Chill\ActivityBundle\Repository\ActivityACLAwareRepository;
|
||||||
|
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||||
|
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
||||||
|
use Chill\MainBundle\Entity\Center;
|
||||||
|
use Chill\MainBundle\Entity\Scope;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||||
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class ActivityACLAwareRepositoryTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser;
|
||||||
|
|
||||||
|
private CenterResolverManagerInterface $centerResolverManager;
|
||||||
|
|
||||||
|
private ActivityRepository $activityRepository;
|
||||||
|
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private Security $security;
|
||||||
|
|
||||||
|
private RequestStack $requestStack;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
|
||||||
|
$this->authorizationHelperForCurrentUser = self::$container->get(AuthorizationHelperForCurrentUserInterface::class);
|
||||||
|
$this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class);
|
||||||
|
$this->activityRepository = self::$container->get(ActivityRepository::class);
|
||||||
|
$this->entityManager = self::$container->get(EntityManagerInterface::class);
|
||||||
|
$this->security = self::$container->get(Security::class);
|
||||||
|
|
||||||
|
$this->requestStack = $requestStack = new RequestStack();
|
||||||
|
$request = $this->prophesize(Request::class);
|
||||||
|
$request->getLocale()->willReturn('fr');
|
||||||
|
$request->getDefaultLocale()->willReturn('fr');
|
||||||
|
$requestStack->push($request->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataFindByAccompanyingPeriod
|
||||||
|
*/
|
||||||
|
public function testFindByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void
|
||||||
|
{
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->isGranted($role, $period)->willReturn(true);
|
||||||
|
$security->getUser()->willReturn($user);
|
||||||
|
|
||||||
|
$repository = new ActivityACLAwareRepository(
|
||||||
|
$this->authorizationHelperForCurrentUser,
|
||||||
|
$this->centerResolverManager,
|
||||||
|
$this->activityRepository,
|
||||||
|
$this->entityManager,
|
||||||
|
$security->reveal(),
|
||||||
|
$this->requestStack
|
||||||
|
);
|
||||||
|
|
||||||
|
$actual = $repository->findByAccompanyingPeriod($period, $role, $start, $limit, $orderBy, $filters);
|
||||||
|
|
||||||
|
self::assertIsArray($actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataFindByAccompanyingPeriod
|
||||||
|
*/
|
||||||
|
public function testFindActivityTypeByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void
|
||||||
|
{
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->isGranted($role, $period)->willReturn(true);
|
||||||
|
$security->getUser()->willReturn($user);
|
||||||
|
|
||||||
|
$repository = new ActivityACLAwareRepository(
|
||||||
|
$this->authorizationHelperForCurrentUser,
|
||||||
|
$this->centerResolverManager,
|
||||||
|
$this->activityRepository,
|
||||||
|
$this->entityManager,
|
||||||
|
$security->reveal(),
|
||||||
|
$this->requestStack
|
||||||
|
);
|
||||||
|
|
||||||
|
$actual = $repository->findActivityTypeByAssociated($period);
|
||||||
|
|
||||||
|
self::assertIsArray($actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataFindByPerson
|
||||||
|
*/
|
||||||
|
public function testFindActivityTypeByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void
|
||||||
|
{
|
||||||
|
$role = ActivityVoter::SEE;
|
||||||
|
$centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
|
||||||
|
$centerResolver->resolveCenters($person)->willReturn($centers);
|
||||||
|
|
||||||
|
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
||||||
|
$authorizationHelper->getReachableScopes($role, Argument::type(Center::class))
|
||||||
|
->willReturn($scopes);
|
||||||
|
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true);
|
||||||
|
$security->getUser()->willReturn($user);
|
||||||
|
|
||||||
|
$repository = new ActivityACLAwareRepository(
|
||||||
|
$authorizationHelper->reveal(),
|
||||||
|
$centerResolver->reveal(),
|
||||||
|
$this->activityRepository,
|
||||||
|
$this->entityManager,
|
||||||
|
$security->reveal(),
|
||||||
|
$this->requestStack
|
||||||
|
);
|
||||||
|
|
||||||
|
$actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters);
|
||||||
|
|
||||||
|
self::assertIsArray($actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataFindByPerson
|
||||||
|
*/
|
||||||
|
public function testFindByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void
|
||||||
|
{
|
||||||
|
$centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
|
||||||
|
$centerResolver->resolveCenters($person)->willReturn($centers);
|
||||||
|
|
||||||
|
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
||||||
|
$authorizationHelper->getReachableScopes($role, Argument::type(Center::class))
|
||||||
|
->willReturn($scopes);
|
||||||
|
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true);
|
||||||
|
$security->getUser()->willReturn($user);
|
||||||
|
|
||||||
|
$repository = new ActivityACLAwareRepository(
|
||||||
|
$authorizationHelper->reveal(),
|
||||||
|
$centerResolver->reveal(),
|
||||||
|
$this->activityRepository,
|
||||||
|
$this->entityManager,
|
||||||
|
$security->reveal(),
|
||||||
|
$this->requestStack
|
||||||
|
);
|
||||||
|
|
||||||
|
$actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters);
|
||||||
|
|
||||||
|
self::assertIsArray($actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideDataFindByPerson(): iterable
|
||||||
|
{
|
||||||
|
$this->setUp();
|
||||||
|
|
||||||
|
/** @var Person $person */
|
||||||
|
if (null === $person = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('p')->from(Person::class, 'p')->setMaxResults(1)
|
||||||
|
->getQuery()->getSingleResult()) {
|
||||||
|
throw new \RuntimeException("person not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var AccompanyingPeriod $period1 */
|
||||||
|
if (null === $period1 = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('a')
|
||||||
|
->from(AccompanyingPeriod::class, 'a')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult()) {
|
||||||
|
throw new \RuntimeException("no period found");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var AccompanyingPeriod $period2 */
|
||||||
|
if (null === $period2 = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('a')
|
||||||
|
->from(AccompanyingPeriod::class, 'a')
|
||||||
|
->where('a.id > :pid')
|
||||||
|
->setParameter('pid', $period1->getId())
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult()) {
|
||||||
|
throw new \RuntimeException("no second period found");
|
||||||
|
}
|
||||||
|
// add a period
|
||||||
|
$period1->addPerson($person);
|
||||||
|
$period2->addPerson($person);
|
||||||
|
$period1->getParticipationsContainsPerson($person)->first()->setEndDate(
|
||||||
|
(new \DateTime('now'))->add(new \DateInterval('P1M'))
|
||||||
|
);
|
||||||
|
|
||||||
|
if ([] === $types = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('t')
|
||||||
|
->from(ActivityType::class, 't')
|
||||||
|
->setMaxResults(2)
|
||||||
|
->getQuery()
|
||||||
|
->getResult()) {
|
||||||
|
throw new \RuntimeException("no types");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] === $jobs = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('j')
|
||||||
|
->from(UserJob::class, 'j')
|
||||||
|
->setMaxResults(2)
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
) {
|
||||||
|
throw new \RuntimeException("no jobs found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $user = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('u')
|
||||||
|
->from(User::class, 'u')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult()
|
||||||
|
) {
|
||||||
|
throw new \RuntimeException("no user found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] === $centers = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('c')->from(Center::class, 'c')->setMaxResults(2)->getQuery()
|
||||||
|
->getResult()) {
|
||||||
|
throw new \RuntimeException("no centers found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] === $scopes = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('s')->from(Scope::class, 's')->setMaxResults(2)->getQuery()
|
||||||
|
->getResult()) {
|
||||||
|
throw new \RuntimeException("no scopes found");
|
||||||
|
}
|
||||||
|
|
||||||
|
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], []];
|
||||||
|
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['my_activities' => true]];
|
||||||
|
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['types' => $types]];
|
||||||
|
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['jobs' => $jobs]];
|
||||||
|
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]];
|
||||||
|
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
|
||||||
|
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideDataFindByAccompanyingPeriod(): iterable
|
||||||
|
{
|
||||||
|
$this->setUp();
|
||||||
|
|
||||||
|
if (null === $period = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('a')
|
||||||
|
->from(AccompanyingPeriod::class, 'a')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult()) {
|
||||||
|
throw new \RuntimeException("no period found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] === $types = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('t')
|
||||||
|
->from(ActivityType::class, 't')
|
||||||
|
->setMaxResults(2)
|
||||||
|
->getQuery()
|
||||||
|
->getResult()) {
|
||||||
|
throw new \RuntimeException("no types");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] === $jobs = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('j')
|
||||||
|
->from(UserJob::class, 'j')
|
||||||
|
->setMaxResults(2)
|
||||||
|
->getQuery()
|
||||||
|
->getResult()
|
||||||
|
) {
|
||||||
|
throw new \RuntimeException("no jobs found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $user = $this->entityManager
|
||||||
|
->createQueryBuilder()
|
||||||
|
->select('u')
|
||||||
|
->from(User::class, 'u')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult()
|
||||||
|
) {
|
||||||
|
throw new \RuntimeException("no user found");
|
||||||
|
}
|
||||||
|
|
||||||
|
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []];
|
||||||
|
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]];
|
||||||
|
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]];
|
||||||
|
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]];
|
||||||
|
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]];
|
||||||
|
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
|
||||||
|
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
|
||||||
|
}
|
||||||
|
}
|
@ -135,6 +135,10 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' }
|
- { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' }
|
||||||
|
|
||||||
|
Chill\ActivityBundle\Export\Filter\ACPFilters\PeriodHavingActivityBetweenDatesFilter:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export_filter, alias: 'period_having_activity_betw_dates_filter' }
|
||||||
|
|
||||||
## Aggregators
|
## Aggregators
|
||||||
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator:
|
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator:
|
||||||
tags:
|
tags:
|
||||||
@ -144,6 +148,10 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: chill.export_aggregator, alias: activity_common_type_aggregator }
|
- { name: chill.export_aggregator, alias: activity_common_type_aggregator }
|
||||||
|
|
||||||
|
Chill\ActivityBundle\Export\Aggregator\ActivityLocationAggregator:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export_aggregator, alias: activity_common_location_aggregator }
|
||||||
|
|
||||||
chill.activity.export.user_aggregator:
|
chill.activity.export.user_aggregator:
|
||||||
class: Chill\ActivityBundle\Export\Aggregator\ActivityUserAggregator
|
class: Chill\ActivityBundle\Export\Aggregator\ActivityUserAggregator
|
||||||
tags:
|
tags:
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
export:
|
||||||
|
filter:
|
||||||
|
activity:
|
||||||
|
course_having_activity_between_date:
|
||||||
|
Only course having an activity between from and to: Seulement les parcours ayant reçu au moins un échange entre le {from, date, short} et le {to, date, short}
|
@ -83,12 +83,20 @@ Third persons: Tiers non-pro.
|
|||||||
Others persons: Usagers
|
Others persons: Usagers
|
||||||
Third parties: Tiers professionnels
|
Third parties: Tiers professionnels
|
||||||
Users concerned: T(M)S
|
Users concerned: T(M)S
|
||||||
|
|
||||||
activity:
|
activity:
|
||||||
|
date: Date de l'échange
|
||||||
Insert a document: Insérer un document
|
Insert a document: Insérer un document
|
||||||
Remove a document: Supprimer le document
|
Remove a document: Supprimer le document
|
||||||
comment: Commentaire
|
comment: Commentaire
|
||||||
No documents: Aucun document
|
No documents: Aucun document
|
||||||
|
|
||||||
|
# activity filter in list page
|
||||||
|
activity_filter:
|
||||||
|
My activities: Mes échanges (où j'interviens)
|
||||||
|
Types: Par type d'échange
|
||||||
|
Jobs: Par métier impliqué
|
||||||
|
|
||||||
#timeline
|
#timeline
|
||||||
'%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"'
|
'%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"'
|
||||||
|
|
||||||
@ -365,6 +373,12 @@ export:
|
|||||||
by_usersscope:
|
by_usersscope:
|
||||||
Filter by users scope: Filtrer les échanges par services d'au moins un utilisateur participant
|
Filter by users scope: Filtrer les échanges par services d'au moins un utilisateur participant
|
||||||
'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%'
|
'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%'
|
||||||
|
course_having_activity_between_date:
|
||||||
|
Title: Filtre les parcours ayant reçu un échange entre deux dates
|
||||||
|
Receiving an activity after: Ayant reçu un échange après le
|
||||||
|
Receiving an activity before: Ayant reçu un échange avant le
|
||||||
|
|
||||||
|
|
||||||
aggregator:
|
aggregator:
|
||||||
activity:
|
activity:
|
||||||
by_sent_received:
|
by_sent_received:
|
||||||
@ -372,6 +386,9 @@ export:
|
|||||||
is sent: envoyé
|
is sent: envoyé
|
||||||
is received: reçu
|
is received: reçu
|
||||||
Group activity by sentreceived: Grouper les échanges par envoyé / reçu
|
Group activity by sentreceived: Grouper les échanges par envoyé / reçu
|
||||||
|
by_location:
|
||||||
|
Activity Location: Localisation de l'échange
|
||||||
|
Title: Grouper les échanges par localisation de l'échange
|
||||||
|
|
||||||
generic_doc:
|
generic_doc:
|
||||||
filter:
|
filter:
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
{% macro table_elements(elements, family) %}
|
{% macro table_elements(elements, type) %}
|
||||||
|
|
||||||
<table class="table table-bordered border-dark budget-table">
|
<table class="table table-bordered border-dark budget-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="{{ family }} el-type">{{ 'Budget element type'|trans }}</th>
|
<th class="{{ type }} el-type">{{ 'Budget element type'|trans }}</th>
|
||||||
<th class="{{ family }}">{{ 'Amount'|trans }}</th>
|
<th class="{{ type }}">{{ 'Amount'|trans }}</th>
|
||||||
<th class="{{ family }}">{{ 'Validity period'|trans }}</th>
|
<th class="{{ type }}">{{ 'Validity period'|trans }}</th>
|
||||||
<th class="{{ family }}"> </th>
|
<th class="{{ type }}"> </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -38,17 +39,17 @@
|
|||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
{% if is_granted('CHILL_BUDGET_ELEMENT_SEE', f) %}
|
{% if is_granted('CHILL_BUDGET_ELEMENT_SEE', f) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_budget_' ~ family ~ '_view', { 'id': f.id } ) }}" class="btn btn-sm btn-show"></a>
|
<a href="{{ path('chill_budget_' ~ type ~ '_view', { 'id': f.id } ) }}" class="btn btn-sm btn-show"></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_granted('CHILL_BUDGET_ELEMENT_UPDATE', f) %}
|
{% if is_granted('CHILL_BUDGET_ELEMENT_UPDATE', f) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_budget_' ~ family ~'_edit', { 'id': f.id } ) }}" class="btn btn-sm btn-edit"></a>
|
<a href="{{ path('chill_budget_' ~ type ~'_edit', { 'id': f.id } ) }}" class="btn btn-sm btn-edit"></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_granted('CHILL_BUDGET_ELEMENT_DELETE', f) %}
|
{% if is_granted('CHILL_BUDGET_ELEMENT_DELETE', f) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_budget_' ~ family ~ '_delete', { 'id': f.id } ) }}" class="btn btn-sm btn-delete"></a>
|
<a href="{{ path('chill_budget_' ~ type ~ '_delete', { 'id': f.id } ) }}" class="btn btn-sm btn-delete"></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -69,7 +70,7 @@
|
|||||||
</table>
|
</table>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro table_results(actualCharges, actualResources) %}
|
{% macro table_results(actualCharges, actualResources, results) %}
|
||||||
|
|
||||||
{% set totalCharges = 0 %}
|
{% set totalCharges = 0 %}
|
||||||
{% for c in actualCharges %}
|
{% for c in actualCharges %}
|
||||||
@ -97,6 +98,20 @@
|
|||||||
{{ result|format_currency('EUR') }}
|
{{ result|format_currency('EUR') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% for result in results %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ result.label }}</td>
|
||||||
|
<td>
|
||||||
|
{% if result.type == 'currency' %}
|
||||||
|
{{ result.result|format_currency('EUR') }}
|
||||||
|
{% elseif result.type == 'percentage' %}
|
||||||
|
{{ result.result|round(2, 'ceil') ~ '%' }}
|
||||||
|
{% else %}
|
||||||
|
{{ result.result|round(2, 'common') }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
<h3 class="subtitle">{{ 'Budget calculator'|trans }}</h3>
|
<h3 class="subtitle">{{ 'Budget calculator'|trans }}</h3>
|
||||||
{{ table_results(charges, resources) }}
|
{{ table_results(charges, resources, results) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_granted('CHILL_BUDGET_ELEMENT_CREATE', person) %}
|
{% if is_granted('CHILL_BUDGET_ELEMENT_CREATE', person) %}
|
||||||
|
@ -147,6 +147,9 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
|
|||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$query->addWhereClause(implode(" OR ", $or), $orParams, $orTypes);
|
||||||
|
|
||||||
return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content);
|
return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,8 +46,7 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
|
|||||||
|
|
||||||
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
|
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
|
||||||
{
|
{
|
||||||
$course = $this->getRelatedEntity($entityWorkflow)
|
$course = $this->getRelatedEntity($entityWorkflow)?->getCourse();
|
||||||
->getCourse();
|
|
||||||
$persons = [];
|
$persons = [];
|
||||||
|
|
||||||
if (null !== $course) {
|
if (null !== $course) {
|
||||||
|
@ -77,6 +77,8 @@ class CenterController extends AbstractController
|
|||||||
|
|
||||||
$entities = $em->getRepository(\Chill\MainBundle\Entity\Center::class)->findAll();
|
$entities = $em->getRepository(\Chill\MainBundle\Entity\Center::class)->findAll();
|
||||||
|
|
||||||
|
usort($entities, fn (Center $a, Center $b) => $a->getName() <=> $b->getName());
|
||||||
|
|
||||||
return $this->render('@ChillMain/Center/index.html.twig', [
|
return $this->render('@ChillMain/Center/index.html.twig', [
|
||||||
'entities' => $entities,
|
'entities' => $entities,
|
||||||
]);
|
]);
|
||||||
|
@ -19,5 +19,13 @@ interface CronJobInterface
|
|||||||
|
|
||||||
public function getKey(): string;
|
public function getKey(): string;
|
||||||
|
|
||||||
public function run(): void;
|
/**
|
||||||
|
* Execute the cronjob
|
||||||
|
*
|
||||||
|
* If data is returned, this data is passed as argument on the next execution
|
||||||
|
*
|
||||||
|
* @param array $lastExecutionData the data which was returned from the previous execution
|
||||||
|
* @return array|null optionally return an array with the same data than the previous execution
|
||||||
|
*/
|
||||||
|
public function run(array $lastExecutionData): null|array;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ namespace Chill\MainBundle\Cron;
|
|||||||
use Chill\MainBundle\Entity\CronJobExecution;
|
use Chill\MainBundle\Entity\CronJobExecution;
|
||||||
use Chill\MainBundle\Repository\CronJobExecutionRepositoryInterface;
|
use Chill\MainBundle\Repository\CronJobExecutionRepositoryInterface;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@ -46,6 +47,8 @@ class CronManager implements CronManagerInterface
|
|||||||
|
|
||||||
private const UPDATE_BEFORE_EXEC = 'UPDATE ' . CronJobExecution::class . ' cr SET cr.lastStart = :now WHERE cr.key = :key';
|
private const UPDATE_BEFORE_EXEC = 'UPDATE ' . CronJobExecution::class . ' cr SET cr.lastStart = :now WHERE cr.key = :key';
|
||||||
|
|
||||||
|
private const UPDATE_LAST_EXECUTION_DATA = 'UPDATE ' . CronJobExecution::class . ' cr SET cr.lastExecutionData = :data WHERE cr.key = :key';
|
||||||
|
|
||||||
private CronJobExecutionRepositoryInterface $cronJobExecutionRepository;
|
private CronJobExecutionRepositoryInterface $cronJobExecutionRepository;
|
||||||
|
|
||||||
private EntityManagerInterface $entityManager;
|
private EntityManagerInterface $entityManager;
|
||||||
@ -85,6 +88,9 @@ class CronManager implements CronManagerInterface
|
|||||||
foreach ($orderedJobs as $job) {
|
foreach ($orderedJobs as $job) {
|
||||||
if ($job->canRun($lasts[$job->getKey()] ?? null)) {
|
if ($job->canRun($lasts[$job->getKey()] ?? null)) {
|
||||||
if (array_key_exists($job->getKey(), $lasts)) {
|
if (array_key_exists($job->getKey(), $lasts)) {
|
||||||
|
|
||||||
|
$executionData = $lasts[$job->getKey()]->getLastExecutionData();
|
||||||
|
|
||||||
$this->entityManager
|
$this->entityManager
|
||||||
->createQuery(self::UPDATE_BEFORE_EXEC)
|
->createQuery(self::UPDATE_BEFORE_EXEC)
|
||||||
->setParameters([
|
->setParameters([
|
||||||
@ -96,12 +102,17 @@ class CronManager implements CronManagerInterface
|
|||||||
$execution = new CronJobExecution($job->getKey());
|
$execution = new CronJobExecution($job->getKey());
|
||||||
$this->entityManager->persist($execution);
|
$this->entityManager->persist($execution);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$executionData = $execution->getLastExecutionData();
|
||||||
}
|
}
|
||||||
$this->entityManager->clear();
|
$this->entityManager->clear();
|
||||||
|
|
||||||
|
// note: at this step, the entity manager does not have any entity CronJobExecution
|
||||||
|
// into his internal memory
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->logger->info(sprintf('%sWill run job', self::LOG_PREFIX), ['job' => $job->getKey()]);
|
$this->logger->info(sprintf('%sWill run job', self::LOG_PREFIX), ['job' => $job->getKey()]);
|
||||||
$job->run();
|
$result = $job->run($executionData);
|
||||||
|
|
||||||
$this->entityManager
|
$this->entityManager
|
||||||
->createQuery(self::UPDATE_AFTER_EXEC)
|
->createQuery(self::UPDATE_AFTER_EXEC)
|
||||||
@ -112,6 +123,14 @@ class CronManager implements CronManagerInterface
|
|||||||
])
|
])
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
|
if (null !== $result) {
|
||||||
|
$this->entityManager
|
||||||
|
->createQuery(self::UPDATE_LAST_EXECUTION_DATA)
|
||||||
|
->setParameter('data', $result, Types::JSON)
|
||||||
|
->setParameter('key', $job->getKey(), Types::STRING)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
|
||||||
$this->logger->info(sprintf('%sSuccessfully run job', self::LOG_PREFIX), ['job' => $job->getKey()]);
|
$this->logger->info(sprintf('%sSuccessfully run job', self::LOG_PREFIX), ['job' => $job->getKey()]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -133,7 +152,7 @@ class CronManager implements CronManagerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<0: CronJobInterface[], 1: array<string, CronJobExecution>>
|
* @return array{0: array<CronJobInterface>, 1: array<string, CronJobExecution>}
|
||||||
*/
|
*/
|
||||||
private function getOrderedJobs(): array
|
private function getOrderedJobs(): array
|
||||||
{
|
{
|
||||||
@ -174,7 +193,7 @@ class CronManager implements CronManagerInterface
|
|||||||
{
|
{
|
||||||
foreach ($this->jobs as $job) {
|
foreach ($this->jobs as $job) {
|
||||||
if ($job->getKey() === $forceJob) {
|
if ($job->getKey() === $forceJob) {
|
||||||
$job->run();
|
$job->run([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ class CronJobExecution
|
|||||||
private string $key;
|
private string $key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DateTimeImmutable
|
|
||||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
|
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
|
||||||
*/
|
*/
|
||||||
private ?DateTimeImmutable $lastEnd = null;
|
private ?DateTimeImmutable $lastEnd = null;
|
||||||
@ -46,6 +45,11 @@ class CronJobExecution
|
|||||||
*/
|
*/
|
||||||
private ?int $lastStatus = null;
|
private ?int $lastStatus = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="json", options={"default": "'{}'::jsonb", "jsonb": true})
|
||||||
|
*/
|
||||||
|
private array $lastExecutionData = [];
|
||||||
|
|
||||||
public function __construct(string $key)
|
public function __construct(string $key)
|
||||||
{
|
{
|
||||||
$this->key = $key;
|
$this->key = $key;
|
||||||
@ -92,4 +96,16 @@ class CronJobExecution
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLastExecutionData(): array
|
||||||
|
{
|
||||||
|
return $this->lastExecutionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLastExecutionData(array $lastExecutionData): CronJobExecution
|
||||||
|
{
|
||||||
|
$this->lastExecutionData = $lastExecutionData;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ class UserJob
|
|||||||
protected ?int $id = null;
|
protected ?int $id = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array|string[]A
|
* @var array<string, string>
|
||||||
* @ORM\Column(name="label", type="json")
|
* @ORM\Column(name="label", type="json")
|
||||||
* @Serializer\Groups({"read", "docgen:read"})
|
* @Serializer\Groups({"read", "docgen:read"})
|
||||||
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
|
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
|
||||||
|
@ -97,7 +97,7 @@ interface ExportInterface extends ExportElementInterface
|
|||||||
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
||||||
* @param mixed $data The data from the export's form (as defined in `buildForm`)
|
* @param mixed $data The data from the export's form (as defined in `buildForm`)
|
||||||
*
|
*
|
||||||
* @return callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
* @return (callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||||
*/
|
*/
|
||||||
public function getLabels($key, array $values, $data);
|
public function getLabels($key, array $values, $data);
|
||||||
|
|
||||||
|
@ -12,7 +12,10 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Form\Type\Listing;
|
namespace Chill\MainBundle\Form\Type\Listing;
|
||||||
|
|
||||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||||
|
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SearchType;
|
use Symfony\Component\Form\Extension\Core\Type\SearchType;
|
||||||
@ -27,13 +30,6 @@ use function count;
|
|||||||
|
|
||||||
final class FilterOrderType extends \Symfony\Component\Form\AbstractType
|
final class FilterOrderType extends \Symfony\Component\Form\AbstractType
|
||||||
{
|
{
|
||||||
private RequestStack $requestStack;
|
|
||||||
|
|
||||||
public function __construct(RequestStack $requestStack)
|
|
||||||
{
|
|
||||||
$this->requestStack = $requestStack;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
/** @var FilterOrderHelper $helper */
|
/** @var FilterOrderHelper $helper */
|
||||||
@ -43,22 +39,16 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
|
|||||||
$builder->add('q', SearchType::class, [
|
$builder->add('q', SearchType::class, [
|
||||||
'label' => false,
|
'label' => false,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'placeholder' => 'filter_order.Search',
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$checkboxesBuilder = $builder->create('checkboxes', null, ['compound' => true]);
|
$checkboxesBuilder = $builder->create('checkboxes', null, ['compound' => true]);
|
||||||
|
|
||||||
foreach ($helper->getCheckboxes() as $name => $c) {
|
foreach ($helper->getCheckboxes() as $name => $c) {
|
||||||
$choices = array_combine(
|
$choices = self::buildCheckboxChoices($c['choices'], $c['trans']);
|
||||||
array_map(static function ($c, $t) {
|
|
||||||
if (null !== $t) {
|
|
||||||
return $t;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $c;
|
|
||||||
}, $c['choices'], $c['trans']),
|
|
||||||
$c['choices']
|
|
||||||
);
|
|
||||||
|
|
||||||
$checkboxesBuilder->add($name, ChoiceType::class, [
|
$checkboxesBuilder->add($name, ChoiceType::class, [
|
||||||
'choices' => $choices,
|
'choices' => $choices,
|
||||||
@ -71,6 +61,25 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
|
|||||||
$builder->add($checkboxesBuilder);
|
$builder->add($checkboxesBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ([] !== $helper->getEntityChoices()) {
|
||||||
|
$entityChoicesBuilder = $builder->create('entity_choices', null, ['compound' => true]);
|
||||||
|
|
||||||
|
foreach ($helper->getEntityChoices() as $key => [
|
||||||
|
'label' => $label, 'choices' => $choices, 'options' => $opts, 'class' => $class
|
||||||
|
]) {
|
||||||
|
$entityChoicesBuilder->add($key, EntityType::class, [
|
||||||
|
'label' => $label,
|
||||||
|
'choices' => $choices,
|
||||||
|
'class' => $class,
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
...$opts,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder->add($entityChoicesBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
if (0 < count($helper->getDateRanges())) {
|
if (0 < count($helper->getDateRanges())) {
|
||||||
$dateRangesBuilder = $builder->create('dateRanges', null, ['compound' => true]);
|
$dateRangesBuilder = $builder->create('dateRanges', null, ['compound' => true]);
|
||||||
|
|
||||||
@ -97,29 +106,51 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
|
|||||||
$builder->add($dateRangesBuilder);
|
$builder->add($dateRangesBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) {
|
if ([] !== $helper->getSingleCheckbox()) {
|
||||||
switch ($key) {
|
$singleCheckBoxBuilder = $builder->create('single_checkboxes', null, ['compound' => true]);
|
||||||
case 'q':
|
|
||||||
case 'checkboxes' . $key:
|
|
||||||
case $key . '_from':
|
|
||||||
case $key . '_to':
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'page':
|
foreach ($helper->getSingleCheckbox() as $name => ['label' => $label]) {
|
||||||
$builder->add($key, HiddenType::class, [
|
$singleCheckBoxBuilder->add($name, CheckboxType::class, ['label' => $label, 'required' => false]);
|
||||||
'data' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
$builder->add($key, HiddenType::class, [
|
|
||||||
'data' => $value,
|
|
||||||
]);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$builder->add($singleCheckBoxBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ([] !== $helper->getUserPickers()) {
|
||||||
|
$userPickersBuilder = $builder->create('user_pickers', null, ['compound' => true]);
|
||||||
|
|
||||||
|
foreach ($helper->getUserPickers() as $name => [
|
||||||
|
'label' => $label, 'options' => $opts
|
||||||
|
]) {
|
||||||
|
|
||||||
|
$userPickersBuilder->add(
|
||||||
|
$name,
|
||||||
|
PickUserDynamicType::class,
|
||||||
|
[
|
||||||
|
'multiple' => true,
|
||||||
|
'label' => $label,
|
||||||
|
...$opts,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder->add($userPickersBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildCheckboxChoices(array $choices, array $trans = []): array
|
||||||
|
{
|
||||||
|
return array_combine(
|
||||||
|
array_map(static function ($c, $t) {
|
||||||
|
if (null !== $t) {
|
||||||
|
return $t;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $c;
|
||||||
|
}, $choices, $trans),
|
||||||
|
$choices
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||||
|
@ -42,3 +42,7 @@ form {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: .375em;
|
margin-bottom: .375em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chill_filter_order {
|
||||||
|
background: $gray-100;
|
||||||
|
}
|
@ -66,6 +66,10 @@ export default {
|
|||||||
return appMessages.fr.the_activity;
|
return appMessages.fr.the_activity;
|
||||||
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
|
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
|
||||||
return appMessages.fr.the_course;
|
return appMessages.fr.the_course;
|
||||||
|
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork':
|
||||||
|
return appMessages.fr.the_action;
|
||||||
|
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument':
|
||||||
|
return appMessages.fr.the_evaluation_document;
|
||||||
case 'Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow':
|
case 'Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow':
|
||||||
return appMessages.fr.the_workflow;
|
return appMessages.fr.the_workflow;
|
||||||
default:
|
default:
|
||||||
@ -78,6 +82,10 @@ export default {
|
|||||||
return `/fr/activity/${n.relatedEntityId}/show`
|
return `/fr/activity/${n.relatedEntityId}/show`
|
||||||
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
|
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
|
||||||
return `/fr/parcours/${n.relatedEntityId}`
|
return `/fr/parcours/${n.relatedEntityId}`
|
||||||
|
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork':
|
||||||
|
return `/fr/person/accompanying-period/work/${n.relatedEntityId}/show`
|
||||||
|
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument':
|
||||||
|
return `/fr/person/accompanying-period/work/evaluation/document/${n.relatedEntityId}/show`
|
||||||
case 'Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow':
|
case 'Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow':
|
||||||
return `/fr/main/workflow/${n.relatedEntityId}/show`
|
return `/fr/main/workflow/${n.relatedEntityId}/show`
|
||||||
default:
|
default:
|
||||||
|
@ -46,6 +46,7 @@ const appMessages = {
|
|||||||
the_course: "le parcours",
|
the_course: "le parcours",
|
||||||
the_action: "l'action",
|
the_action: "l'action",
|
||||||
the_evaluation: "l'évaluation",
|
the_evaluation: "l'évaluation",
|
||||||
|
the_evaluation_document: "le document",
|
||||||
the_task: "la tâche",
|
the_task: "la tâche",
|
||||||
the_workflow: "le workflow",
|
the_workflow: "le workflow",
|
||||||
StartDate: "Date d'ouverture",
|
StartDate: "Date d'ouverture",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span v-if="data.working_ref_status === 'to_review'" class="badge bg-danger address-details-button-warning">L'adresse de référence a été modifiée</span>
|
<span v-if="data.working_ref_status === 'to_review'" class="badge bg-danger address-details-button-warning">L'adresse de référence a été modifiée</span>
|
||||||
<a v-if="data.loading === false" @click.prevent="clickOrOpen" class="btn btn-misc address-details-button">
|
<a v-if="data.loading === false" @click.prevent="clickOrOpen" class="btn btn-sm address-details-button" title="Plus de détails">
|
||||||
<span class="fa fa-map"></span> <!-- button -->
|
<span class="fa fa-map-o"></span>
|
||||||
</a>
|
</a>
|
||||||
<span v-if="data.loading" class="fa fa-spin fa-spinner "></span>
|
<span v-if="data.loading" class="fa fa-spin fa-spinner "></span>
|
||||||
<AddressModal :address="data.working_address" @update-address="onUpdateAddress" ref="address_modal"></AddressModal>
|
<AddressModal :address="data.working_address" @update-address="onUpdateAddress" ref="address_modal"></AddressModal>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<component :is="component" class="chill-entity entity-address my-3">
|
<component :is="component" class="chill-entity entity-address">
|
||||||
|
|
||||||
<component :is="component" class="address" :class="multiline">
|
<component :is="component" class="address" :class="multiline">
|
||||||
|
|
||||||
|
@ -1,65 +1,144 @@
|
|||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
<div class="chill_filter_order container my-4">
|
<div class="accordion my-3" id="filterOrderAccordion">
|
||||||
<div class="row">
|
<h2 class="accordion-header" id="filterOrderHeading">
|
||||||
{% if form.vars.has_search_box %}
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#filterOrderCollapse" aria-expanded="true" aria-controls="filterOrderCollapse">
|
||||||
<div class="col-md-12">
|
<strong><i class="fa fa-fw fa-filter"></i>Filtrer la liste</strong>
|
||||||
<div class="input-group mb-3">
|
</button>
|
||||||
{{ form_widget(form.q)}}
|
</h2>
|
||||||
<button type="submit" class="btn btn-chill-l-gray"><i class="fa fa-search"></i></button>
|
<div class="accordion-collapse collapse" id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion">
|
||||||
</div>
|
{% set btnSubmit = 0 %}
|
||||||
</div>
|
<div class="accordion-body chill_filter_order container-xxl p-5 py-2">
|
||||||
{% endif %}
|
<div class="row my-2">
|
||||||
</div>
|
{% if form.vars.has_search_box %}
|
||||||
{% if form.dateRanges is defined %}
|
<div class="col-sm-12">
|
||||||
{% if form.dateRanges|length > 0 %}
|
<div class="input-group">
|
||||||
{% for dateRangeName, _o in form.dateRanges %}
|
{{ form_widget(form.q) }}
|
||||||
<div class="row gx-2 justify-content-center">
|
<button type="submit" class="btn btn-misc"><i class="fa fa-search"></i></button>
|
||||||
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %}
|
|
||||||
<div class="col-md-5">
|
|
||||||
{{ form_label(form.dateRanges[dateRangeName])}}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span>
|
|
||||||
{{ form_widget(form.dateRanges[dateRangeName]['from']) }}
|
|
||||||
<span class="input-group-text">{{ 'chill_calendar.To'|trans }}</span>
|
|
||||||
{{ form_widget(form.dateRanges[dateRangeName]['to']) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-1">
|
|
||||||
<button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if form.dateRanges is defined %}
|
||||||
|
{% set btnSubmit = 1 %}
|
||||||
|
{% if form.dateRanges|length > 0 %}
|
||||||
|
{% for dateRangeName, _o in form.dateRanges %}
|
||||||
|
<div class="row my-2">
|
||||||
|
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %}
|
||||||
|
{{ form_label(form.dateRanges[dateRangeName])}}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-sm-4 col-form-label">{{ 'filter_order.By date'|trans }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="col-sm-8 pt-1">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span>
|
||||||
|
{{ form_widget(form.dateRanges[dateRangeName]['from']) }}
|
||||||
|
<span class="input-group-text">{{ 'chill_calendar.To'|trans }}</span>
|
||||||
|
{{ form_widget(form.dateRanges[dateRangeName]['to']) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if form.checkboxes is defined %}
|
{% if form.checkboxes is defined %}
|
||||||
{% if form.checkboxes|length > 0 %}
|
{% set btnSubmit = 1 %}
|
||||||
{% for checkbox_name, options in form.checkboxes %}
|
{% if form.checkboxes|length > 0 %}
|
||||||
<div class="row gx-0">
|
{% for checkbox_name, options in form.checkboxes %}
|
||||||
<div class="col-md-12">
|
<div class="row my-2">
|
||||||
{% for c in form['checkboxes'][checkbox_name].children %}
|
<div class="col-sm-4 col-form-label">{{ 'filter_order.By'|trans }}</div>
|
||||||
<div class="form-check form-check-inline">
|
<div class="col-sm-8 pt-2">
|
||||||
|
{% for c in form['checkboxes'][checkbox_name].children %}
|
||||||
{{ form_widget(c) }}
|
{{ form_widget(c) }}
|
||||||
{{ form_label(c) }}
|
{{ form_label(c) }}
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if loop.last %}
|
|
||||||
<div class="row gx-0">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<ul class="record_actions">
|
|
||||||
<li>
|
|
||||||
<button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if form.entity_choices is defined %}
|
||||||
|
{% set btnSubmit = 1 %}
|
||||||
|
{% if form.entity_choices |length > 0 %}
|
||||||
|
{% for checkbox_name, options in form.entity_choices %}
|
||||||
|
<div class="row my-2">
|
||||||
|
{% if form.entity_choices[checkbox_name].vars.label is not same as(false) %}
|
||||||
|
{{ form_label(form.entity_choices[checkbox_name])}}
|
||||||
|
{% endif %}
|
||||||
|
<div class="col-sm-8 pt-2">
|
||||||
|
{% for c in form['entity_choices'][checkbox_name].children %}
|
||||||
|
{{ form_widget(c) }}
|
||||||
|
{{ form_label(c) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if form.user_pickers is defined %}
|
||||||
|
{% set btnSubmit = 1 %}
|
||||||
|
{% if form.user_pickers.children|length > 0 %}
|
||||||
|
{% for name, options in form.user_pickers %}
|
||||||
|
<div class="row my-2">
|
||||||
|
{% if form.user_pickers[name].vars.label is not same as(false) %}
|
||||||
|
{{ form_label(form.user_pickers[name]) }}
|
||||||
|
{% else %}
|
||||||
|
{{ form_label(form.user_pickers[name].vars.label) }}
|
||||||
|
{% endif %}
|
||||||
|
<div class="col-sm-8 pt-2">
|
||||||
|
{{ form_widget(form.user_pickers[name]) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if form.single_checkboxes is defined %}
|
||||||
|
{% set btnSubmit = 1 %}
|
||||||
|
{% for name, _o in form.single_checkboxes %}
|
||||||
|
<div class="row my-2">
|
||||||
|
<div class="col-sm-4 col-form-label">{{ 'filter_order.By'|trans }}</div>
|
||||||
|
<div class="col-sm-8 pt-2">
|
||||||
|
{{ form_widget(form.single_checkboxes[name]) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
{% if btnSubmit == 1 %}
|
||||||
|
<div class="row my-2">
|
||||||
|
<button type="submit" class="btn btn-sm btn-misc"><i class="fa fa-fw fa-filter"></i>{{ 'Filter'|trans }}</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if active|length > 0 %}
|
||||||
|
<div class="activeFilters mt-3">
|
||||||
|
{% for f in active %}
|
||||||
|
<span class="badge rounded-pill bg-secondary ms-1 {{ f.position }} {{ f.name }}">
|
||||||
|
{%- if f.label != '' %}
|
||||||
|
<span class="text-dark">{{ f.label|trans }} : </span>
|
||||||
|
{% endif -%}
|
||||||
|
{%- if f.position == 'search_box' and f.value is not null %}
|
||||||
|
<span class="text-dark">{{ 'filter_order.search_box'|trans ~ ' :' }}</span>
|
||||||
|
{% endif -%}
|
||||||
|
{{ f.value}}{#
|
||||||
|
#}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for k,v in otherParameters %}
|
||||||
|
<input type="hidden" name="{{ k }}" value="{{ v }}" />
|
||||||
|
{% endfor %}
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Service\AddressGeographicalUnit;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
final readonly class CollateAddressWithReferenceOrPostalCode implements CollateAddressWithReferenceOrPostalCodeInterface
|
||||||
|
{
|
||||||
|
private const LOG_PREFIX = '[collate addresses] ';
|
||||||
|
/**
|
||||||
|
* For the address having an "invented" postal code, find the postal code "reference" with the same code,
|
||||||
|
* and the most similar name. When two reference code match, we add
|
||||||
|
*
|
||||||
|
* This query intentionally includes also address with reference, as the reference may be wrong.
|
||||||
|
*/
|
||||||
|
private const FORCE_ORIGINAL_POSTAL_CODE = <<<'SQL'
|
||||||
|
WITH recollate AS (
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT cma.id AS address_id, cmpc.id, cmpc.label, cmpc.code, cmpc_reference.id AS cmpc_reference_id, cmpc_reference.label, cmpc_reference.code,
|
||||||
|
RANK() OVER (PARTITION BY cma.id ORDER BY SIMILARITY(cmpc.label, cmpc_reference.label) DESC, cmpc_reference.id ASC) AS ranked
|
||||||
|
FROM
|
||||||
|
chill_main_address cma JOIN chill_main_postal_code cmpc on cma.postcode_id = cmpc.id,
|
||||||
|
chill_main_postal_code cmpc_reference
|
||||||
|
WHERE
|
||||||
|
-- use only postal code which are reference
|
||||||
|
cmpc_reference.id != cmpc.id AND cmpc_reference.origin = 0
|
||||||
|
-- only where cmpc is created manually
|
||||||
|
AND cmpc.origin != 0
|
||||||
|
-- only when postal code match
|
||||||
|
AND TRIM(REPLACE(LOWER(cmpc.code), ' ', '')) = LOWER(cmpc_reference.code)
|
||||||
|
AND cmpc.country_id = cmpc_reference.country_id
|
||||||
|
AND cma.id > :since_id -- to set the first id
|
||||||
|
) sq
|
||||||
|
WHERE ranked = 1)
|
||||||
|
UPDATE chill_main_address SET postcode_id = cmpc_reference_id FROM recollate WHERE recollate.address_id = chill_main_address.id;
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* associate the address with the most similar address reference.
|
||||||
|
*
|
||||||
|
* This query intentionally ignores the existing addressreference_id, to let fixing the address match the
|
||||||
|
* most similar address reference.
|
||||||
|
*/
|
||||||
|
private const FORCE_MOST_SIMILAR_ADDRESS_REFERENCE = <<<'SQL'
|
||||||
|
WITH recollate AS (
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT cma.id AS address_id, cma.streetnumber, cma.street, cmpc.code, cmpc.label, cmar.id AS address_reference_id, cmar.streetnumber, cmar.street, cmpc_reference.code, cmpc_reference.label,
|
||||||
|
similarity(cma.street, cmar.street),
|
||||||
|
RANK() OVER (PARTITION BY cma.id ORDER BY SIMILARITY (cma.street, cmar.street) DESC, SIMILARITY (cma.streetnumber, cmar.streetnumber), cmar.id ASC) AS ranked
|
||||||
|
FROM
|
||||||
|
chill_main_address cma
|
||||||
|
JOIN chill_main_postal_code cmpc on cma.postcode_id = cmpc.id,
|
||||||
|
chill_main_address_reference cmar JOIN chill_main_postal_code cmpc_reference ON cmar.postcode_id = cmpc_reference.id
|
||||||
|
WHERE
|
||||||
|
-- only if cmpc is a reference (must be matched before executing this query)
|
||||||
|
cma.postcode_id = cmar.postcode_id
|
||||||
|
-- join cmpc to cma
|
||||||
|
AND SIMILARITY(LOWER(cma.street), LOWER(cmar.street)) > 0.6 AND LOWER(cma.streetnumber) = LOWER(cmar.streetnumber)
|
||||||
|
-- only addresses which match the address reference - let the user decide if the reference has changed
|
||||||
|
AND cma.refstatus = 'match'
|
||||||
|
-- only the most recent
|
||||||
|
AND cma.id > :since_id
|
||||||
|
) AS sq
|
||||||
|
WHERE ranked = 1
|
||||||
|
)
|
||||||
|
UPDATE chill_main_address SET addressreference_id = recollate.address_reference_id FROM recollate WHERE chill_main_address.id = recollate.address_id;
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the point's address with the:
|
||||||
|
*
|
||||||
|
* - address reference point, if the address match the reference with sufficient similarity
|
||||||
|
* - or the postcal code center
|
||||||
|
*/
|
||||||
|
private const UPDATE_POINT = <<<'SQL'
|
||||||
|
WITH address_geom AS (
|
||||||
|
SELECT cma.id AS address_id, COALESCE(cmar.point, cmpc.center) AS point
|
||||||
|
FROM chill_main_address cma
|
||||||
|
LEFT JOIN chill_main_address_reference cmar ON cma.addressreference_id = cmar.id AND similarity(cma.street, cmar.street) > 0.6 AND LOWER(cma.streetnumber) = LOWER(cmar.streetnumber)
|
||||||
|
LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id
|
||||||
|
WHERE cma.id > :since_id
|
||||||
|
)
|
||||||
|
UPDATE chill_main_address SET point = address_geom.point FROM address_geom WHERE address_geom.address_id = chill_main_address.id
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
private const MAX_ADDRESS_ID = <<<'SQL'
|
||||||
|
SELECT MAX(id) AS max_id FROM chill_main_address;
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private Connection $connection,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function __invoke(int $sinceId = 0): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
[
|
||||||
|
$postCodeSetReferenceFromMostSimilar,
|
||||||
|
$addressReferenceMatch,
|
||||||
|
$pointUpdates,
|
||||||
|
$lastId,
|
||||||
|
] = $this->connection->transactional(function () use ($sinceId) {
|
||||||
|
$postCodeSetReferenceFromMostSimilar = $this->connection->executeStatement(self::FORCE_ORIGINAL_POSTAL_CODE, ['since_id' => $sinceId]);
|
||||||
|
$addressReferenceMatch = $this->connection->executeStatement(self::FORCE_MOST_SIMILAR_ADDRESS_REFERENCE, ['since_id' => $sinceId]);
|
||||||
|
$pointUpdates = $this->connection->executeStatement(self::UPDATE_POINT, ['since_id' => $sinceId]);
|
||||||
|
$lastId = $this->connection->fetchOne(self::MAX_ADDRESS_ID);
|
||||||
|
|
||||||
|
return [
|
||||||
|
$postCodeSetReferenceFromMostSimilar,
|
||||||
|
$addressReferenceMatch,
|
||||||
|
$pointUpdates,
|
||||||
|
$lastId,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logger->error(self::LOG_PREFIX . "error while re-collating addresses", [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info(self::LOG_PREFIX . "Collate the addresses with reference", [
|
||||||
|
'set_postcode_from_most_similar' => $postCodeSetReferenceFromMostSimilar,
|
||||||
|
'address_reference_match' => $addressReferenceMatch,
|
||||||
|
'point_update' => $pointUpdates,
|
||||||
|
'since_id' => $sinceId,
|
||||||
|
'last_id' => $lastId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $lastId;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Service\AddressGeographicalUnit;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Cron\CronJobInterface;
|
||||||
|
use Chill\MainBundle\Entity\CronJobExecution;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
|
|
||||||
|
final readonly class CollateAddressWithReferenceOrPostalCodeCronJob implements CronJobInterface
|
||||||
|
{
|
||||||
|
private const LAST_MAX_ID = 'last-max-id';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ClockInterface $clock,
|
||||||
|
private CollateAddressWithReferenceOrPostalCodeInterface $collateAddressWithReferenceOrPostalCode,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||||
|
{
|
||||||
|
if (null === $cronJobExecution) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = $this->clock->now();
|
||||||
|
|
||||||
|
return $now->sub(new \DateInterval('PT6H')) > $cronJobExecution->getLastStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKey(): string
|
||||||
|
{
|
||||||
|
return 'collate-address';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(array $lastExecutionData): null|array
|
||||||
|
{
|
||||||
|
$maxId = ($this->collateAddressWithReferenceOrPostalCode)($lastExecutionData[self::LAST_MAX_ID] ?? 0);
|
||||||
|
|
||||||
|
return [self::LAST_MAX_ID => $maxId];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Service\AddressGeographicalUnit;
|
||||||
|
|
||||||
|
interface CollateAddressWithReferenceOrPostalCodeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function __invoke(int $sinceId = 0): int;
|
||||||
|
}
|
@ -49,8 +49,10 @@ class RefreshAddressToGeographicalUnitMaterializedViewCronJob implements CronJob
|
|||||||
return 'refresh-materialized-view-address-to-geog-units';
|
return 'refresh-materialized-view-address-to-geog-units';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(): void
|
public function run(array $lastExecutionData): null|array
|
||||||
{
|
{
|
||||||
$this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit');
|
$this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit');
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Templating\Listing;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||||
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||||
|
use Symfony\Component\PropertyAccess\PropertyPathInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final readonly class FilterOrderGetActiveFilterHelper
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private TranslatorInterface $translator,
|
||||||
|
private PropertyAccessorInterface $propertyAccessor,
|
||||||
|
private UserRender $userRender,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the data required to display the active filters
|
||||||
|
*
|
||||||
|
* @param FilterOrderHelper $filterOrderHelper
|
||||||
|
* @return array<array{label: string, value: string, position: string, name: string}>
|
||||||
|
*/
|
||||||
|
public function getActiveFilters(FilterOrderHelper $filterOrderHelper): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
if ($filterOrderHelper->hasSearchBox() && '' !== $filterOrderHelper->getQueryString()) {
|
||||||
|
$result[] = ['label' => '', 'value' => $filterOrderHelper->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($filterOrderHelper->getDateRanges() as $name => ['label' => $label]) {
|
||||||
|
$base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string)$label];
|
||||||
|
|
||||||
|
if (null !== ($from = $filterOrderHelper->getDateRangeData($name)['from'] ?? null)) {
|
||||||
|
$result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base];
|
||||||
|
}
|
||||||
|
if (null !== ($to = $filterOrderHelper->getDateRangeData($name)['to'] ?? null)) {
|
||||||
|
$result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($filterOrderHelper->getCheckboxes() as $name => ['choices' => $choices, 'trans' => $trans]) {
|
||||||
|
$translatedChoice = array_combine($choices, [...$trans]);
|
||||||
|
foreach ($filterOrderHelper->getCheckboxData($name) as $keyChoice) {
|
||||||
|
$result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($filterOrderHelper->getEntityChoices() as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) {
|
||||||
|
foreach ($filterOrderHelper->getEntityChoiceData($name) as $selected) {
|
||||||
|
if (is_callable($options['choice_label'])) {
|
||||||
|
$value = call_user_func($options['choice_label'], $selected);
|
||||||
|
} elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) {
|
||||||
|
$value = $this->propertyAccessor->getValue($selected, $options['choice_label']);
|
||||||
|
} else {
|
||||||
|
if (!$selected instanceof \Stringable) {
|
||||||
|
throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = (string)$selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($filterOrderHelper->getUserPickers() as $name => ['label' => $label, 'options' => $options]) {
|
||||||
|
foreach ($filterOrderHelper->getUserPickerData($name) as $user) {
|
||||||
|
$result[] = ['value' => $this->userRender->renderString($user, []), 'label' => (string) $label, 'position' => FilterOrderPositionEnum::UserPicker->value, 'name' => $name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($filterOrderHelper->getSingleCheckbox() as $name => ['label' => $label]) {
|
||||||
|
if (true === $filterOrderHelper->getSingleCheckboxData($name)) {
|
||||||
|
$result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Templating\Listing;
|
namespace Chill\MainBundle\Templating\Listing;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Form\Type\Listing\FilterOrderType;
|
use Chill\MainBundle\Form\Type\Listing\FilterOrderType;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Symfony\Component\Form\FormFactoryInterface;
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
@ -18,46 +19,85 @@ use Symfony\Component\Form\FormInterface;
|
|||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
use function array_merge;
|
use function array_merge;
|
||||||
use function count;
|
|
||||||
|
|
||||||
class FilterOrderHelper
|
final class FilterOrderHelper
|
||||||
{
|
{
|
||||||
private array $checkboxes = [];
|
private array $checkboxes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label: string}>
|
||||||
|
*/
|
||||||
|
private array $singleCheckbox = [];
|
||||||
|
|
||||||
private array $dateRanges = [];
|
private array $dateRanges = [];
|
||||||
|
|
||||||
private FormFactoryInterface $formFactory;
|
public const FORM_NAME = 'f';
|
||||||
|
|
||||||
private ?string $formName = 'f';
|
|
||||||
|
|
||||||
private array $formOptions = [];
|
private array $formOptions = [];
|
||||||
|
|
||||||
private string $formType = FilterOrderType::class;
|
private string $formType = FilterOrderType::class;
|
||||||
|
|
||||||
private RequestStack $requestStack;
|
|
||||||
|
|
||||||
private ?array $searchBoxFields = null;
|
private ?array $searchBoxFields = null;
|
||||||
|
|
||||||
private ?array $submitted = null;
|
private ?array $submitted = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label: string, choices: array, options: array}>
|
||||||
|
*/
|
||||||
|
private array $entityChoices = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label: string, options: array}>
|
||||||
|
*/
|
||||||
|
private array $userPickers = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
FormFactoryInterface $formFactory,
|
private readonly FormFactoryInterface $formFactory,
|
||||||
RequestStack $requestStack
|
private readonly RequestStack $requestStack,
|
||||||
) {
|
) {
|
||||||
$this->formFactory = $formFactory;
|
|
||||||
$this->requestStack = $requestStack;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self
|
public function addSingleCheckbox(string $name, string $label): self
|
||||||
{
|
{
|
||||||
$missing = count($choices) - count($trans) - 1;
|
$this->singleCheckbox[$name] = ['label' => $label];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string $class
|
||||||
|
*/
|
||||||
|
public function addEntityChoice(string $name, string $class, string $label, array $choices, array $options = []): self
|
||||||
|
{
|
||||||
|
$this->entityChoices[$name] = ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityChoices(): array
|
||||||
|
{
|
||||||
|
return $this->entityChoices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addUserPicker(string $name, ?string $label = null, array $options = []): self
|
||||||
|
{
|
||||||
|
$this->userPickers[$name] = ['label' => $label, 'options' => $options];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self
|
||||||
|
{
|
||||||
|
if ([] === $trans) {
|
||||||
|
$trans = $choices;
|
||||||
|
}
|
||||||
|
|
||||||
$this->checkboxes[$name] = [
|
$this->checkboxes[$name] = [
|
||||||
'choices' => $choices, 'default' => $default,
|
'choices' => $choices,
|
||||||
'trans' => array_merge(
|
'default' => $default,
|
||||||
$trans,
|
'trans' => $trans,
|
||||||
0 < $missing ?
|
...$options,
|
||||||
array_fill(0, $missing, null) : []
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@ -73,7 +113,7 @@ class FilterOrderHelper
|
|||||||
public function buildForm(): FormInterface
|
public function buildForm(): FormInterface
|
||||||
{
|
{
|
||||||
return $this->formFactory
|
return $this->formFactory
|
||||||
->createNamed($this->formName, $this->formType, $this->getDefaultData(), array_merge([
|
->createNamed(self::FORM_NAME, $this->formType, $this->getDefaultData(), array_merge([
|
||||||
'helper' => $this,
|
'helper' => $this,
|
||||||
'method' => 'GET',
|
'method' => 'GET',
|
||||||
'csrf_protection' => false,
|
'csrf_protection' => false,
|
||||||
@ -81,11 +121,49 @@ class FilterOrderHelper
|
|||||||
->handleRequest($this->requestStack->getCurrentRequest());
|
->handleRequest($this->requestStack->getCurrentRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUserPickers(): array
|
||||||
|
{
|
||||||
|
return $this->userPickers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<User>
|
||||||
|
*/
|
||||||
|
public function getUserPickerData(string $name): array
|
||||||
|
{
|
||||||
|
return $this->getFormData()['user_pickers'][$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasCheckboxData(string $name): bool
|
||||||
|
{
|
||||||
|
return array_key_exists($name, $this->checkboxes);
|
||||||
|
}
|
||||||
|
|
||||||
public function getCheckboxData(string $name): array
|
public function getCheckboxData(string $name): array
|
||||||
{
|
{
|
||||||
return $this->getFormData()['checkboxes'][$name];
|
return $this->getFormData()['checkboxes'][$name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasSingleCheckboxData(string $name): bool
|
||||||
|
{
|
||||||
|
return array_key_exists($name, $this->singleCheckbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSingleCheckboxData(string $name): ?bool
|
||||||
|
{
|
||||||
|
return $this->getFormData()['single_checkboxes'][$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasEntityChoice(string $name): bool
|
||||||
|
{
|
||||||
|
return array_key_exists($name, $this->entityChoices);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityChoiceData($name): mixed
|
||||||
|
{
|
||||||
|
return $this->getFormData()['entity_choices'][$name];
|
||||||
|
}
|
||||||
|
|
||||||
public function getCheckboxes(): array
|
public function getCheckboxes(): array
|
||||||
{
|
{
|
||||||
return $this->checkboxes;
|
return $this->checkboxes;
|
||||||
@ -97,7 +175,20 @@ class FilterOrderHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<'to': DateTimeImmutable, 'from': DateTimeImmutable>
|
* @return array<string, array{label: string}>
|
||||||
|
*/
|
||||||
|
public function getSingleCheckbox(): array
|
||||||
|
{
|
||||||
|
return $this->singleCheckbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasDateRangeData(string $name): bool
|
||||||
|
{
|
||||||
|
return array_key_exists($name, $this->dateRanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{to: ?DateTimeImmutable, from: ?DateTimeImmutable}
|
||||||
*/
|
*/
|
||||||
public function getDateRangeData(string $name): array
|
public function getDateRangeData(string $name): array
|
||||||
{
|
{
|
||||||
@ -128,7 +219,13 @@ class FilterOrderHelper
|
|||||||
|
|
||||||
private function getDefaultData(): array
|
private function getDefaultData(): array
|
||||||
{
|
{
|
||||||
$r = [];
|
$r = [
|
||||||
|
'checkboxes' => [],
|
||||||
|
'dateRanges' => [],
|
||||||
|
'single_checkboxes' => [],
|
||||||
|
'entity_choices' => [],
|
||||||
|
'user_pickers' => []
|
||||||
|
];
|
||||||
|
|
||||||
if ($this->hasSearchBox()) {
|
if ($this->hasSearchBox()) {
|
||||||
$r['q'] = '';
|
$r['q'] = '';
|
||||||
@ -143,6 +240,18 @@ class FilterOrderHelper
|
|||||||
$r['dateRanges'][$name]['to'] = $defaults['to'];
|
$r['dateRanges'][$name]['to'] = $defaults['to'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($this->singleCheckbox as $name => $c) {
|
||||||
|
$r['single_checkboxes'][$name] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->entityChoices as $name => $c) {
|
||||||
|
$r['entity_choices'][$name] = ($c['options']['multiple'] ?? true) ? [] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->userPickers as $name => $u) {
|
||||||
|
$r['user_pickers'][$name] = ($u['options']['multiple'] ?? true) ? [] : null;
|
||||||
|
}
|
||||||
|
|
||||||
return $r;
|
return $r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ namespace Chill\MainBundle\Templating\Listing;
|
|||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Symfony\Component\Form\FormFactoryInterface;
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
class FilterOrderHelperBuilder
|
class FilterOrderHelperBuilder
|
||||||
{
|
{
|
||||||
@ -27,14 +29,36 @@ class FilterOrderHelperBuilder
|
|||||||
|
|
||||||
private ?array $searchBoxFields = null;
|
private ?array $searchBoxFields = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label: string}>
|
||||||
|
*/
|
||||||
|
private array $singleCheckboxes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label: string, class: class-string, choices: array, options: array}>
|
||||||
|
*/
|
||||||
|
private array $entityChoices = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array{label: string, options: array}>
|
||||||
|
*/
|
||||||
|
private array $userPickers = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
FormFactoryInterface $formFactory,
|
FormFactoryInterface $formFactory,
|
||||||
RequestStack $requestStack
|
RequestStack $requestStack,
|
||||||
) {
|
) {
|
||||||
$this->formFactory = $formFactory;
|
$this->formFactory = $formFactory;
|
||||||
$this->requestStack = $requestStack;
|
$this->requestStack = $requestStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addSingleCheckbox(string $name, string $label): self
|
||||||
|
{
|
||||||
|
$this->singleCheckboxes[$name] = ['label' => $label];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self
|
public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self
|
||||||
{
|
{
|
||||||
$this->checkboxes[$name] = ['choices' => $choices, 'default' => $default, 'trans' => $trans];
|
$this->checkboxes[$name] = ['choices' => $choices, 'default' => $default, 'trans' => $trans];
|
||||||
@ -42,6 +66,16 @@ class FilterOrderHelperBuilder
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string $class
|
||||||
|
*/
|
||||||
|
public function addEntityChoice(string $name, string $label, string $class, array $choices, ?array $options = []): self
|
||||||
|
{
|
||||||
|
$this->entityChoices[$name] = ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function addDateRange(string $name, ?string $label = null, ?DateTimeImmutable $from = null, ?DateTimeImmutable $to = null): self
|
public function addDateRange(string $name, ?string $label = null, ?DateTimeImmutable $from = null, ?DateTimeImmutable $to = null): self
|
||||||
{
|
{
|
||||||
$this->dateRanges[$name] = ['from' => $from, 'to' => $to, 'label' => $label];
|
$this->dateRanges[$name] = ['from' => $from, 'to' => $to, 'label' => $label];
|
||||||
@ -56,11 +90,18 @@ class FilterOrderHelperBuilder
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addUserPicker(string $name, ?string $label = null, ?array $options = []): self
|
||||||
|
{
|
||||||
|
$this->userPickers[$name] = ['label' => $label, 'options' => $options];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function build(): FilterOrderHelper
|
public function build(): FilterOrderHelper
|
||||||
{
|
{
|
||||||
$helper = new FilterOrderHelper(
|
$helper = new FilterOrderHelper(
|
||||||
$this->formFactory,
|
$this->formFactory,
|
||||||
$this->requestStack
|
$this->requestStack,
|
||||||
);
|
);
|
||||||
|
|
||||||
$helper->setSearchBox($this->searchBoxFields);
|
$helper->setSearchBox($this->searchBoxFields);
|
||||||
@ -75,6 +116,18 @@ class FilterOrderHelperBuilder
|
|||||||
$helper->addCheckbox($name, $choices, $default, $trans);
|
$helper->addCheckbox($name, $choices, $default, $trans);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
$this->singleCheckboxes as $name => ['label' => $label]
|
||||||
|
) {
|
||||||
|
$helper->addSingleCheckbox($name, $label);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
$this->entityChoices as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]
|
||||||
|
) {
|
||||||
|
$helper->addEntityChoice($name, $class, $label, $choices, $options);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (
|
foreach (
|
||||||
$this->dateRanges as $name => [
|
$this->dateRanges as $name => [
|
||||||
'from' => $from,
|
'from' => $from,
|
||||||
@ -85,6 +138,17 @@ class FilterOrderHelperBuilder
|
|||||||
$helper->addDateRange($name, $label, $from, $to);
|
$helper->addDateRange($name, $label, $from, $to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
$this->userPickers as $name => [
|
||||||
|
'label' => $label,
|
||||||
|
'options' => $options
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
$helper->addUserPicker($name, $label, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return $helper;
|
return $helper;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ namespace Chill\MainBundle\Templating\Listing;
|
|||||||
|
|
||||||
use Symfony\Component\Form\FormFactoryInterface;
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface
|
class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface
|
||||||
{
|
{
|
||||||
@ -22,7 +24,7 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
FormFactoryInterface $formFactory,
|
FormFactoryInterface $formFactory,
|
||||||
RequestStack $requestStack
|
RequestStack $requestStack,
|
||||||
) {
|
) {
|
||||||
$this->formFactory = $formFactory;
|
$this->formFactory = $formFactory;
|
||||||
$this->requestStack = $requestStack;
|
$this->requestStack = $requestStack;
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Templating\Listing;
|
||||||
|
|
||||||
|
enum FilterOrderPositionEnum: string
|
||||||
|
{
|
||||||
|
case SearchBox = 'search_box';
|
||||||
|
case Checkboxes = 'checkboxes';
|
||||||
|
case DateRange = 'date_range';
|
||||||
|
case EntityChoice = 'entity_choice';
|
||||||
|
case SingleCheckbox = 'single_checkbox';
|
||||||
|
case UserPicker = 'user_picker';
|
||||||
|
}
|
@ -11,13 +11,24 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Templating\Listing;
|
namespace Chill\MainBundle\Templating\Listing;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
use Twig\Error\LoaderError;
|
||||||
|
use Twig\Error\RuntimeError;
|
||||||
|
use Twig\Error\SyntaxError;
|
||||||
use Twig\Extension\AbstractExtension;
|
use Twig\Extension\AbstractExtension;
|
||||||
use Twig\TwigFilter;
|
use Twig\TwigFilter;
|
||||||
|
|
||||||
class Templating extends AbstractExtension
|
class Templating extends AbstractExtension
|
||||||
{
|
{
|
||||||
public function getFilters()
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly FilterOrderGetActiveFilterHelper $filterOrderGetActiveFilterHelper,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilters(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new TwigFilter('chill_render_filter_order_helper', [$this, 'renderFilterOrderHelper'], [
|
new TwigFilter('chill_render_filter_order_helper', [$this, 'renderFilterOrderHelper'], [
|
||||||
@ -26,16 +37,42 @@ class Templating extends AbstractExtension
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SyntaxError
|
||||||
|
* @throws RuntimeError
|
||||||
|
* @throws LoaderError
|
||||||
|
*/
|
||||||
public function renderFilterOrderHelper(
|
public function renderFilterOrderHelper(
|
||||||
Environment $environment,
|
Environment $environment,
|
||||||
FilterOrderHelper $helper,
|
FilterOrderHelper $helper,
|
||||||
?string $template = '@ChillMain/FilterOrder/base.html.twig',
|
?string $template = '@ChillMain/FilterOrder/base.html.twig',
|
||||||
?array $options = []
|
?array $options = []
|
||||||
) {
|
): string {
|
||||||
|
$otherParameters = [];
|
||||||
|
|
||||||
|
foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) {
|
||||||
|
switch ($key) {
|
||||||
|
case FilterOrderHelper::FORM_NAME:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PaginatorFactory::DEFAULT_CURRENT_PAGE_KEY:
|
||||||
|
// when filtering, go back to page 1
|
||||||
|
$otherParameters[PaginatorFactory::DEFAULT_CURRENT_PAGE_KEY] = 1;
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$otherParameters[$key] = $value;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $environment->render($template, [
|
return $environment->render($template, [
|
||||||
'helper' => $helper,
|
'helper' => $helper,
|
||||||
|
'active' => $this->filterOrderGetActiveFilterHelper->getActiveFilters($helper),
|
||||||
'form' => $helper->buildForm()->createView(),
|
'form' => $helper->buildForm()->createView(),
|
||||||
'options' => $options,
|
'options' => $options,
|
||||||
|
'otherParameters' => $otherParameters,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Cron;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Cron\CronJobInterface;
|
||||||
|
use Chill\MainBundle\Cron\CronManager;
|
||||||
|
use Chill\MainBundle\Entity\CronJobExecution;
|
||||||
|
use Chill\MainBundle\Repository\CronJobExecutionRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class CronJobDatabaseInteractionTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private CronJobExecutionRepository $cronJobExecutionRepository;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$this->entityManager = self::$container->get(EntityManagerInterface::class);
|
||||||
|
$this->cronJobExecutionRepository = self::$container->get(CronJobExecutionRepository::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCompleteLifeCycle(): void
|
||||||
|
{
|
||||||
|
$cronjob = $this->prophesize(CronJobInterface::class);
|
||||||
|
$cronjob->canRun(null)->willReturn(true);
|
||||||
|
$cronjob->canRun(Argument::type(CronJobExecution::class))->willReturn(true);
|
||||||
|
$cronjob->getKey()->willReturn('test-with-data');
|
||||||
|
$cronjob->run([])->willReturn(['test' => 'execution-0']);
|
||||||
|
$cronjob->run(['test' => 'execution-0'])->willReturn(['test' => 'execution-1']);
|
||||||
|
|
||||||
|
$cronjob->run([])->shouldBeCalledOnce();
|
||||||
|
$cronjob->run(['test' => 'execution-0'])->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$manager = new CronManager(
|
||||||
|
$this->cronJobExecutionRepository,
|
||||||
|
$this->entityManager,
|
||||||
|
[$cronjob->reveal()],
|
||||||
|
new NullLogger()
|
||||||
|
);
|
||||||
|
|
||||||
|
// run a first time
|
||||||
|
$manager->run();
|
||||||
|
|
||||||
|
// run a second time
|
||||||
|
$manager->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class JobWithReturn implements CronJobInterface
|
||||||
|
{
|
||||||
|
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKey(): string
|
||||||
|
{
|
||||||
|
return 'with-data';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(array $lastExecutionData): null|array
|
||||||
|
{
|
||||||
|
return ['data' => 'test'];
|
||||||
|
}
|
||||||
|
}
|
@ -40,7 +40,7 @@ final class CronManagerTest extends TestCase
|
|||||||
$jobToExecute = $this->prophesize(CronJobInterface::class);
|
$jobToExecute = $this->prophesize(CronJobInterface::class);
|
||||||
$jobToExecute->getKey()->willReturn('to-exec');
|
$jobToExecute->getKey()->willReturn('to-exec');
|
||||||
$jobToExecute->canRun(Argument::type(CronJobExecution::class))->willReturn(true);
|
$jobToExecute->canRun(Argument::type(CronJobExecution::class))->willReturn(true);
|
||||||
$jobToExecute->run()->shouldBeCalled();
|
$jobToExecute->run([])->shouldBeCalled();
|
||||||
|
|
||||||
$executions = [
|
$executions = [
|
||||||
['key' => $jobOld1->getKey(), 'lastStart' => new DateTimeImmutable('yesterday'), 'lastEnd' => new DateTimeImmutable('1 hours ago'), 'lastStatus' => CronJobExecution::SUCCESS],
|
['key' => $jobOld1->getKey(), 'lastStart' => new DateTimeImmutable('yesterday'), 'lastEnd' => new DateTimeImmutable('1 hours ago'), 'lastStatus' => CronJobExecution::SUCCESS],
|
||||||
@ -64,7 +64,7 @@ final class CronManagerTest extends TestCase
|
|||||||
$jobAlreadyExecuted = new JobCanRun('k');
|
$jobAlreadyExecuted = new JobCanRun('k');
|
||||||
$jobNeverExecuted = $this->prophesize(CronJobInterface::class);
|
$jobNeverExecuted = $this->prophesize(CronJobInterface::class);
|
||||||
$jobNeverExecuted->getKey()->willReturn('never-executed');
|
$jobNeverExecuted->getKey()->willReturn('never-executed');
|
||||||
$jobNeverExecuted->run()->shouldBeCalled();
|
$jobNeverExecuted->run([])->shouldBeCalled();
|
||||||
$jobNeverExecuted->canRun(null)->willReturn(true);
|
$jobNeverExecuted->canRun(null)->willReturn(true);
|
||||||
|
|
||||||
$executions = [
|
$executions = [
|
||||||
@ -86,7 +86,7 @@ final class CronManagerTest extends TestCase
|
|||||||
$jobAlreadyExecuted = new JobCanRun('k');
|
$jobAlreadyExecuted = new JobCanRun('k');
|
||||||
$jobNeverExecuted = $this->prophesize(CronJobInterface::class);
|
$jobNeverExecuted = $this->prophesize(CronJobInterface::class);
|
||||||
$jobNeverExecuted->getKey()->willReturn('never-executed');
|
$jobNeverExecuted->getKey()->willReturn('never-executed');
|
||||||
$jobNeverExecuted->run()->shouldBeCalled();
|
$jobNeverExecuted->run([])->shouldBeCalled();
|
||||||
$jobNeverExecuted->canRun(null)->willReturn(true);
|
$jobNeverExecuted->canRun(null)->willReturn(true);
|
||||||
|
|
||||||
$executions = [
|
$executions = [
|
||||||
@ -178,8 +178,9 @@ class JobCanRun implements CronJobInterface
|
|||||||
return $this->key;
|
return $this->key;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(): void
|
public function run(array $lastExecutionData): null|array
|
||||||
{
|
{
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +196,8 @@ class JobCannotRun implements CronJobInterface
|
|||||||
return 'job-b';
|
return 'job-b';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(): void
|
public function run(array $lastExecutionData): null|array
|
||||||
{
|
{
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Services\AddressGeographicalUnit;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\CronJobExecution;
|
||||||
|
use Chill\MainBundle\Service\AddressGeographicalUnit\CollateAddressWithReferenceOrPostalCodeCronJob;
|
||||||
|
use Chill\MainBundle\Service\AddressGeographicalUnit\CollateAddressWithReferenceOrPostalCodeInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\Clock\MockClock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class CollateAddressWithReferenceOrPostalCodeCronJobTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataCanRun
|
||||||
|
*/
|
||||||
|
public function testCanRun(\DateTimeImmutable $now, ?\DateTimeImmutable $lastExecution, bool $expected): void
|
||||||
|
{
|
||||||
|
$execution = match ($lastExecution) {
|
||||||
|
null => null,
|
||||||
|
default => (new CronJobExecution('collate-address'))->setLastStart($lastExecution),
|
||||||
|
};
|
||||||
|
|
||||||
|
$clock = new MockClock($now);
|
||||||
|
$collator = $this->prophesize(CollateAddressWithReferenceOrPostalCodeInterface::class);
|
||||||
|
|
||||||
|
$job = new CollateAddressWithReferenceOrPostalCodeCronJob($clock, $collator->reveal());
|
||||||
|
|
||||||
|
self::assertEquals($expected, $job->canRun($execution));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRun(): void
|
||||||
|
{
|
||||||
|
$clock = new MockClock();
|
||||||
|
$collator = $this->prophesize(CollateAddressWithReferenceOrPostalCodeInterface::class);
|
||||||
|
$collator->__invoke(0)->shouldBeCalledOnce();
|
||||||
|
$collator->__invoke(0)->willReturn(1);
|
||||||
|
|
||||||
|
$job = new CollateAddressWithReferenceOrPostalCodeCronJob($clock, $collator->reveal());
|
||||||
|
|
||||||
|
$actual = $job->run(['last-max-id' => 0]);
|
||||||
|
self::assertEquals(['last-max-id' => 1], $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideDataCanRun(): iterable
|
||||||
|
{
|
||||||
|
yield [new \DateTimeImmutable('2023-07-10T12:00:00'), new \DateTimeImmutable('2023-07-10T11:00:00'), false];
|
||||||
|
yield [new \DateTimeImmutable('2023-07-10T12:00:00'), new \DateTimeImmutable('2023-07-10T05:00:00'), true];
|
||||||
|
yield [new \DateTimeImmutable('2023-07-10T12:00:00'), new \DateTimeImmutable('2023-07-01T12:00:00'), true];
|
||||||
|
yield [new \DateTimeImmutable('2023-07-10T12:00:00'), null, true];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Services\AddressGeographicalUnit;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Service\AddressGeographicalUnit\CollateAddressWithReferenceOrPostalCode;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class CollateAddressWithReferenceOrPostalCodeTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
private Connection $connection;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$this->connection = self::$container->get(Connection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRun(): void
|
||||||
|
{
|
||||||
|
$collator = new CollateAddressWithReferenceOrPostalCode(
|
||||||
|
$this->connection,
|
||||||
|
new NullLogger()
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $collator(0);
|
||||||
|
|
||||||
|
self::assertGreaterThan(0, $result);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20230711152947 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add data to ';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_cronjob_execution ADD lastExecutionData JSONB DEFAULT \'{}\'::jsonb NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_cronjob_execution DROP COLUMN lastExecutionData');
|
||||||
|
}
|
||||||
|
}
|
@ -54,3 +54,12 @@ duration:
|
|||||||
few {# minutes}
|
few {# minutes}
|
||||||
other {# minutes}
|
other {# minutes}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filter_order:
|
||||||
|
by_date:
|
||||||
|
From: Depuis le {from_date, date, long}
|
||||||
|
To: Jusqu'au {to_date, date, long}
|
||||||
|
By: Filtrer par
|
||||||
|
Search: Chercher dans la liste
|
||||||
|
By date: Filtrer par date
|
||||||
|
search_box: Filtrer par contenu
|
||||||
|
@ -39,8 +39,10 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface
|
|||||||
return 'accompanying-period-step-change';
|
return 'accompanying-period-step-change';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(): void
|
public function run(array $lastExecutionData): null|array
|
||||||
{
|
{
|
||||||
($this->requestor)();
|
($this->requestor)();
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,45 +11,39 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\PersonBundle\Controller;
|
namespace Chill\PersonBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
|
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
|
||||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\Form\Form;
|
use Symfony\Component\Form\Form;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
class AccompanyingCourseWorkController extends AbstractController
|
final class AccompanyingCourseWorkController extends AbstractController
|
||||||
{
|
{
|
||||||
private LoggerInterface $chillLogger;
|
|
||||||
|
|
||||||
private PaginatorFactory $paginator;
|
|
||||||
|
|
||||||
private SerializerInterface $serializer;
|
|
||||||
|
|
||||||
private TranslatorInterface $trans;
|
|
||||||
|
|
||||||
private AccompanyingPeriodWorkRepository $workRepository;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
TranslatorInterface $trans,
|
private readonly TranslatorInterface $trans,
|
||||||
SerializerInterface $serializer,
|
private readonly SerializerInterface $serializer,
|
||||||
AccompanyingPeriodWorkRepository $workRepository,
|
private readonly AccompanyingPeriodWorkRepository $workRepository,
|
||||||
PaginatorFactory $paginator,
|
private readonly PaginatorFactory $paginator,
|
||||||
LoggerInterface $chillLogger
|
private readonly LoggerInterface $chillLogger,
|
||||||
|
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory
|
||||||
) {
|
) {
|
||||||
$this->trans = $trans;
|
|
||||||
$this->serializer = $serializer;
|
|
||||||
$this->workRepository = $workRepository;
|
|
||||||
$this->paginator = $paginator;
|
|
||||||
$this->chillLogger = $chillLogger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,11 +156,21 @@ class AccompanyingCourseWorkController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period);
|
$this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period);
|
||||||
|
|
||||||
|
$filter = $this->buildFilterOrder($period);
|
||||||
|
|
||||||
|
$filterData = [
|
||||||
|
'types' => $filter->hasEntityChoice('typesFilter') ? $filter->getEntityChoiceData('typesFilter') : [],
|
||||||
|
'before' => $filter->getDateRangeData('dateFilter')['to'],
|
||||||
|
'after' => $filter->getDateRangeData('dateFilter')['from'],
|
||||||
|
'user' => $filter->getUserPickerData('userFilter')
|
||||||
|
];
|
||||||
|
|
||||||
$totalItems = $this->workRepository->countByAccompanyingPeriod($period);
|
$totalItems = $this->workRepository->countByAccompanyingPeriod($period);
|
||||||
$paginator = $this->paginator->create($totalItems);
|
$paginator = $this->paginator->create($totalItems);
|
||||||
|
|
||||||
$works = $this->workRepository->findByAccompanyingPeriodOpenFirst(
|
$works = $this->workRepository->findByAccompanyingPeriodOpenFirst(
|
||||||
$period,
|
$period,
|
||||||
|
$filterData,
|
||||||
$paginator->getItemsPerPage(),
|
$paginator->getItemsPerPage(),
|
||||||
$paginator->getCurrentPageFirstItemNumber()
|
$paginator->getCurrentPageFirstItemNumber()
|
||||||
);
|
);
|
||||||
@ -175,6 +179,7 @@ class AccompanyingCourseWorkController extends AbstractController
|
|||||||
'accompanyingCourse' => $period,
|
'accompanyingCourse' => $period,
|
||||||
'works' => $works,
|
'works' => $works,
|
||||||
'paginator' => $paginator,
|
'paginator' => $paginator,
|
||||||
|
'filter' => $filter
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +204,7 @@ class AccompanyingCourseWorkController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createDeleteForm(int $id): Form
|
private function createDeleteForm(int $id): FormInterface
|
||||||
{
|
{
|
||||||
$params = [];
|
$params = [];
|
||||||
$params['id'] = $id;
|
$params['id'] = $id;
|
||||||
@ -210,4 +215,26 @@ class AccompanyingCourseWorkController extends AbstractController
|
|||||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||||
->getForm();
|
->getForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildFilterOrder($associatedPeriod): FilterOrderHelper
|
||||||
|
{
|
||||||
|
$filterBuilder = $this->filterOrderHelperFactory->create(self::class);
|
||||||
|
$types = $this->workRepository->findActionTypeByPeriod($associatedPeriod);
|
||||||
|
|
||||||
|
$filterBuilder
|
||||||
|
->addDateRange('dateFilter', 'accompanying_course_work.date_filter');
|
||||||
|
|
||||||
|
if (1 < count($types)) {
|
||||||
|
$filterBuilder
|
||||||
|
->addEntityChoice('typesFilter', 'accompanying_course_work.types_filter', \Chill\PersonBundle\Entity\SocialWork\SocialAction::class, $types, [
|
||||||
|
'choice_label' => fn (SocialAction $sa) => $this->translatableStringHelper->localize($sa->getTitle())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filterBuilder
|
||||||
|
->addUserPicker('userFilter', 'accompanying_course_work.user_filter', ['required' => false])
|
||||||
|
;
|
||||||
|
|
||||||
|
return $filterBuilder->build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\PersonBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
class AccompanyingCourseWorkEvaluationDocumentController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private Security $security)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(
|
||||||
|
* "{_locale}/person/accompanying-period/work/evaluation/document/{id}/show",
|
||||||
|
* name="chill_person_accompanying_period_work_evaluation_document_show",
|
||||||
|
* methods={"GET"}
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function showAccompanyingCourseWork(AccompanyingPeriodWorkEvaluationDocument $document): Response
|
||||||
|
{
|
||||||
|
$work = $document->getAccompanyingPeriodWorkEvaluation()->getAccompanyingPeriodWork();
|
||||||
|
|
||||||
|
return $this->redirectToRoute(
|
||||||
|
$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work) ?
|
||||||
|
'chill_person_accompanying_period_work_edit' : 'chill_person_accompanying_period_work_show',
|
||||||
|
[
|
||||||
|
'id' => $work->getId()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,7 @@ use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
|
|||||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||||
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
|
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
|
||||||
use Chill\PersonBundle\Repository\PersonRepository;
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
@ -45,95 +46,13 @@ use Symfony\Component\Form\FormBuilderInterface;
|
|||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
use function strlen;
|
use function strlen;
|
||||||
|
|
||||||
class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
|
final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
|
||||||
{
|
{
|
||||||
private const FIELDS = [
|
|
||||||
'id',
|
|
||||||
'step',
|
|
||||||
'stepSince',
|
|
||||||
'openingDate',
|
|
||||||
'closingDate',
|
|
||||||
'referrer',
|
|
||||||
'referrerSince',
|
|
||||||
'administrativeLocation',
|
|
||||||
'locationIsPerson',
|
|
||||||
'locationIsTemp',
|
|
||||||
'locationPersonName',
|
|
||||||
'locationPersonId',
|
|
||||||
'origin',
|
|
||||||
'closingMotive',
|
|
||||||
'confidential',
|
|
||||||
'emergency',
|
|
||||||
'intensity',
|
|
||||||
'job',
|
|
||||||
'isRequestorPerson',
|
|
||||||
'isRequestorThirdParty',
|
|
||||||
'requestorPerson',
|
|
||||||
'requestorPersonId',
|
|
||||||
'requestorThirdParty',
|
|
||||||
'requestorThirdPartyId',
|
|
||||||
'scopes',
|
|
||||||
'socialIssues',
|
|
||||||
'createdAt',
|
|
||||||
'createdBy',
|
|
||||||
'updatedAt',
|
|
||||||
'updatedBy',
|
|
||||||
];
|
|
||||||
|
|
||||||
private ExportAddressHelper $addressHelper;
|
|
||||||
|
|
||||||
private DateTimeHelper $dateTimeHelper;
|
|
||||||
|
|
||||||
private EntityManagerInterface $entityManager;
|
|
||||||
|
|
||||||
private PersonRenderInterface $personRender;
|
|
||||||
|
|
||||||
private PersonRepository $personRepository;
|
|
||||||
|
|
||||||
private RollingDateConverterInterface $rollingDateConverter;
|
|
||||||
|
|
||||||
private SocialIssueRender $socialIssueRender;
|
|
||||||
|
|
||||||
private SocialIssueRepository $socialIssueRepository;
|
|
||||||
|
|
||||||
private ThirdPartyRender $thirdPartyRender;
|
|
||||||
|
|
||||||
private ThirdPartyRepository $thirdPartyRepository;
|
|
||||||
|
|
||||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
|
||||||
|
|
||||||
private TranslatorInterface $translator;
|
|
||||||
|
|
||||||
private UserHelper $userHelper;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ExportAddressHelper $addressHelper,
|
private EntityManagerInterface $entityManager,
|
||||||
DateTimeHelper $dateTimeHelper,
|
private RollingDateConverterInterface $rollingDateConverter,
|
||||||
EntityManagerInterface $entityManager,
|
private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper,
|
||||||
PersonRenderInterface $personRender,
|
|
||||||
PersonRepository $personRepository,
|
|
||||||
ThirdPartyRepository $thirdPartyRepository,
|
|
||||||
ThirdPartyRender $thirdPartyRender,
|
|
||||||
SocialIssueRepository $socialIssueRepository,
|
|
||||||
SocialIssueRender $socialIssueRender,
|
|
||||||
TranslatableStringHelperInterface $translatableStringHelper,
|
|
||||||
TranslatorInterface $translator,
|
|
||||||
RollingDateConverterInterface $rollingDateConverter,
|
|
||||||
UserHelper $userHelper
|
|
||||||
) {
|
) {
|
||||||
$this->addressHelper = $addressHelper;
|
|
||||||
$this->dateTimeHelper = $dateTimeHelper;
|
|
||||||
$this->entityManager = $entityManager;
|
|
||||||
$this->personRender = $personRender;
|
|
||||||
$this->personRepository = $personRepository;
|
|
||||||
$this->socialIssueRender = $socialIssueRender;
|
|
||||||
$this->socialIssueRepository = $socialIssueRepository;
|
|
||||||
$this->thirdPartyRender = $thirdPartyRender;
|
|
||||||
$this->thirdPartyRepository = $thirdPartyRepository;
|
|
||||||
$this->translatableStringHelper = $translatableStringHelper;
|
|
||||||
$this->translator = $translator;
|
|
||||||
$this->rollingDateConverter = $rollingDateConverter;
|
|
||||||
$this->userHelper = $userHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder)
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
@ -169,141 +88,12 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
|
|||||||
|
|
||||||
public function getLabels($key, array $values, $data)
|
public function getLabels($key, array $values, $data)
|
||||||
{
|
{
|
||||||
if (substr($key, 0, strlen('address_fields')) === 'address_fields') {
|
return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data);
|
||||||
return $this->addressHelper->getLabel($key, $values, $data, 'address_fields');
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($key) {
|
|
||||||
case 'stepSince':
|
|
||||||
case 'openingDate':
|
|
||||||
case 'closingDate':
|
|
||||||
case 'referrerSince':
|
|
||||||
case 'createdAt':
|
|
||||||
case 'updatedAt':
|
|
||||||
return $this->dateTimeHelper->getLabel('export.list.acp.' . $key);
|
|
||||||
|
|
||||||
case 'origin':
|
|
||||||
case 'closingMotive':
|
|
||||||
case 'job':
|
|
||||||
return function ($value) use ($key) {
|
|
||||||
if ('_header' === $value) {
|
|
||||||
return 'export.list.acp.' . $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $value) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR));
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'locationPersonName':
|
|
||||||
case 'requestorPerson':
|
|
||||||
return function ($value) use ($key) {
|
|
||||||
if ('_header' === $value) {
|
|
||||||
return 'export.list.acp.' . $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $value || null === $person = $this->personRepository->find($value)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->personRender->renderString($person, []);
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'requestorThirdParty':
|
|
||||||
return function ($value) use ($key) {
|
|
||||||
if ('_header' === $value) {
|
|
||||||
return 'export.list.acp.' . $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->thirdPartyRender->renderString($thirdparty, []);
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'scopes':
|
|
||||||
return function ($value) use ($key) {
|
|
||||||
if ('_header' === $value) {
|
|
||||||
return 'export.list.acp.' . $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $value) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode(
|
|
||||||
'|',
|
|
||||||
array_map(
|
|
||||||
fn ($s) => $this->translatableStringHelper->localize($s),
|
|
||||||
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'socialIssues':
|
|
||||||
return function ($value) use ($key) {
|
|
||||||
if ('_header' === $value) {
|
|
||||||
return 'export.list.acp.' . $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $value) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode(
|
|
||||||
'|',
|
|
||||||
array_map(
|
|
||||||
fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []),
|
|
||||||
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'step':
|
|
||||||
return fn ($value) => match ($value) {
|
|
||||||
'_header' => 'export.list.acp.step',
|
|
||||||
null => '',
|
|
||||||
AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'),
|
|
||||||
AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'),
|
|
||||||
AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'),
|
|
||||||
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'),
|
|
||||||
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'),
|
|
||||||
default => $value,
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'intensity':
|
|
||||||
return fn ($value) => match ($value) {
|
|
||||||
'_header' => 'export.list.acp.intensity',
|
|
||||||
null => '',
|
|
||||||
AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'),
|
|
||||||
AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'),
|
|
||||||
default => $value,
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return static function ($value) use ($key) {
|
|
||||||
if ('_header' === $value) {
|
|
||||||
return 'export.list.acp.' . $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $value) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getQueryKeys($data)
|
public function getQueryKeys($data)
|
||||||
{
|
{
|
||||||
return array_merge(
|
return $this->listAccompanyingPeriodHelper->getQueryKeys($data);
|
||||||
self::FIELDS,
|
|
||||||
$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getResult($query, $data)
|
public function getResult($query, $data)
|
||||||
@ -341,7 +131,12 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
|
|||||||
->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT)
|
->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT)
|
||||||
->setParameter('authorized_centers', $centers);
|
->setParameter('authorized_centers', $centers);
|
||||||
|
|
||||||
$this->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date']));
|
$this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date']));
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->addOrderBy('acp.openingDate')
|
||||||
|
->addOrderBy('acp.closingDate')
|
||||||
|
->addOrderBy('acp.id');
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
@ -357,91 +152,4 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
|
|||||||
Declarations::ACP_TYPE,
|
Declarations::ACP_TYPE,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addSelectClauses(QueryBuilder $qb, DateTimeImmutable $calcDate): void
|
|
||||||
{
|
|
||||||
// add the regular fields
|
|
||||||
foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) {
|
|
||||||
$qb->addSelect(sprintf('acp.%s AS %s', $field, $field));
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the field which are simple association
|
|
||||||
foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) {
|
|
||||||
$qb
|
|
||||||
->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t")
|
|
||||||
->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity));
|
|
||||||
}
|
|
||||||
|
|
||||||
// step at date
|
|
||||||
$qb
|
|
||||||
->addSelect('stepHistory.step AS step')
|
|
||||||
->addSelect('stepHistory.startDate AS stepSince')
|
|
||||||
->leftJoin('acp.stepHistories', 'stepHistory')
|
|
||||||
->andWhere(
|
|
||||||
$qb->expr()->andX(
|
|
||||||
$qb->expr()->lte('stepHistory.startDate', ':calcDate'),
|
|
||||||
$qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate'))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// referree at date
|
|
||||||
$qb
|
|
||||||
->addSelect('referrer_t.label AS referrer')
|
|
||||||
->addSelect('userHistory.startDate AS referrerSince')
|
|
||||||
->leftJoin('acp.userHistories', 'userHistory')
|
|
||||||
->leftJoin('userHistory.user', 'referrer_t')
|
|
||||||
->andWhere(
|
|
||||||
$qb->expr()->orX(
|
|
||||||
$qb->expr()->isNull('userHistory'),
|
|
||||||
$qb->expr()->andX(
|
|
||||||
$qb->expr()->lte('userHistory.startDate', ':calcDate'),
|
|
||||||
$qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate'))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// location of the acp
|
|
||||||
$qb
|
|
||||||
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson')
|
|
||||||
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp')
|
|
||||||
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName')
|
|
||||||
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId')
|
|
||||||
->leftJoin('acp.locationHistories', 'locationHistory')
|
|
||||||
->andWhere(
|
|
||||||
$qb->expr()->orX(
|
|
||||||
$qb->expr()->isNull('locationHistory'),
|
|
||||||
$qb->expr()->andX(
|
|
||||||
$qb->expr()->lte('locationHistory.startDate', ':calcDate'),
|
|
||||||
$qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate'))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
->leftJoin(
|
|
||||||
PersonHouseholdAddress::class,
|
|
||||||
'personAddress',
|
|
||||||
Join::WITH,
|
|
||||||
'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))'
|
|
||||||
)
|
|
||||||
->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id');
|
|
||||||
|
|
||||||
$this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields');
|
|
||||||
|
|
||||||
// requestor
|
|
||||||
$qb
|
|
||||||
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson')
|
|
||||||
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty')
|
|
||||||
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId')
|
|
||||||
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId')
|
|
||||||
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson')
|
|
||||||
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty');
|
|
||||||
|
|
||||||
$qb
|
|
||||||
// scopes
|
|
||||||
->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes')
|
|
||||||
// social issues
|
|
||||||
->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues');
|
|
||||||
|
|
||||||
// add parameter
|
|
||||||
$qb->setParameter('calcDate', $calcDate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,12 @@ use function count;
|
|||||||
use function in_array;
|
use function in_array;
|
||||||
use function strlen;
|
use function strlen;
|
||||||
|
|
||||||
class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface
|
/**
|
||||||
|
* List the persons, having an accompanying period.
|
||||||
|
*
|
||||||
|
* Details of the accompanying period are not included
|
||||||
|
*/
|
||||||
|
class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface
|
||||||
{
|
{
|
||||||
private ExportAddressHelper $addressHelper;
|
private ExportAddressHelper $addressHelper;
|
||||||
|
|
||||||
@ -185,6 +190,11 @@ class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterfac
|
|||||||
|
|
||||||
$this->listPersonHelper->addSelect($qb, $fields, $data['address_date']);
|
$this->listPersonHelper->addSelect($qb, $fields, $data['address_date']);
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->addOrderBy('person.lastName')
|
||||||
|
->addOrderBy('person.firstName')
|
||||||
|
->addOrderBy('person.id');
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\PersonBundle\Export\Export;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Export\ExportElementValidatedInterface;
|
||||||
|
use Chill\MainBundle\Export\FormatterInterface;
|
||||||
|
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||||
|
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
|
||||||
|
use Chill\MainBundle\Export\ListInterface;
|
||||||
|
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||||
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
|
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
|
||||||
|
use Chill\PersonBundle\Export\Helper\ListPersonHelper;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\ORM\AbstractQuery;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Validator\Constraints\Callback;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
use function array_key_exists;
|
||||||
|
use function count;
|
||||||
|
use function in_array;
|
||||||
|
use function strlen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the persons having an accompanying period, with the accompanying period details
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInterface, GroupedExportInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ListPersonHelper $listPersonHelper,
|
||||||
|
private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
$builder->add('address_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'Data valid at this date',
|
||||||
|
'help' => 'Data regarding center, addresses, and so on will be computed at this date',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return ['address_date' => new RollingDate(RollingDate::T_TODAY)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllowedFormattersTypes()
|
||||||
|
{
|
||||||
|
return [FormatterInterface::TYPE_LIST];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription()
|
||||||
|
{
|
||||||
|
return 'export.list.person_with_acp.Create a list of people having an accompaying periods with details of period, according to various filters.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGroup(): string
|
||||||
|
{
|
||||||
|
return 'Exports of persons';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels($key, array $values, $data)
|
||||||
|
{
|
||||||
|
if (in_array($key, $this->listPersonHelper->getAllKeys(), true)) {
|
||||||
|
return $this->listPersonHelper->getLabels($key, $values, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryKeys($data)
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
$this->listPersonHelper->getAllKeys(),
|
||||||
|
$this->listAccompanyingPeriodHelper->getQueryKeys($data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult($query, $data)
|
||||||
|
{
|
||||||
|
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return 'export.list.person_with_acp.List peoples having an accompanying period with period details';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType()
|
||||||
|
{
|
||||||
|
return Declarations::PERSON_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* param array{fields: string[], address_date: DateTimeImmutable} $data.
|
||||||
|
*/
|
||||||
|
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||||
|
{
|
||||||
|
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||||
|
|
||||||
|
$qb = $this->entityManager->createQueryBuilder();
|
||||||
|
|
||||||
|
$qb->from(Person::class, 'person')
|
||||||
|
->join('person.accompanyingPeriodParticipations', 'acppart')
|
||||||
|
->join('acppart.accompanyingPeriod', 'acp')
|
||||||
|
->andWhere($qb->expr()->neq('acp.step', "'" . AccompanyingPeriod::STEP_DRAFT . "'"))
|
||||||
|
->andWhere(
|
||||||
|
$qb->expr()->exists(
|
||||||
|
'SELECT 1 FROM ' . PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)'
|
||||||
|
)
|
||||||
|
)->setParameter('authorized_centers', $centers);
|
||||||
|
|
||||||
|
$this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date']));
|
||||||
|
$this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date']));
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->addOrderBy('person.lastName')
|
||||||
|
->addOrderBy('person.firstName')
|
||||||
|
->addOrderBy('person.id')
|
||||||
|
->addOrderBy('acp.id');
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requiredRole(): string
|
||||||
|
{
|
||||||
|
return PersonVoter::LISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsModifiers()
|
||||||
|
{
|
||||||
|
return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,317 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\PersonBundle\Export\Helper;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Address;
|
||||||
|
use Chill\MainBundle\Entity\Scope;
|
||||||
|
use Chill\MainBundle\Export\Helper\DateTimeHelper;
|
||||||
|
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
|
||||||
|
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||||
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||||
|
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
||||||
|
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
|
||||||
|
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
|
||||||
|
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
|
||||||
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final readonly class ListAccompanyingPeriodHelper
|
||||||
|
{
|
||||||
|
public const FIELDS = [
|
||||||
|
'acpId',
|
||||||
|
'step',
|
||||||
|
'stepSince',
|
||||||
|
'openingDate',
|
||||||
|
'closingDate',
|
||||||
|
'referrer',
|
||||||
|
'referrerSince',
|
||||||
|
'administrativeLocation',
|
||||||
|
'locationIsPerson',
|
||||||
|
'locationIsTemp',
|
||||||
|
'locationPersonName',
|
||||||
|
'locationPersonId',
|
||||||
|
'origin',
|
||||||
|
'closingMotive',
|
||||||
|
'confidential',
|
||||||
|
'emergency',
|
||||||
|
'intensity',
|
||||||
|
'job',
|
||||||
|
'isRequestorPerson',
|
||||||
|
'isRequestorThirdParty',
|
||||||
|
'requestorPerson',
|
||||||
|
'requestorPersonId',
|
||||||
|
'requestorThirdParty',
|
||||||
|
'requestorThirdPartyId',
|
||||||
|
'scopes',
|
||||||
|
'socialIssues',
|
||||||
|
'acpCreatedAt',
|
||||||
|
'acpCreatedBy',
|
||||||
|
'acpUpdatedAt',
|
||||||
|
'acpUpdatedBy',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ExportAddressHelper $addressHelper,
|
||||||
|
private DateTimeHelper $dateTimeHelper,
|
||||||
|
private PersonRenderInterface $personRender,
|
||||||
|
private PersonRepository $personRepository,
|
||||||
|
private ThirdPartyRepository $thirdPartyRepository,
|
||||||
|
private ThirdPartyRender $thirdPartyRender,
|
||||||
|
private SocialIssueRepository $socialIssueRepository,
|
||||||
|
private SocialIssueRender $socialIssueRender,
|
||||||
|
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
private TranslatorInterface $translator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryKeys($data)
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
ListAccompanyingPeriodHelper::FIELDS,
|
||||||
|
$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'acp_address_fields')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels($key, array $values, $data)
|
||||||
|
{
|
||||||
|
if (str_starts_with($key, 'acp_address_fields')) {
|
||||||
|
return $this->addressHelper->getLabel($key, $values, $data, 'acp_address_fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($key) {
|
||||||
|
case 'stepSince':
|
||||||
|
case 'openingDate':
|
||||||
|
case 'closingDate':
|
||||||
|
case 'referrerSince':
|
||||||
|
case 'acpCreatedAt':
|
||||||
|
case 'acpUpdatedAt':
|
||||||
|
return $this->dateTimeHelper->getLabel('export.list.acp.' . $key);
|
||||||
|
|
||||||
|
case 'origin':
|
||||||
|
case 'closingMotive':
|
||||||
|
case 'job':
|
||||||
|
return function ($value) use ($key) {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.list.acp.' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR));
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'locationPersonName':
|
||||||
|
case 'requestorPerson':
|
||||||
|
return function ($value) use ($key) {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.list.acp.' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value || null === $person = $this->personRepository->find($value)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->personRender->renderString($person, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'requestorThirdParty':
|
||||||
|
return function ($value) use ($key) {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.list.acp.' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->thirdPartyRender->renderString($thirdparty, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'scopes':
|
||||||
|
return function ($value) use ($key) {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.list.acp.' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(
|
||||||
|
'|',
|
||||||
|
array_map(
|
||||||
|
fn ($s) => $this->translatableStringHelper->localize($s),
|
||||||
|
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'socialIssues':
|
||||||
|
return function ($value) use ($key) {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.list.acp.' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(
|
||||||
|
'|',
|
||||||
|
array_map(
|
||||||
|
fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []),
|
||||||
|
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'step':
|
||||||
|
return fn ($value) => match ($value) {
|
||||||
|
'_header' => 'export.list.acp.step',
|
||||||
|
null => '',
|
||||||
|
AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'),
|
||||||
|
AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'),
|
||||||
|
AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'),
|
||||||
|
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'),
|
||||||
|
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'),
|
||||||
|
default => $value,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'intensity':
|
||||||
|
return fn ($value) => match ($value) {
|
||||||
|
'_header' => 'export.list.acp.intensity',
|
||||||
|
null => '',
|
||||||
|
AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'),
|
||||||
|
AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'),
|
||||||
|
default => $value,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return static function ($value) use ($key) {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.list.acp.' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void
|
||||||
|
{
|
||||||
|
$qb->addSelect('acp.id AS acpId');
|
||||||
|
$qb->addSelect('acp.createdAt AS acpCreatedAt');
|
||||||
|
$qb->addSelect('acp.updatedAt AS acpUpdatedAt');
|
||||||
|
|
||||||
|
// add the regular fields
|
||||||
|
foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity'] as $field) {
|
||||||
|
$qb->addSelect(sprintf('acp.%s AS %s', $field, $field));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the field which are simple association
|
||||||
|
$qb
|
||||||
|
->leftJoin('acp.createdBy', "acp_created_by_t")
|
||||||
|
->addSelect('acp_created_by_t.label AS acpCreatedBy');
|
||||||
|
$qb
|
||||||
|
->leftJoin('acp.updatedBy', "acp_updated_by_t")
|
||||||
|
->addSelect('acp_updated_by_t.label AS acpUpdatedBy');
|
||||||
|
|
||||||
|
foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) {
|
||||||
|
$qb
|
||||||
|
->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t")
|
||||||
|
->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// step at date
|
||||||
|
$qb
|
||||||
|
->addSelect('stepHistory.step AS step')
|
||||||
|
->addSelect('stepHistory.startDate AS stepSince')
|
||||||
|
->leftJoin('acp.stepHistories', 'stepHistory')
|
||||||
|
->andWhere(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->lte('stepHistory.startDate', ':calcDate'),
|
||||||
|
$qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// referree at date
|
||||||
|
$qb
|
||||||
|
->addSelect('referrer_t.label AS referrer')
|
||||||
|
->addSelect('userHistory.startDate AS referrerSince')
|
||||||
|
->leftJoin('acp.userHistories', 'userHistory')
|
||||||
|
->leftJoin('userHistory.user', 'referrer_t')
|
||||||
|
->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('userHistory'),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->lte('userHistory.startDate', ':calcDate'),
|
||||||
|
$qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// location of the acp
|
||||||
|
$qb
|
||||||
|
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson')
|
||||||
|
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp')
|
||||||
|
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName')
|
||||||
|
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId')
|
||||||
|
->leftJoin('acp.locationHistories', 'locationHistory')
|
||||||
|
->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('locationHistory'),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->lte('locationHistory.startDate', ':calcDate'),
|
||||||
|
$qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate'))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->leftJoin(
|
||||||
|
PersonHouseholdAddress::class,
|
||||||
|
'acpPersonAddress',
|
||||||
|
Join::WITH,
|
||||||
|
'locationHistory.personLocation = acpPersonAddress.person AND (acpPersonAddress.validFrom <= :calcDate AND (acpPersonAddress.validTo IS NULL OR acpPersonAddress.validTo > :calcDate))'
|
||||||
|
)
|
||||||
|
->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(acpPersonAddress.address)) = acp_address.id');
|
||||||
|
|
||||||
|
$this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'acp_address_fields');
|
||||||
|
|
||||||
|
// requestor
|
||||||
|
$qb
|
||||||
|
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson')
|
||||||
|
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty')
|
||||||
|
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId')
|
||||||
|
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId')
|
||||||
|
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson')
|
||||||
|
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
// scopes
|
||||||
|
->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes')
|
||||||
|
// social issues
|
||||||
|
->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues');
|
||||||
|
|
||||||
|
// add parameter
|
||||||
|
$qb->setParameter('calcDate', $calcDate);
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\PersonBundle\Export\Helper;
|
namespace Chill\PersonBundle\Export\Helper;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Language;
|
||||||
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
|
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
|
||||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||||
use Chill\MainBundle\Repository\CivilityRepositoryInterface;
|
use Chill\MainBundle\Repository\CivilityRepositoryInterface;
|
||||||
@ -42,7 +43,7 @@ use function strlen;
|
|||||||
class ListPersonHelper
|
class ListPersonHelper
|
||||||
{
|
{
|
||||||
public const FIELDS = [
|
public const FIELDS = [
|
||||||
'id',
|
'personId',
|
||||||
'civility',
|
'civility',
|
||||||
'firstName',
|
'firstName',
|
||||||
'lastName',
|
'lastName',
|
||||||
@ -114,7 +115,26 @@ class ListPersonHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array|value-of<self::FIELDS>[] $fields
|
* Those keys are the "direct" keys, which are created when we decide to use to list all the keys.
|
||||||
|
*
|
||||||
|
* This method must be used in `getKeys` instead of the `self::FIELDS`
|
||||||
|
*
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
public function getAllKeys(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...array_filter(
|
||||||
|
ListPersonHelper::FIELDS,
|
||||||
|
fn (string $key) => !in_array($key, ['address_fields', 'lifecycleUpdate'], true)
|
||||||
|
),
|
||||||
|
...$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'),
|
||||||
|
...['createdAt', 'createdBy', 'updatedAt', 'updatedBy'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<value-of<self::FIELDS>> $fields
|
||||||
*/
|
*/
|
||||||
public function addSelect(QueryBuilder $qb, array $fields, DateTimeImmutable $computedDate): void
|
public function addSelect(QueryBuilder $qb, array $fields, DateTimeImmutable $computedDate): void
|
||||||
{
|
{
|
||||||
@ -124,6 +144,11 @@ class ListPersonHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch ($f) {
|
switch ($f) {
|
||||||
|
case 'personId':
|
||||||
|
$qb->addSelect('person.id AS personId');
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case 'countryOfBirth':
|
case 'countryOfBirth':
|
||||||
case 'nationality':
|
case 'nationality':
|
||||||
$qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f));
|
$qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f));
|
||||||
@ -138,25 +163,7 @@ class ListPersonHelper
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'spokenLanguages':
|
case 'spokenLanguages':
|
||||||
$qb
|
$qb->addSelect('(SELECT AGGREGATE(language.id) FROM ' . Language::class . ' language WHERE language MEMBER OF person.spokenLanguages) AS spokenLanguages');
|
||||||
->leftJoin('person.spokenLanguages', 'spokenLanguage')
|
|
||||||
->addSelect('AGGREGATE(spokenLanguage.id) AS spokenLanguages')
|
|
||||||
->addGroupBy('person');
|
|
||||||
|
|
||||||
if (in_array('center', $fields, true)) {
|
|
||||||
$qb->addGroupBy('center');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array('address_fields', $fields, true)) {
|
|
||||||
$qb
|
|
||||||
->addGroupBy('address_fieldsid')
|
|
||||||
->addGroupBy('address_fieldscountry_t.id')
|
|
||||||
->addGroupBy('address_fieldspostcode_t.id');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array('household_id', $fields, true)) {
|
|
||||||
$qb->addGroupBy('household_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -95,29 +95,103 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
|
|||||||
* * then, closed works
|
* * then, closed works
|
||||||
*
|
*
|
||||||
* @return AccompanyingPeriodWork[]
|
* @return AccompanyingPeriodWork[]
|
||||||
|
* @param array{types?: list<SocialAction>, user?: list<User>, after?: null|\DateTimeImmutable, before?: null|\DateTimeImmutable} $filters
|
||||||
*/
|
*/
|
||||||
public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, int $limit = 10, int $offset = 0): array
|
public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, array $filters, int $limit = 10, int $offset = 0): array
|
||||||
{
|
{
|
||||||
$rsm = new ResultSetMappingBuilder($this->em);
|
$rsm = new ResultSetMappingBuilder($this->em);
|
||||||
$rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');
|
$rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');
|
||||||
|
|
||||||
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
|
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
|
||||||
WHERE accompanyingPeriod_id = :periodId
|
LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id
|
||||||
ORDER BY
|
WHERE accompanyingPeriod_id = :periodId";
|
||||||
CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
|
|
||||||
startdate DESC,
|
// implement filters
|
||||||
enddate DESC,
|
|
||||||
id DESC
|
if ([] !== ($filters['types'] ?? [])) {
|
||||||
LIMIT :limit OFFSET :offset";
|
$sql .= " AND w.socialaction_id IN (:types)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([] !== ($filters['user'] ?? [])) {
|
||||||
|
$sql .= " AND rw.user_id IN ("
|
||||||
|
. implode(
|
||||||
|
', ',
|
||||||
|
// we add a user_xx for each key of the 'user' list
|
||||||
|
array_map(fn (User $u, int $idx) => ':user_' . $idx, $filters['user'], array_keys($filters['user']))
|
||||||
|
)
|
||||||
|
. ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " AND daterange(:after::date, :before::date) && daterange(w.startDate, w.endDate)";
|
||||||
|
|
||||||
|
// if the start and end date were inversed, we inverse the order to avoid an error
|
||||||
|
if (null !== ($filters['after'] ?? null) && null !== ($filters['before']) && $filters['after'] > $filters['before']) {
|
||||||
|
$before = $filters['after'];
|
||||||
|
$after = $filters['before'];
|
||||||
|
} else {
|
||||||
|
$before = $filters['before'];
|
||||||
|
$after = $filters['after'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// set limit and offset
|
||||||
|
$sql .= " ORDER BY
|
||||||
|
CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
|
||||||
|
startdate DESC,
|
||||||
|
enddate DESC,
|
||||||
|
id DESC";
|
||||||
|
|
||||||
|
$sql .= " LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
$typeIds = [];
|
||||||
|
foreach ($filters['types'] as $type) {
|
||||||
|
$typeIds[] = $type->getId();
|
||||||
|
}
|
||||||
|
|
||||||
$nq = $this->em->createNativeQuery($sql, $rsm)
|
$nq = $this->em->createNativeQuery($sql, $rsm)
|
||||||
->setParameter('periodId', $period->getId(), Types::INTEGER)
|
->setParameter('periodId', $period->getId(), Types::INTEGER)
|
||||||
|
->setParameter('types', $typeIds)
|
||||||
|
->setParameter('after', $after)
|
||||||
|
->setParameter('before', $before)
|
||||||
->setParameter('limit', $limit, Types::INTEGER)
|
->setParameter('limit', $limit, Types::INTEGER)
|
||||||
->setParameter('offset', $offset, Types::INTEGER);
|
->setParameter('offset', $offset, Types::INTEGER);
|
||||||
|
|
||||||
|
foreach ($filters['user'] as $key => $user) {
|
||||||
|
$nq->setParameter('user_' . $key, $user);
|
||||||
|
}
|
||||||
|
|
||||||
return $nq->getResult();
|
return $nq->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of types of social actions associated to the accompanying period
|
||||||
|
*
|
||||||
|
* @return array<SocialAction>
|
||||||
|
*/
|
||||||
|
public function findActionTypeByPeriod(AccompanyingPeriod $period): array
|
||||||
|
{
|
||||||
|
$in = $this->em->createQueryBuilder();
|
||||||
|
$in
|
||||||
|
->select('1')
|
||||||
|
->from(AccompanyingPeriodWork::class, 'apw');
|
||||||
|
|
||||||
|
|
||||||
|
$in->andWhere('apw.accompanyingPeriod = :period')->setParameter('period', $period);
|
||||||
|
|
||||||
|
|
||||||
|
// join between the embedded exist query and the main query
|
||||||
|
$in->andWhere('apw.socialAction = sa');
|
||||||
|
|
||||||
|
$qb = $this->em->createQueryBuilder()->setParameters($in->getParameters());
|
||||||
|
$qb
|
||||||
|
->select('sa')
|
||||||
|
->from(SocialAction::class, 'sa')
|
||||||
|
->where(
|
||||||
|
$qb->expr()->exists($in->getDQL())
|
||||||
|
);
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
|
public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
|
||||||
{
|
{
|
||||||
return $this->buildQueryNearEndDateByUser($user, $since, $until)
|
return $this->buildQueryNearEndDateByUser($user, $since, $until)
|
||||||
|
@ -68,27 +68,45 @@
|
|||||||
<ul class="list-content fa-ul">
|
<ul class="list-content fa-ul">
|
||||||
<li v-if="person.current_household_id">
|
<li v-if="person.current_household_id">
|
||||||
<i class="fa fa-li fa-map-marker"></i>
|
<i class="fa fa-li fa-map-marker"></i>
|
||||||
<address-render-box v-if="person.current_household_address"
|
<address-render-box v-if="person.current_household_address" :address="person.current_household_address" :isMultiline="isMultiline"></address-render-box>
|
||||||
:address="person.current_household_address"
|
<p v-else class="chill-no-data-statement">{{ $t('renderbox.household_without_address') }}</p>
|
||||||
:isMultiline="isMultiline">
|
|
||||||
</address-render-box>
|
|
||||||
<p v-else class="chill-no-data-statement">
|
|
||||||
{{ $t('renderbox.household_without_address') }}
|
|
||||||
</p>
|
|
||||||
<a v-if="options.addHouseholdLink === true"
|
<a v-if="options.addHouseholdLink === true"
|
||||||
:href="getCurrentHouseholdUrl"
|
:href="getCurrentHouseholdUrl"
|
||||||
:title="$t('persons_associated.show_household_number', {id: person.current_household_id})">
|
:title="$t('persons_associated.show_household_number', {id: person.current_household_id})">
|
||||||
<span class="badge rounded-pill bg-chill-beige">
|
<span class="badge rounded-pill bg-chill-beige">
|
||||||
<i class="fa fa-fw fa-home"></i><!--{{ $t('persons_associated.show_household') }}-->
|
<i class="fa fa-fw fa-home"></i><!--{{ $t('persons_associated.show_household') }}-->
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="options.addNoData">
|
<li v-else-if="options.addNoData">
|
||||||
<i class="fa fa-li fa-map-marker"></i>
|
<i class="fa fa-li fa-map-marker"></i><p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
|
||||||
<p class="chill-no-data-statement">
|
|
||||||
{{ $t('renderbox.no_data') }}
|
|
||||||
</p>
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<template v-if="this.showResidentialAddresses && (person.current_residential_addresses || []).length > 0">
|
||||||
|
<li v-for="(addr, i) in person.current_residential_addresses" :key="i">
|
||||||
|
<i class="fa fa-li fa-map-marker"></i>
|
||||||
|
<div v-if="addr.address">
|
||||||
|
<span class="item-key">{{ $t('renderbox.residential_address') }}:</span>
|
||||||
|
<div style="margin-top: -1em;">
|
||||||
|
<address-render-box :address="addr.address" :isMultiline="isMultiline"></address-render-box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="addr.hostPerson" class="mt-3">
|
||||||
|
<p>{{ $t('renderbox.located_at') }}:</p>
|
||||||
|
<span class="chill-entity entity-person badge-person">
|
||||||
|
<person-text v-if="addr.hostPerson" :person="addr.hostPerson"></person-text>
|
||||||
|
</span>
|
||||||
|
<address-render-box v-if="addr.hostPerson.address" :address="addr.hostPerson.address" :isMultiline="isMultiline"></address-render-box>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="addr.hostThirdParty" class="mt-3">
|
||||||
|
<p>{{ $t('renderbox.located_at') }}:</p>
|
||||||
|
<span class="chill-entity entity-person badge-thirdparty">
|
||||||
|
<third-party-text v-if="addr.hostThirdParty" :thirdparty="addr.hostThirdParty"></third-party-text>
|
||||||
|
</span>
|
||||||
|
<address-render-box v-if="addr.hostThirdParty.address" :address="addr.hostThirdParty.address" :isMultiline="isMultiline"></address-render-box>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
<li v-if="person.email">
|
<li v-if="person.email">
|
||||||
<i class="fa fa-li fa-envelope-o"></i>
|
<i class="fa fa-li fa-envelope-o"></i>
|
||||||
@ -131,53 +149,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item-col mx-3"
|
|
||||||
v-if="this.showResidentialAddresses && (person.current_residential_addresses || []).length > 0">
|
|
||||||
<div class="float-button bottom">
|
|
||||||
<div class="box">
|
|
||||||
<ul class="list-content fa-ul">
|
|
||||||
<li v-for="(addr, i) in person.current_residential_addresses" :key="i">
|
|
||||||
<i class="fa fa-li fa-map-marker"></i>
|
|
||||||
<div v-if="addr.address">
|
|
||||||
<address-render-box
|
|
||||||
:address="addr.address"
|
|
||||||
:isMultiline="isMultiline">
|
|
||||||
</address-render-box>
|
|
||||||
<p>({{ $t('renderbox.residential_address') }})</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="addr.hostPerson" class="mt-3">
|
|
||||||
<p>{{ $t('renderbox.located_at') }}:</p>
|
|
||||||
<span class="chill-entity entity-person badge-person">
|
|
||||||
<person-text
|
|
||||||
v-if="addr.hostPerson"
|
|
||||||
:person="addr.hostPerson"
|
|
||||||
></person-text>
|
|
||||||
</span>
|
|
||||||
<address-render-box v-if="addr.hostPerson.address"
|
|
||||||
:address="addr.hostPerson.address"
|
|
||||||
:isMultiline="isMultiline">
|
|
||||||
</address-render-box>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="addr.hostThirdParty" class="mt-3">
|
|
||||||
<p>{{ $t('renderbox.located_at') }}:</p>
|
|
||||||
<span class="chill-entity entity-person badge-thirdparty">
|
|
||||||
<third-party-text
|
|
||||||
v-if="addr.hostThirdParty"
|
|
||||||
:thirdparty="addr.hostThirdParty"
|
|
||||||
></third-party-text>
|
|
||||||
</span>
|
|
||||||
<address-render-box v-if="addr.hostThirdParty.address"
|
|
||||||
:address="addr.hostThirdParty.address"
|
|
||||||
:isMultiline="isMultiline">
|
|
||||||
</address-render-box>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot name="end-bloc"></slot>
|
<slot name="end-bloc"></slot>
|
||||||
|
@ -5,18 +5,23 @@
|
|||||||
{% block js %}
|
{% block js %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||||
|
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
||||||
|
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="accompanying-course-work">
|
<div class="accompanying-course-work">
|
||||||
|
|
||||||
<h1>{{ block('title') }}</h1>
|
<h1>{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
{{ filter|chill_render_filter_order_helper }}
|
||||||
|
|
||||||
{% if works|length == 0 %}
|
{% if works|length == 0 %}
|
||||||
<p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p>
|
<p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
<span class=" d-block d-sm-inline-block">
|
<span class=" d-block d-sm-inline-block">
|
||||||
{{ address|chill_entity_render_box({
|
{{ address|chill_entity_render_box({
|
||||||
'render': 'inline', 'multiline': false, 'with_picto': true, 'with_delimiter': true,
|
'render': 'inline', 'multiline': false, 'with_picto': true, 'with_delimiter': true,
|
||||||
'details_button': false
|
'details_button': true
|
||||||
}) }}
|
}) }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -56,13 +56,12 @@
|
|||||||
{%- if address is not null -%}
|
{%- if address is not null -%}
|
||||||
{{ address|chill_entity_render_box({
|
{{ address|chill_entity_render_box({
|
||||||
'render': 'inline', 'multiline': false, 'with_picto': true, 'with_delimiter': true,
|
'render': 'inline', 'multiline': false, 'with_picto': true, 'with_delimiter': true,
|
||||||
'details_button': false
|
'details_button': true
|
||||||
}) }}
|
}) }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% if person.getCurrentHousehold is not null %}
|
{% if person.getCurrentHousehold is not null %}
|
||||||
<a class="btn household-link text-end"
|
<a href="{{ chill_path_add_return_path('chill_person_household_summary', { 'household_id' : person.getCurrentHousehold.id } ) }}"
|
||||||
href="{{ chill_path_add_return_path('chill_person_household_summary', { 'household_id' : person.getCurrentHousehold.id } ) }}"
|
class="btn btn-sm household-link" title="{{ 'Show household'|trans }}">
|
||||||
title="{{ 'Show household'|trans }}">
|
|
||||||
<i class="fa fa-lg fa-home"></i>
|
<i class="fa fa-lg fa-home"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -47,8 +47,7 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
|
|||||||
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
|
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'persons' => $this->getRelatedEntity($entityWorkflow)
|
'persons' => $this->getRelatedEntity($entityWorkflow)?->getPersons() ?? [],
|
||||||
->getPersons(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,35 +4,27 @@ services:
|
|||||||
autowire: true
|
autowire: true
|
||||||
|
|
||||||
## Indicators
|
## Indicators
|
||||||
chill.person.export.count_person:
|
Chill\PersonBundle\Export\Export\CountPerson:
|
||||||
class: Chill\PersonBundle\Export\Export\CountPerson
|
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export, alias: count_person }
|
- { name: chill.export, alias: count_person }
|
||||||
|
|
||||||
chill.person.export.count_person_with_accompanying_course:
|
Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse:
|
||||||
class: Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse
|
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export, alias: count_person_with_accompanying_course }
|
- { name: chill.export, alias: count_person_with_accompanying_course }
|
||||||
|
|
||||||
Chill\PersonBundle\Export\Export\ListPerson:
|
Chill\PersonBundle\Export\Export\ListPerson:
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export, alias: list_person }
|
- { name: chill.export, alias: list_person }
|
||||||
|
|
||||||
Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriod:
|
Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod:
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export, alias: list_person_with_acp }
|
- { name: chill.export, alias: list_person_with_acp }
|
||||||
|
|
||||||
|
Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriodDetails:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export, alias: list_person_with_acp_details }
|
||||||
|
|
||||||
Chill\PersonBundle\Export\Export\ListAccompanyingPeriod:
|
Chill\PersonBundle\Export\Export\ListAccompanyingPeriod:
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export, alias: list_acp }
|
- { name: chill.export, alias: list_acp }
|
||||||
|
|
||||||
|
@ -913,6 +913,9 @@ accompanying_course_work:
|
|||||||
social_evaluation: Évaluation
|
social_evaluation: Évaluation
|
||||||
private_comment: Commentaire privé
|
private_comment: Commentaire privé
|
||||||
timeSpent: Temps de rédaction
|
timeSpent: Temps de rédaction
|
||||||
|
date_filter: Filtrer par date
|
||||||
|
types_filter: Filtrer par type d'action
|
||||||
|
user_filter: Filtrer par intervenant
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -998,6 +1001,8 @@ notification:
|
|||||||
Notify referrer: Notifier le référent
|
Notify referrer: Notifier le référent
|
||||||
Notify any: Notifier d'autres utilisateurs
|
Notify any: Notifier d'autres utilisateurs
|
||||||
|
|
||||||
|
personId: Identifiant de l'usager
|
||||||
|
|
||||||
export:
|
export:
|
||||||
export:
|
export:
|
||||||
acp_stats:
|
acp_stats:
|
||||||
@ -1147,13 +1152,15 @@ export:
|
|||||||
list:
|
list:
|
||||||
person_with_acp:
|
person_with_acp:
|
||||||
List peoples having an accompanying period: Liste des usagers ayant un parcours d'accompagnement
|
List peoples having an accompanying period: Liste des usagers ayant un parcours d'accompagnement
|
||||||
|
List peoples having an accompanying period with period details: Liste des usagers concernés avec détail de chaque parcours
|
||||||
Create a list of people having an accompaying periods, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager
|
Create a list of people having an accompaying periods, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager
|
||||||
|
Create a list of people having an accompaying periods with details of period, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager. Ajoute les détails du parcours à la liste.
|
||||||
acp:
|
acp:
|
||||||
List of accompanying periods: Liste des parcours d'accompagnements
|
List of accompanying periods: Liste des parcours d'accompagnements
|
||||||
Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres.
|
Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres.
|
||||||
Date of calculation for associated elements: Date de calcul des éléments associés
|
Date of calculation for associated elements: Date de calcul des éléments associés
|
||||||
The associated referree, localisation, and other elements will be valid at this date: Les éléments associés, comme la localisation, le référent et d'autres éléments seront valides à cette date
|
The associated referree, localisation, and other elements will be valid at this date: Les éléments associés, comme la localisation, le référent et d'autres éléments seront valides à cette date
|
||||||
id: Identifiant du parcours
|
acpId: Identifiant du parcours
|
||||||
openingDate: Date d'ouverture du parcours
|
openingDate: Date d'ouverture du parcours
|
||||||
closingDate: Date de fermeture du parcours
|
closingDate: Date de fermeture du parcours
|
||||||
closingMotive: Motif de cloture
|
closingMotive: Motif de cloture
|
||||||
@ -1161,14 +1168,14 @@ export:
|
|||||||
confidential: Confidentiel
|
confidential: Confidentiel
|
||||||
emergency: Urgent
|
emergency: Urgent
|
||||||
intensity: Intensité
|
intensity: Intensité
|
||||||
createdAt: Créé le
|
acpCreatedAt: Créé le
|
||||||
updatedAt: Dernière mise à jour le
|
acpUpdatedAt: Dernière mise à jour le
|
||||||
acpOrigin: Origine du parcours
|
acpOrigin: Origine du parcours
|
||||||
origin: Origine du parcours
|
origin: Origine du parcours
|
||||||
acpClosingMotive: Motif de fermeture
|
acpClosingMotive: Motif de fermeture
|
||||||
acpJob: Métier du parcours
|
acpJob: Métier du parcours
|
||||||
createdBy: Créé par
|
acpCreatedBy: Créé par
|
||||||
updatedBy: Dernière modification par
|
acpUpdatedBy: Dernière modification par
|
||||||
administrativeLocation: Location administrative
|
administrativeLocation: Location administrative
|
||||||
step: Etape
|
step: Etape
|
||||||
stepSince: Dernière modification de l'étape
|
stepSince: Dernière modification de l'étape
|
||||||
@ -1176,7 +1183,7 @@ export:
|
|||||||
referrerSince: Référent depuis le
|
referrerSince: Référent depuis le
|
||||||
locationIsPerson: Parcours localisé auprès d'un usager concerné
|
locationIsPerson: Parcours localisé auprès d'un usager concerné
|
||||||
locationIsTemp: Parcours avec une localisation temporaire
|
locationIsTemp: Parcours avec une localisation temporaire
|
||||||
acpLocationPersonName: Usager auprès duquel le parcours est localisé
|
locationPersonName: Usager auprès duquel le parcours est localisé
|
||||||
locationPersonId: Identifiant de l'usager auprès duquel le parcours est localisé
|
locationPersonId: Identifiant de l'usager auprès duquel le parcours est localisé
|
||||||
acpaddress_fieldscountry: Pays de l'adresse
|
acpaddress_fieldscountry: Pays de l'adresse
|
||||||
isRequestorPerson: Le demandeur est-il un usager ?
|
isRequestorPerson: Le demandeur est-il un usager ?
|
||||||
|
@ -26,6 +26,7 @@ use Chill\TaskBundle\Event\TaskEvent;
|
|||||||
use Chill\TaskBundle\Event\UI\UIEvent;
|
use Chill\TaskBundle\Event\UI\UIEvent;
|
||||||
use Chill\TaskBundle\Form\SingleTaskType;
|
use Chill\TaskBundle\Form\SingleTaskType;
|
||||||
use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface;
|
use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface;
|
||||||
|
use Chill\TaskBundle\Repository\SingleTaskStateRepository;
|
||||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@ -71,7 +72,8 @@ final class SingleTaskController extends AbstractController
|
|||||||
EventDispatcherInterface $eventDispatcher,
|
EventDispatcherInterface $eventDispatcher,
|
||||||
TimelineBuilder $timelineBuilder,
|
TimelineBuilder $timelineBuilder,
|
||||||
LoggerInterface $logger,
|
LoggerInterface $logger,
|
||||||
FilterOrderHelperFactoryInterface $filterOrderHelperFactory
|
FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
|
||||||
|
private SingleTaskStateRepository $singleTaskStateRepository
|
||||||
) {
|
) {
|
||||||
$this->eventDispatcher = $eventDispatcher;
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
$this->timelineBuilder = $timelineBuilder;
|
$this->timelineBuilder = $timelineBuilder;
|
||||||
@ -299,13 +301,17 @@ final class SingleTaskController extends AbstractController
|
|||||||
$this->denyAccessUnlessGranted(TaskVoter::SHOW, null);
|
$this->denyAccessUnlessGranted(TaskVoter::SHOW, null);
|
||||||
|
|
||||||
$filterOrder = $this->buildFilterOrder();
|
$filterOrder = $this->buildFilterOrder();
|
||||||
|
|
||||||
|
$filteredUsers = $filterOrder->getUserPickerData('userPicker');
|
||||||
|
|
||||||
$flags = array_merge(
|
$flags = array_merge(
|
||||||
$filterOrder->getCheckboxData('status'),
|
$filterOrder->getCheckboxData('status'),
|
||||||
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
|
array_map(static fn ($i) => 'state_' . $i, $filterOrder->hasCheckboxData('states') ? $filterOrder->getCheckboxData('states') : [])
|
||||||
);
|
);
|
||||||
$nb = $this->singleTaskAclAwareRepository->countByAllViewable(
|
$nb = $this->singleTaskAclAwareRepository->countByAllViewable(
|
||||||
$filterOrder->getQueryString(),
|
$filterOrder->getQueryString(),
|
||||||
$flags
|
$flags,
|
||||||
|
$filteredUsers
|
||||||
);
|
);
|
||||||
$paginator = $this->paginatorFactory->create($nb);
|
$paginator = $this->paginatorFactory->create($nb);
|
||||||
|
|
||||||
@ -313,6 +319,7 @@ final class SingleTaskController extends AbstractController
|
|||||||
$tasks = $this->singleTaskAclAwareRepository->findByAllViewable(
|
$tasks = $this->singleTaskAclAwareRepository->findByAllViewable(
|
||||||
$filterOrder->getQueryString(),
|
$filterOrder->getQueryString(),
|
||||||
$flags,
|
$flags,
|
||||||
|
$filteredUsers,
|
||||||
$paginator->getCurrentPageFirstItemNumber(),
|
$paginator->getCurrentPageFirstItemNumber(),
|
||||||
$paginator->getItemsPerPage(),
|
$paginator->getItemsPerPage(),
|
||||||
[
|
[
|
||||||
@ -346,7 +353,7 @@ final class SingleTaskController extends AbstractController
|
|||||||
$filterOrder = $this->buildFilterOrder();
|
$filterOrder = $this->buildFilterOrder();
|
||||||
$flags = array_merge(
|
$flags = array_merge(
|
||||||
$filterOrder->getCheckboxData('status'),
|
$filterOrder->getCheckboxData('status'),
|
||||||
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
|
array_map(static fn ($i) => 'state_' . $i, $filterOrder->hasCheckboxData('states') ? $filterOrder->getCheckboxData('states') : [])
|
||||||
);
|
);
|
||||||
$nb = $this->singleTaskAclAwareRepository->countByCourse(
|
$nb = $this->singleTaskAclAwareRepository->countByCourse(
|
||||||
$course,
|
$course,
|
||||||
@ -395,7 +402,7 @@ final class SingleTaskController extends AbstractController
|
|||||||
$filterOrder = $this->buildFilterOrder();
|
$filterOrder = $this->buildFilterOrder();
|
||||||
$flags = array_merge(
|
$flags = array_merge(
|
||||||
$filterOrder->getCheckboxData('status'),
|
$filterOrder->getCheckboxData('status'),
|
||||||
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
|
array_map(static fn ($i) => 'state_' . $i, $filterOrder->hasCheckboxData('states') ? $filterOrder->getCheckboxData('states') : [])
|
||||||
);
|
);
|
||||||
$nb = $this->singleTaskAclAwareRepository->countByPerson(
|
$nb = $this->singleTaskAclAwareRepository->countByPerson(
|
||||||
$person,
|
$person,
|
||||||
@ -447,10 +454,10 @@ final class SingleTaskController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
|
|
||||||
$filterOrder = $this->buildFilterOrder();
|
$filterOrder = $this->buildFilterOrder(false);
|
||||||
$flags = array_merge(
|
$flags = array_merge(
|
||||||
$filterOrder->getCheckboxData('status'),
|
$filterOrder->getCheckboxData('status'),
|
||||||
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
|
array_map(static fn ($i) => 'state_' . $i, $filterOrder->hasCheckboxData('states') ? $filterOrder->getCheckboxData('states') : [])
|
||||||
);
|
);
|
||||||
$nb = $this->singleTaskAclAwareRepository->countByCurrentUsersTasks(
|
$nb = $this->singleTaskAclAwareRepository->countByCurrentUsersTasks(
|
||||||
$filterOrder->getQueryString(),
|
$filterOrder->getQueryString(),
|
||||||
@ -662,7 +669,7 @@ final class SingleTaskController extends AbstractController
|
|||||||
return $form;
|
return $form;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildFilterOrder(): FilterOrderHelper
|
private function buildFilterOrder($includeFilterByUser = true): FilterOrderHelper
|
||||||
{
|
{
|
||||||
$statuses = ['no-alert', 'warning', 'alert'];
|
$statuses = ['no-alert', 'warning', 'alert'];
|
||||||
$statusTrans = [
|
$statusTrans = [
|
||||||
@ -670,17 +677,26 @@ final class SingleTaskController extends AbstractController
|
|||||||
'Tasks near deadline',
|
'Tasks near deadline',
|
||||||
'Tasks over deadline',
|
'Tasks over deadline',
|
||||||
];
|
];
|
||||||
$states = [
|
|
||||||
// todo: get a list of possible states dynamically
|
|
||||||
'new', 'in_progress', 'closed', 'canceled',
|
|
||||||
];
|
|
||||||
|
|
||||||
return $this->filterOrderHelperFactory
|
$filterBuilder = $this->filterOrderHelperFactory
|
||||||
->create(self::class)
|
->create(self::class)
|
||||||
->addSearchBox()
|
->addSearchBox()
|
||||||
->addCheckbox('status', $statuses, $statuses, $statusTrans)
|
->addCheckbox('status', $statuses, $statuses, $statusTrans);
|
||||||
->addCheckbox('states', $states, ['new', 'in_progress'])
|
|
||||||
->build();
|
$states = $this->singleTaskStateRepository->findAllExistingStates();
|
||||||
|
$checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['closed', 'canceled', 'validated'], true)));
|
||||||
|
|
||||||
|
if ([] !== $states) {
|
||||||
|
$filterBuilder
|
||||||
|
->addCheckbox('states', $states, $checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($includeFilterByUser) {
|
||||||
|
$filterBuilder
|
||||||
|
->addUserPicker('userPicker', 'Filter by user', ['multiple' => true, 'required' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filterBuilder->build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,18 +51,37 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
|
|||||||
|
|
||||||
public function buildBaseQuery(
|
public function buildBaseQuery(
|
||||||
?string $pattern = null,
|
?string $pattern = null,
|
||||||
?array $flags = []
|
?array $flags = [],
|
||||||
|
?array $users = []
|
||||||
): QueryBuilder {
|
): QueryBuilder {
|
||||||
$qb = $this->em->createQueryBuilder();
|
$qb = $this->em->createQueryBuilder();
|
||||||
$qb
|
$qb
|
||||||
->from(SingleTask::class, 't');
|
->from(SingleTask::class, 't');
|
||||||
|
|
||||||
if (!empty($pattern)) {
|
if (null !== $pattern && '' !== $pattern) {
|
||||||
$qb->andWhere($qb->expr()->like('LOWER(UNACCENT(t.title))', 'LOWER(UNACCENT(:pattern))'))
|
$qb->andWhere($qb->expr()->like('LOWER(UNACCENT(t.title))', 'LOWER(UNACCENT(:pattern))'))
|
||||||
->setParameter('pattern', '%' . $pattern . '%');
|
->setParameter('pattern', '%' . $pattern . '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($flags) > 0) {
|
if (null !== $users && count($users) > 0) {
|
||||||
|
$orXUser = $qb->expr()->orX();
|
||||||
|
|
||||||
|
foreach ($users as $key => $user) {
|
||||||
|
$orXUser->add(
|
||||||
|
$qb->expr()->eq('t.assignee', ':user_' . $key)
|
||||||
|
);
|
||||||
|
|
||||||
|
$qb->setParameter('user_' . $key, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($orXUser->count() > 0) {
|
||||||
|
$qb->andWhere($orXUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $flags && count($flags) > 0) {
|
||||||
$orXDate = $qb->expr()->orX();
|
$orXDate = $qb->expr()->orX();
|
||||||
$orXState = $qb->expr()->orX();
|
$orXState = $qb->expr()->orX();
|
||||||
$now = new DateTime();
|
$now = new DateTime();
|
||||||
@ -183,9 +202,10 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
|
|||||||
|
|
||||||
public function countByAllViewable(
|
public function countByAllViewable(
|
||||||
?string $pattern = null,
|
?string $pattern = null,
|
||||||
?array $flags = []
|
?array $flags = [],
|
||||||
|
?array $users = []
|
||||||
): int {
|
): int {
|
||||||
$qb = $this->buildBaseQuery($pattern, $flags);
|
$qb = $this->buildBaseQuery($pattern, $flags, $users);
|
||||||
|
|
||||||
return $this
|
return $this
|
||||||
->addACLGlobal($qb)
|
->addACLGlobal($qb)
|
||||||
@ -231,11 +251,12 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
|
|||||||
public function findByAllViewable(
|
public function findByAllViewable(
|
||||||
?string $pattern = null,
|
?string $pattern = null,
|
||||||
?array $flags = [],
|
?array $flags = [],
|
||||||
|
?array $users = [],
|
||||||
?int $start = 0,
|
?int $start = 0,
|
||||||
?int $limit = 50,
|
?int $limit = 50,
|
||||||
?array $orderBy = []
|
?array $orderBy = []
|
||||||
): array {
|
): array {
|
||||||
$qb = $this->buildBaseQuery($pattern, $flags);
|
$qb = $this->buildBaseQuery($pattern, $flags, $users);
|
||||||
$qb = $this->addACLGlobal($qb);
|
$qb = $this->addACLGlobal($qb);
|
||||||
|
|
||||||
return $this->getResult($qb, $start, $limit, $orderBy);
|
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||||
|
@ -18,7 +18,8 @@ interface SingleTaskAclAwareRepositoryInterface
|
|||||||
{
|
{
|
||||||
public function countByAllViewable(
|
public function countByAllViewable(
|
||||||
?string $pattern = null,
|
?string $pattern = null,
|
||||||
?array $flags = []
|
?array $flags = [],
|
||||||
|
?array $users = []
|
||||||
): int;
|
): int;
|
||||||
|
|
||||||
public function countByCourse(
|
public function countByCourse(
|
||||||
@ -38,6 +39,7 @@ interface SingleTaskAclAwareRepositoryInterface
|
|||||||
public function findByAllViewable(
|
public function findByAllViewable(
|
||||||
?string $pattern = null,
|
?string $pattern = null,
|
||||||
?array $flags = [],
|
?array $flags = [],
|
||||||
|
?array $users = [],
|
||||||
?int $start = 0,
|
?int $start = 0,
|
||||||
?int $limit = 50,
|
?int $limit = 50,
|
||||||
?array $orderBy = []
|
?array $orderBy = []
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\TaskBundle\Repository;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Doctrine\DBAL\Exception;
|
||||||
|
|
||||||
|
class SingleTaskStateRepository
|
||||||
|
{
|
||||||
|
private const FIND_ALL_STATES = <<<'SQL'
|
||||||
|
SELECT DISTINCT jsonb_array_elements_text(current_states) FROM chill_task.single_task
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private Connection $connection
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of all states associated to at least one single task in the database
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function findAllExistingStates(): array
|
||||||
|
{
|
||||||
|
$states = [];
|
||||||
|
|
||||||
|
foreach ($this->connection->fetchAllNumeric(self::FIND_ALL_STATES) as $row) {
|
||||||
|
if ('' !== $row[0] && null !== $row[0]) {
|
||||||
|
$states[] = $row[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $states;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -27,8 +27,10 @@
|
|||||||
{% block css %}
|
{% block css %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{{ encore_entry_link_tags('page_task_list') }}
|
{{ encore_entry_link_tags('page_task_list') }}
|
||||||
|
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{{ encore_entry_script_tags('page_task_list') }}
|
{{ encore_entry_script_tags('page_task_list') }}
|
||||||
|
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
|
_defaults:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
|
||||||
chill_task.single_task_repository:
|
chill_task.single_task_repository:
|
||||||
class: Chill\TaskBundle\Repository\SingleTaskRepository
|
class: Chill\TaskBundle\Repository\SingleTaskRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
@ -10,8 +14,8 @@ services:
|
|||||||
- "@chill.main.security.authorization.helper"
|
- "@chill.main.security.authorization.helper"
|
||||||
Chill\TaskBundle\Repository\SingleTaskRepository: '@chill_task.single_task_repository'
|
Chill\TaskBundle\Repository\SingleTaskRepository: '@chill_task.single_task_repository'
|
||||||
|
|
||||||
Chill\TaskBundle\Repository\SingleTaskAclAwareRepository:
|
Chill\TaskBundle\Repository\SingleTaskAclAwareRepository: ~
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
|
|
||||||
Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface: '@Chill\TaskBundle\Repository\SingleTaskAclAwareRepository'
|
Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface: '@Chill\TaskBundle\Repository\SingleTaskAclAwareRepository'
|
||||||
|
|
||||||
|
Chill\TaskBundle\Repository\SingleTaskStateRepository: ~
|
||||||
|
@ -65,6 +65,7 @@ Not assigned: Aucun utilisateur assigné
|
|||||||
For person: Pour
|
For person: Pour
|
||||||
By: Par
|
By: Par
|
||||||
Any tasks: Aucune tâche
|
Any tasks: Aucune tâche
|
||||||
|
Filter by user: Filtrer par utilisateur(s)
|
||||||
|
|
||||||
# transitions - default task definition
|
# transitions - default task definition
|
||||||
"new": "nouvelle"
|
"new": "nouvelle"
|
||||||
|
@ -71,12 +71,9 @@ class ThirdPartyApiSearch implements SearchApiInterface
|
|||||||
->setSelectKey('tparty')
|
->setSelectKey('tparty')
|
||||||
->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)")
|
->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)")
|
||||||
->setFromClause('chill_3party.third_party AS tparty
|
->setFromClause('chill_3party.third_party AS tparty
|
||||||
LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id
|
|
||||||
LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id
|
|
||||||
LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id
|
LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id
|
||||||
LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id
|
LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id)
|
||||||
LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id')
|
LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id');
|
||||||
->andWhereClause('tparty.active IS TRUE');
|
|
||||||
|
|
||||||
$strs = explode(' ', $pattern);
|
$strs = explode(' ', $pattern);
|
||||||
$wheres = [];
|
$wheres = [];
|
||||||
@ -102,9 +99,8 @@ class ThirdPartyApiSearch implements SearchApiInterface
|
|||||||
(parent.canonicalized LIKE '%s' || LOWER(UNACCENT(?)) || '%')::int
|
(parent.canonicalized LIKE '%s' || LOWER(UNACCENT(?)) || '%')::int
|
||||||
) + " .
|
) + " .
|
||||||
// take postcode label into account, but lower than the canonicalized field
|
// take postcode label into account, but lower than the canonicalized field
|
||||||
"COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0) + " .
|
"COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)";
|
||||||
"COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)";
|
$pertinenceArgs[] = [$str, $str, $str, $str, $str];
|
||||||
$pertinenceArgs[] = [$str, $str, $str, $str, $str, $str];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user