mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-12 09:44:58 +00:00
Compare commits
67 Commits
Author | SHA1 | Date | |
---|---|---|---|
2b96160200
|
|||
933c353357
|
|||
e6da727a11 | |||
0719a541a6 | |||
883147ed9c | |||
718de2fad0 | |||
643d8f99be
|
|||
3954d69c94 | |||
78a5e81e33
|
|||
b05e128b5e
|
|||
8f39d0320c | |||
1cc61cee36
|
|||
a21cefab31
|
|||
f2673d6c83
|
|||
18535ee85f
|
|||
aea6796ba1 | |||
872d5e8ebf
|
|||
c0901947ca
|
|||
ea0e5dfa14
|
|||
59da93fd75 | |||
1d9b8729ab | |||
22ded77bde | |||
995e2dac80 | |||
28c41aaf85
|
|||
ce2ce42530 | |||
1409a3b23a
|
|||
51e382760d | |||
b0fcffea2d
|
|||
66e1047752 | |||
7bee376718 | |||
6bc45bbca3 | |||
f3912e5544 | |||
0950074121 | |||
94e9b75e40 | |||
23c7a92546 | |||
8391dbe448 | |||
a7842b2597
|
|||
1552b3c9d7
|
|||
2259a31260 | |||
36eed4323c | |||
9bf8c9b0ed | |||
1930c48d28 | |||
29306d2b66 | |||
e0758215ba | |||
e82c7cdc6c | |||
3f66e1a862
|
|||
efee2d8b44 | |||
f3829d3390 | |||
a2e705bd92 | |||
e38b369149
|
|||
5a36a8660d | |||
f7d385eba1 | |||
edd66f6a6c | |||
ef1eb2031e | |||
cc97199c5d | |||
20d5fabc18 | |||
61982634a6 | |||
6c58e7eb3e | |||
4b25970ce0 | |||
52d51264ba | |||
4e934653be | |||
1ee0e8e350 | |||
4da7040a49 | |||
a34b5f8588 | |||
0d626fb345 | |||
25d4b6acbb | |||
ad72192e24
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: '[export] Add a list for people with their associated course'
|
||||
time: 2023-07-07T12:36:09.596469063+02:00
|
||||
custom:
|
||||
Issue: "125"
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: '[export] Add ordering by person''s lastname or course opening date in list
|
||||
which concerns accompanying course or peoples'
|
||||
time: 2023-07-07T12:41:32.112725962+02:00
|
||||
custom:
|
||||
Issue: ""
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: '[Export] allow to group activities by localisation'
|
||||
time: 2023-07-11T15:00:55.770070399+02:00
|
||||
custom:
|
||||
Issue: "128"
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: '[export] Add a filter "filter course having an activity between two dates"'
|
||||
time: 2023-07-11T15:59:29.065329834+02:00
|
||||
custom:
|
||||
Issue: "129"
|
37
.changes/v2.5.0.md
Normal file
37
.changes/v2.5.0.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## 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
|
||||
|
||||
### 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
|
@@ -7,7 +7,7 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
|
||||
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.
|
||||
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:
|
||||
|
@@ -7,7 +7,6 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v2.4.0 - 2023-07-07
|
||||
|
||||
### Feature
|
||||
* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] on "filter by user working" on accompanying period, add two dates to filters intervention within a period
|
||||
* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add an aggregator by user's job working on a course
|
||||
|
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>
|
||||
Cron Jobs <cronjob.rst>
|
||||
Info about entities <entity-info.rst>
|
||||
Info about database (in French) <database-principles.rst>
|
||||
|
||||
Layout and UI
|
||||
**************
|
||||
|
@@ -1,11 +1,12 @@
|
||||
{% macro table_elements(elements, family) %}
|
||||
{% macro table_elements(elements, type) %}
|
||||
|
||||
<table class="table table-bordered border-dark budget-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="{{ family }} el-type">{{ 'Budget element type'|trans }}</th>
|
||||
<th class="{{ family }}">{{ 'Amount'|trans }}</th>
|
||||
<th class="{{ family }}">{{ 'Validity period'|trans }}</th>
|
||||
<th class="{{ family }}"> </th>
|
||||
<th class="{{ type }} el-type">{{ 'Budget element type'|trans }}</th>
|
||||
<th class="{{ type }}">{{ 'Amount'|trans }}</th>
|
||||
<th class="{{ type }}">{{ 'Validity period'|trans }}</th>
|
||||
<th class="{{ type }}"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -38,17 +39,17 @@
|
||||
<ul class="record_actions">
|
||||
{% if is_granted('CHILL_BUDGET_ELEMENT_SEE', f) %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_BUDGET_ELEMENT_UPDATE', f) %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_BUDGET_ELEMENT_DELETE', f) %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</ul>
|
||||
@@ -69,7 +70,7 @@
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table_results(actualCharges, actualResources) %}
|
||||
{% macro table_results(actualCharges, actualResources, results) %}
|
||||
|
||||
{% set totalCharges = 0 %}
|
||||
{% for c in actualCharges %}
|
||||
@@ -97,6 +98,20 @@
|
||||
{{ result|format_currency('EUR') }}
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
|
||||
<div class="mt-5">
|
||||
<h3 class="subtitle">{{ 'Budget calculator'|trans }}</h3>
|
||||
{{ table_results(charges, resources) }}
|
||||
{{ table_results(charges, resources, results) }}
|
||||
</div>
|
||||
|
||||
{% if is_granted('CHILL_BUDGET_ELEMENT_CREATE', person) %}
|
||||
|
@@ -46,8 +46,7 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
|
||||
|
||||
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
|
||||
{
|
||||
$course = $this->getRelatedEntity($entityWorkflow)
|
||||
->getCourse();
|
||||
$course = $this->getRelatedEntity($entityWorkflow)?->getCourse();
|
||||
$persons = [];
|
||||
|
||||
if (null !== $course) {
|
||||
|
@@ -19,5 +19,13 @@ interface CronJobInterface
|
||||
|
||||
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\Repository\CronJobExecutionRepositoryInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
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_LAST_EXECUTION_DATA = 'UPDATE ' . CronJobExecution::class . ' cr SET cr.lastExecutionData = :data WHERE cr.key = :key';
|
||||
|
||||
private CronJobExecutionRepositoryInterface $cronJobExecutionRepository;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
@@ -85,6 +88,9 @@ class CronManager implements CronManagerInterface
|
||||
foreach ($orderedJobs as $job) {
|
||||
if ($job->canRun($lasts[$job->getKey()] ?? null)) {
|
||||
if (array_key_exists($job->getKey(), $lasts)) {
|
||||
|
||||
$executionData = $lasts[$job->getKey()]->getLastExecutionData();
|
||||
|
||||
$this->entityManager
|
||||
->createQuery(self::UPDATE_BEFORE_EXEC)
|
||||
->setParameters([
|
||||
@@ -96,12 +102,17 @@ class CronManager implements CronManagerInterface
|
||||
$execution = new CronJobExecution($job->getKey());
|
||||
$this->entityManager->persist($execution);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$executionData = $execution->getLastExecutionData();
|
||||
}
|
||||
$this->entityManager->clear();
|
||||
|
||||
// note: at this step, the entity manager does not have any entity CronJobExecution
|
||||
// into his internal memory
|
||||
|
||||
try {
|
||||
$this->logger->info(sprintf('%sWill run job', self::LOG_PREFIX), ['job' => $job->getKey()]);
|
||||
$job->run();
|
||||
$result = $job->run($executionData);
|
||||
|
||||
$this->entityManager
|
||||
->createQuery(self::UPDATE_AFTER_EXEC)
|
||||
@@ -112,6 +123,14 @@ class CronManager implements CronManagerInterface
|
||||
])
|
||||
->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()]);
|
||||
|
||||
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
|
||||
{
|
||||
@@ -174,7 +193,7 @@ class CronManager implements CronManagerInterface
|
||||
{
|
||||
foreach ($this->jobs as $job) {
|
||||
if ($job->getKey() === $forceJob) {
|
||||
$job->run();
|
||||
$job->run([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,7 +31,6 @@ class CronJobExecution
|
||||
private string $key;
|
||||
|
||||
/**
|
||||
* @var DateTimeImmutable
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
|
||||
*/
|
||||
private ?DateTimeImmutable $lastEnd = null;
|
||||
@@ -46,6 +45,11 @@ class CronJobExecution
|
||||
*/
|
||||
private ?int $lastStatus = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json", options={"default": "'{}'::jsonb", "jsonb": true})
|
||||
*/
|
||||
private array $lastExecutionData = [];
|
||||
|
||||
public function __construct(string $key)
|
||||
{
|
||||
$this->key = $key;
|
||||
@@ -92,4 +96,16 @@ class CronJobExecution
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastExecutionData(): array
|
||||
{
|
||||
return $this->lastExecutionData;
|
||||
}
|
||||
|
||||
public function setLastExecutionData(array $lastExecutionData): CronJobExecution
|
||||
{
|
||||
$this->lastExecutionData = $lastExecutionData;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form\Type\Listing;
|
||||
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
@@ -114,6 +115,28 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
|
||||
|
||||
$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
|
||||
|
@@ -66,6 +66,10 @@ export default {
|
||||
return appMessages.fr.the_activity;
|
||||
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
|
||||
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':
|
||||
return appMessages.fr.the_workflow;
|
||||
default:
|
||||
@@ -78,6 +82,10 @@ export default {
|
||||
return `/fr/activity/${n.relatedEntityId}/show`
|
||||
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
|
||||
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':
|
||||
return `/fr/main/workflow/${n.relatedEntityId}/show`
|
||||
default:
|
||||
|
@@ -46,6 +46,7 @@ const appMessages = {
|
||||
the_course: "le parcours",
|
||||
the_action: "l'action",
|
||||
the_evaluation: "l'évaluation",
|
||||
the_evaluation_document: "le document",
|
||||
the_task: "la tâche",
|
||||
the_workflow: "le workflow",
|
||||
StartDate: "Date d'ouverture",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<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>
|
||||
<a v-if="data.loading === false" @click.prevent="clickOrOpen" class="btn btn-misc address-details-button">
|
||||
<span class="fa fa-map"></span> <!-- 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-o"></span>
|
||||
</a>
|
||||
<span v-if="data.loading" class="fa fa-spin fa-spinner "></span>
|
||||
<AddressModal :address="data.working_address" @update-address="onUpdateAddress" ref="address_modal"></AddressModal>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<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">
|
||||
|
||||
|
@@ -18,6 +18,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if form.dateRanges is defined %}
|
||||
{% set btnSubmit = 1 %}
|
||||
{% if form.dateRanges|length > 0 %}
|
||||
@@ -40,6 +41,7 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if form.checkboxes is defined %}
|
||||
{% set btnSubmit = 1 %}
|
||||
{% if form.checkboxes|length > 0 %}
|
||||
@@ -56,6 +58,7 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if form.entity_choices is defined %}
|
||||
{% set btnSubmit = 1 %}
|
||||
{% if form.entity_choices |length > 0 %}
|
||||
@@ -74,6 +77,25 @@
|
||||
{% 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 %}
|
||||
@@ -91,8 +113,10 @@
|
||||
<button type="submit" class="btn btn-sm btn-misc"><i class="fa fa-fw fa-filter"></i>{{ 'Filter'|trans }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if active|length > 0 %}
|
||||
<div class="activeFilters mt-3">
|
||||
{% for f in active %}
|
||||
|
@@ -0,0 +1,145 @@
|
||||
<?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
|
||||
cma.addressreference_id != cmar.id
|
||||
-- only if cmpc is a reference (must be matched before executing this query)
|
||||
AND 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;
|
||||
|
||||
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
|
||||
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';
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
public function run(array $lastExecutionData): null|array
|
||||
{
|
||||
$this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit');
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
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;
|
||||
@@ -20,6 +21,7 @@ final readonly class FilterOrderGetActiveFilterHelper
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private PropertyAccessorInterface $propertyAccessor,
|
||||
private UserRender $userRender,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -73,6 +75,12 @@ final readonly class FilterOrderGetActiveFilterHelper
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
|
@@ -11,19 +11,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Templating\Listing;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\Listing\FilterOrderType;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessor;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\PropertyAccess\PropertyPath;
|
||||
use Symfony\Component\PropertyAccess\PropertyPathInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
|
||||
final class FilterOrderHelper
|
||||
{
|
||||
@@ -51,6 +46,10 @@ final class FilterOrderHelper
|
||||
*/
|
||||
private array $entityChoices = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array{label: string, options: array}>
|
||||
*/
|
||||
private array $userPickers = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly FormFactoryInterface $formFactory,
|
||||
@@ -80,6 +79,14 @@ final class FilterOrderHelper
|
||||
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) {
|
||||
@@ -114,6 +121,19 @@ final class FilterOrderHelper
|
||||
->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);
|
||||
@@ -203,7 +223,8 @@ final class FilterOrderHelper
|
||||
'checkboxes' => [],
|
||||
'dateRanges' => [],
|
||||
'single_checkboxes' => [],
|
||||
'entity_choices' => []
|
||||
'entity_choices' => [],
|
||||
'user_pickers' => []
|
||||
];
|
||||
|
||||
if ($this->hasSearchBox()) {
|
||||
@@ -227,6 +248,10 @@ final class FilterOrderHelper
|
||||
$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;
|
||||
}
|
||||
|
||||
|
@@ -39,6 +39,11 @@ class FilterOrderHelperBuilder
|
||||
*/
|
||||
private array $entityChoices = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array{label: string, options: array}>
|
||||
*/
|
||||
private array $userPickers = [];
|
||||
|
||||
public function __construct(
|
||||
FormFactoryInterface $formFactory,
|
||||
RequestStack $requestStack,
|
||||
@@ -85,6 +90,13 @@ class FilterOrderHelperBuilder
|
||||
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
|
||||
{
|
||||
$helper = new FilterOrderHelper(
|
||||
@@ -126,6 +138,17 @@ class FilterOrderHelperBuilder
|
||||
$helper->addDateRange($name, $label, $from, $to);
|
||||
}
|
||||
|
||||
|
||||
foreach (
|
||||
$this->userPickers as $name => [
|
||||
'label' => $label,
|
||||
'options' => $options
|
||||
]
|
||||
) {
|
||||
$helper->addUserPicker($name, $label, $options);
|
||||
}
|
||||
|
||||
|
||||
return $helper;
|
||||
}
|
||||
}
|
||||
|
@@ -18,4 +18,5 @@ enum FilterOrderPositionEnum: string
|
||||
case DateRange = 'date_range';
|
||||
case EntityChoice = 'entity_choice';
|
||||
case SingleCheckbox = 'single_checkbox';
|
||||
case UserPicker = 'user_picker';
|
||||
}
|
||||
|
@@ -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->getKey()->willReturn('to-exec');
|
||||
$jobToExecute->canRun(Argument::type(CronJobExecution::class))->willReturn(true);
|
||||
$jobToExecute->run()->shouldBeCalled();
|
||||
$jobToExecute->run([])->shouldBeCalled();
|
||||
|
||||
$executions = [
|
||||
['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');
|
||||
$jobNeverExecuted = $this->prophesize(CronJobInterface::class);
|
||||
$jobNeverExecuted->getKey()->willReturn('never-executed');
|
||||
$jobNeverExecuted->run()->shouldBeCalled();
|
||||
$jobNeverExecuted->run([])->shouldBeCalled();
|
||||
$jobNeverExecuted->canRun(null)->willReturn(true);
|
||||
|
||||
$executions = [
|
||||
@@ -86,7 +86,7 @@ final class CronManagerTest extends TestCase
|
||||
$jobAlreadyExecuted = new JobCanRun('k');
|
||||
$jobNeverExecuted = $this->prophesize(CronJobInterface::class);
|
||||
$jobNeverExecuted->getKey()->willReturn('never-executed');
|
||||
$jobNeverExecuted->run()->shouldBeCalled();
|
||||
$jobNeverExecuted->run([])->shouldBeCalled();
|
||||
$jobNeverExecuted->canRun(null)->willReturn(true);
|
||||
|
||||
$executions = [
|
||||
@@ -178,8 +178,9 @@ class JobCanRun implements CronJobInterface
|
||||
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';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
@@ -39,8 +39,10 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface
|
||||
return 'accompanying-period-step-change';
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
public function run(array $lastExecutionData): null|array
|
||||
{
|
||||
($this->requestor)();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -11,45 +11,39 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
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\AccompanyingPeriodWork;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
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(
|
||||
TranslatorInterface $trans,
|
||||
SerializerInterface $serializer,
|
||||
AccompanyingPeriodWorkRepository $workRepository,
|
||||
PaginatorFactory $paginator,
|
||||
LoggerInterface $chillLogger
|
||||
private readonly TranslatorInterface $trans,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly AccompanyingPeriodWorkRepository $workRepository,
|
||||
private readonly PaginatorFactory $paginator,
|
||||
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);
|
||||
|
||||
$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);
|
||||
$paginator = $this->paginator->create($totalItems);
|
||||
|
||||
$works = $this->workRepository->findByAccompanyingPeriodOpenFirst(
|
||||
$period,
|
||||
$filterData,
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
@@ -175,6 +179,7 @@ class AccompanyingCourseWorkController extends AbstractController
|
||||
'accompanyingCourse' => $period,
|
||||
'works' => $works,
|
||||
'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['id'] = $id;
|
||||
@@ -210,4 +215,26 @@ class AccompanyingCourseWorkController extends AbstractController
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->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()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@@ -95,29 +95,103 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
|
||||
* * then, closed works
|
||||
*
|
||||
* @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->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');
|
||||
|
||||
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
|
||||
WHERE accompanyingPeriod_id = :periodId
|
||||
ORDER BY
|
||||
CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
|
||||
startdate DESC,
|
||||
enddate DESC,
|
||||
id DESC
|
||||
LIMIT :limit OFFSET :offset";
|
||||
LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id
|
||||
WHERE accompanyingPeriod_id = :periodId";
|
||||
|
||||
// implement filters
|
||||
|
||||
if ([] !== ($filters['types'] ?? [])) {
|
||||
$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)
|
||||
->setParameter('periodId', $period->getId(), Types::INTEGER)
|
||||
->setParameter('types', $typeIds)
|
||||
->setParameter('after', $after)
|
||||
->setParameter('before', $before)
|
||||
->setParameter('limit', $limit, Types::INTEGER)
|
||||
->setParameter('offset', $offset, Types::INTEGER);
|
||||
|
||||
foreach ($filters['user'] as $key => $user) {
|
||||
$nq->setParameter('user_' . $key, $user);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
return $this->buildQueryNearEndDateByUser($user, $since, $until)
|
||||
|
@@ -68,27 +68,45 @@
|
||||
<ul class="list-content fa-ul">
|
||||
<li v-if="person.current_household_id">
|
||||
<i class="fa fa-li fa-map-marker"></i>
|
||||
<address-render-box v-if="person.current_household_address"
|
||||
:address="person.current_household_address"
|
||||
:isMultiline="isMultiline">
|
||||
</address-render-box>
|
||||
<p v-else class="chill-no-data-statement">
|
||||
{{ $t('renderbox.household_without_address') }}
|
||||
</p>
|
||||
<address-render-box v-if="person.current_household_address" :address="person.current_household_address" :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"
|
||||
:href="getCurrentHouseholdUrl"
|
||||
:title="$t('persons_associated.show_household_number', {id: person.current_household_id})">
|
||||
<span class="badge rounded-pill bg-chill-beige">
|
||||
<i class="fa fa-fw fa-home"></i><!--{{ $t('persons_associated.show_household') }}-->
|
||||
</span>
|
||||
<span class="badge rounded-pill bg-chill-beige">
|
||||
<i class="fa fa-fw fa-home"></i><!--{{ $t('persons_associated.show_household') }}-->
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-else-if="options.addNoData">
|
||||
<i class="fa fa-li fa-map-marker"></i>
|
||||
<p class="chill-no-data-statement">
|
||||
{{ $t('renderbox.no_data') }}
|
||||
</p>
|
||||
<i class="fa fa-li fa-map-marker"></i><p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
|
||||
</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">
|
||||
<i class="fa fa-li fa-envelope-o"></i>
|
||||
@@ -131,53 +149,6 @@
|
||||
</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>
|
||||
|
||||
<slot name="end-bloc"></slot>
|
||||
|
@@ -5,18 +5,23 @@
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="accompanying-course-work">
|
||||
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
{{ filter|chill_render_filter_order_helper }}
|
||||
|
||||
{% if works|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p>
|
||||
{% else %}
|
||||
|
@@ -59,7 +59,7 @@
|
||||
<span class=" d-block d-sm-inline-block">
|
||||
{{ address|chill_entity_render_box({
|
||||
'render': 'inline', 'multiline': false, 'with_picto': true, 'with_delimiter': true,
|
||||
'details_button': false
|
||||
'details_button': true
|
||||
}) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
@@ -56,13 +56,12 @@
|
||||
{%- if address is not null -%}
|
||||
{{ address|chill_entity_render_box({
|
||||
'render': 'inline', 'multiline': false, 'with_picto': true, 'with_delimiter': true,
|
||||
'details_button': false
|
||||
'details_button': true
|
||||
}) }}
|
||||
{%- endif -%}
|
||||
{% if person.getCurrentHousehold is not null %}
|
||||
<a class="btn household-link text-end"
|
||||
href="{{ chill_path_add_return_path('chill_person_household_summary', { 'household_id' : person.getCurrentHousehold.id } ) }}"
|
||||
title="{{ 'Show household'|trans }}">
|
||||
<a 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 }}">
|
||||
<i class="fa fa-lg fa-home"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@@ -47,8 +47,7 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
|
||||
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
|
||||
{
|
||||
return [
|
||||
'persons' => $this->getRelatedEntity($entityWorkflow)
|
||||
->getPersons(),
|
||||
'persons' => $this->getRelatedEntity($entityWorkflow)?->getPersons() ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -913,6 +913,9 @@ accompanying_course_work:
|
||||
social_evaluation: Évaluation
|
||||
private_comment: Commentaire privé
|
||||
timeSpent: Temps de rédaction
|
||||
date_filter: Filtrer par date
|
||||
types_filter: Filtrer par type d'action
|
||||
user_filter: Filtrer par intervenant
|
||||
|
||||
|
||||
#
|
||||
|
@@ -26,6 +26,7 @@ use Chill\TaskBundle\Event\TaskEvent;
|
||||
use Chill\TaskBundle\Event\UI\UIEvent;
|
||||
use Chill\TaskBundle\Form\SingleTaskType;
|
||||
use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface;
|
||||
use Chill\TaskBundle\Repository\SingleTaskStateRepository;
|
||||
use Chill\TaskBundle\Security\Authorization\TaskVoter;
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -71,7 +72,8 @@ final class SingleTaskController extends AbstractController
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
TimelineBuilder $timelineBuilder,
|
||||
LoggerInterface $logger,
|
||||
FilterOrderHelperFactoryInterface $filterOrderHelperFactory
|
||||
FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
|
||||
private SingleTaskStateRepository $singleTaskStateRepository
|
||||
) {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->timelineBuilder = $timelineBuilder;
|
||||
@@ -299,13 +301,17 @@ final class SingleTaskController extends AbstractController
|
||||
$this->denyAccessUnlessGranted(TaskVoter::SHOW, null);
|
||||
|
||||
$filterOrder = $this->buildFilterOrder();
|
||||
|
||||
$filteredUsers = $filterOrder->getUserPickerData('userPicker');
|
||||
|
||||
$flags = array_merge(
|
||||
$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(
|
||||
$filterOrder->getQueryString(),
|
||||
$flags
|
||||
$flags,
|
||||
$filteredUsers
|
||||
);
|
||||
$paginator = $this->paginatorFactory->create($nb);
|
||||
|
||||
@@ -313,6 +319,7 @@ final class SingleTaskController extends AbstractController
|
||||
$tasks = $this->singleTaskAclAwareRepository->findByAllViewable(
|
||||
$filterOrder->getQueryString(),
|
||||
$flags,
|
||||
$filteredUsers,
|
||||
$paginator->getCurrentPageFirstItemNumber(),
|
||||
$paginator->getItemsPerPage(),
|
||||
[
|
||||
@@ -346,7 +353,7 @@ final class SingleTaskController extends AbstractController
|
||||
$filterOrder = $this->buildFilterOrder();
|
||||
$flags = array_merge(
|
||||
$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(
|
||||
$course,
|
||||
@@ -395,7 +402,7 @@ final class SingleTaskController extends AbstractController
|
||||
$filterOrder = $this->buildFilterOrder();
|
||||
$flags = array_merge(
|
||||
$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(
|
||||
$person,
|
||||
@@ -447,10 +454,10 @@ final class SingleTaskController extends AbstractController
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
|
||||
$filterOrder = $this->buildFilterOrder();
|
||||
$filterOrder = $this->buildFilterOrder(false);
|
||||
$flags = array_merge(
|
||||
$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(
|
||||
$filterOrder->getQueryString(),
|
||||
@@ -662,7 +669,7 @@ final class SingleTaskController extends AbstractController
|
||||
return $form;
|
||||
}
|
||||
|
||||
private function buildFilterOrder(): FilterOrderHelper
|
||||
private function buildFilterOrder($includeFilterByUser = true): FilterOrderHelper
|
||||
{
|
||||
$statuses = ['no-alert', 'warning', 'alert'];
|
||||
$statusTrans = [
|
||||
@@ -670,17 +677,26 @@ final class SingleTaskController extends AbstractController
|
||||
'Tasks near 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)
|
||||
->addSearchBox()
|
||||
->addCheckbox('status', $statuses, $statuses, $statusTrans)
|
||||
->addCheckbox('states', $states, ['new', 'in_progress'])
|
||||
->build();
|
||||
->addCheckbox('status', $statuses, $statuses, $statusTrans);
|
||||
|
||||
$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(
|
||||
?string $pattern = null,
|
||||
?array $flags = []
|
||||
?array $flags = [],
|
||||
?array $users = []
|
||||
): QueryBuilder {
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb
|
||||
->from(SingleTask::class, 't');
|
||||
|
||||
if (!empty($pattern)) {
|
||||
if (null !== $pattern && '' !== $pattern) {
|
||||
$qb->andWhere($qb->expr()->like('LOWER(UNACCENT(t.title))', 'LOWER(UNACCENT(: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();
|
||||
$orXState = $qb->expr()->orX();
|
||||
$now = new DateTime();
|
||||
@@ -183,9 +202,10 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
|
||||
|
||||
public function countByAllViewable(
|
||||
?string $pattern = null,
|
||||
?array $flags = []
|
||||
?array $flags = [],
|
||||
?array $users = []
|
||||
): int {
|
||||
$qb = $this->buildBaseQuery($pattern, $flags);
|
||||
$qb = $this->buildBaseQuery($pattern, $flags, $users);
|
||||
|
||||
return $this
|
||||
->addACLGlobal($qb)
|
||||
@@ -231,11 +251,12 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository
|
||||
public function findByAllViewable(
|
||||
?string $pattern = null,
|
||||
?array $flags = [],
|
||||
?array $users = [],
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?array $orderBy = []
|
||||
): array {
|
||||
$qb = $this->buildBaseQuery($pattern, $flags);
|
||||
$qb = $this->buildBaseQuery($pattern, $flags, $users);
|
||||
$qb = $this->addACLGlobal($qb);
|
||||
|
||||
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||
|
@@ -18,7 +18,8 @@ interface SingleTaskAclAwareRepositoryInterface
|
||||
{
|
||||
public function countByAllViewable(
|
||||
?string $pattern = null,
|
||||
?array $flags = []
|
||||
?array $flags = [],
|
||||
?array $users = []
|
||||
): int;
|
||||
|
||||
public function countByCourse(
|
||||
@@ -38,6 +39,7 @@ interface SingleTaskAclAwareRepositoryInterface
|
||||
public function findByAllViewable(
|
||||
?string $pattern = null,
|
||||
?array $flags = [],
|
||||
?array $users = [],
|
||||
?int $start = 0,
|
||||
?int $limit = 50,
|
||||
?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 %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('page_task_list') }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('page_task_list') }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
@@ -1,4 +1,8 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
chill_task.single_task_repository:
|
||||
class: Chill\TaskBundle\Repository\SingleTaskRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
@@ -10,8 +14,8 @@ services:
|
||||
- "@chill.main.security.authorization.helper"
|
||||
Chill\TaskBundle\Repository\SingleTaskRepository: '@chill_task.single_task_repository'
|
||||
|
||||
Chill\TaskBundle\Repository\SingleTaskAclAwareRepository:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
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
|
||||
By: Par
|
||||
Any tasks: Aucune tâche
|
||||
Filter by user: Filtrer par utilisateur(s)
|
||||
|
||||
# transitions - default task definition
|
||||
"new": "nouvelle"
|
||||
|
@@ -71,12 +71,9 @@ class ThirdPartyApiSearch implements SearchApiInterface
|
||||
->setSelectKey('tparty')
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)")
|
||||
->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_main_address cma_p ON parent.address_id = cma_p.id
|
||||
LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id')
|
||||
->andWhereClause('tparty.active IS TRUE');
|
||||
LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id)
|
||||
LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id');
|
||||
|
||||
$strs = explode(' ', $pattern);
|
||||
$wheres = [];
|
||||
@@ -102,9 +99,8 @@ class ThirdPartyApiSearch implements SearchApiInterface
|
||||
(parent.canonicalized LIKE '%s' || LOWER(UNACCENT(?)) || '%')::int
|
||||
) + " .
|
||||
// take postcode label into account, but lower than the canonicalized field
|
||||
"COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0) + " .
|
||||
"COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)";
|
||||
$pertinenceArgs[] = [$str, $str, $str, $str, $str, $str];
|
||||
"COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)";
|
||||
$pertinenceArgs[] = [$str, $str, $str, $str, $str];
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user