Compare commits

...

219 Commits

Author SHA1 Message Date
96d302ccf7 quick fixes: birthdate make accompaying course form dans les choux 2021-07-01 09:13:00 +02:00
nobohan
0b5daa6c33 quick fix: address: add backurl after posting the address 2021-06-30 18:26:44 +02:00
662e5f967c Merge branch 'frontend/address-to-add-persons-widget' into 'master'
Add address to widget add persons

See merge request Chill-Projet/chill-bundles!105
2021-06-30 07:12:42 +00:00
802125edc6 Add address to widget add persons 2021-06-30 07:50:43 +02:00
028c3e0c31 widget addPersons: fix persons without birthdate 2021-06-29 22:40:23 +02:00
e4adafafc8 remove household search 2021-06-29 22:38:01 +02:00
54316d148c Merge branch 'features/improve-activity' into 'master'
Features/improve activity

See merge request Chill-Projet/chill-bundles!103
2021-06-29 19:18:51 +00:00
3c5a2a65df Merge branch 'features/improve-accompanying-course-summary' into 'master'
Improve accompanying course summary page

See merge request Chill-Projet/chill-bundles!102
2021-06-29 19:16:18 +00:00
741ef7b40d Merge branch 'features/add-suggestion-household-member-editor' into 'master'
Add suggestion household member editor

See merge request Chill-Projet/chill-bundles!97
2021-06-29 19:13:38 +00:00
e7384343b2 Merge remote-tracking branch 'origin/master' into features/improve-accompanying-course-summary 2021-06-29 21:12:21 +02:00
1e2a920200 clean menu builder for activity 2021-06-29 21:03:38 +02:00
b5a78fa341 Merge remote-tracking branch 'origin/master' into features/improve-activity 2021-06-29 20:52:06 +02:00
d9c1f52894 improve activity 2021-06-29 20:51:08 +02:00
nobohan
f05f7415bd household: add household accompanying periods 2021-06-29 19:41:00 +02:00
nobohan
5553c7314c household address: fix conflict on branch merge 2021-06-29 17:47:40 +02:00
7dc70baf57 permet de surcharger le label 'commentaire' des actviités 2021-06-29 17:39:19 +02:00
nobohan
34af401a98 accompanying period: add items in the summary page 2021-06-29 17:37:58 +02:00
5bae8dc5bf Import also scripts tags & css for launching banner 2021-06-29 17:13:57 +02:00
6e277e99ae fix in accompanying period list: thirdparty as requestor + link 2021-06-29 17:09:55 +02:00
2688c0f0ce fix missing div in accompanying course work list 2021-06-29 17:09:38 +02:00
nobohan
0135f5184e accompanying period: wording + minor changes in twig 2021-06-29 17:09:24 +02:00
fd698419e8 Merge branch 'fix-prototypage-details' into 'master'
Fix prototypage details

See merge request Chill-Projet/chill-bundles!94
2021-06-29 15:06:20 +00:00
4a864a0b14 Merge remote-tracking branch 'origin/master' into features/improve-accompanying-course-summary 2021-06-29 16:59:05 +02:00
55dc0d3f9f Ajout informations manquantes dans la page "summary" des
AccompanyingCourse
2021-06-29 16:57:28 +02:00
85835c8b0d Merge branch 'features/api-search' into 'master'
Features/api search

See merge request Chill-Projet/chill-bundles!98
2021-06-29 14:50:28 +00:00
nobohan
ab1edee9fb person menu: add style for submenus 2021-06-29 16:22:58 +02:00
6154e0912b fix in accompanying period list: thirdparty as requestor + link 2021-06-29 16:14:50 +02:00
0388428c6f fix missing div in accompanying course work list 2021-06-29 16:14:30 +02:00
7ac55275c2 Merge remote-tracking branch 'origin/fix_issues_juminet' into features/improve-accompanying-course-summary 2021-06-29 16:06:57 +02:00
1119e20a49 Add minor fixes to summary page:
* add messages when no resources
* add address to person
* replace household link label with household number
2021-06-29 16:03:59 +02:00
ea2870eef8 Merge branch 'features/create-user-endpoint' into 'master'
Feature: create user endpoint

See merge request Chill-Projet/chill-bundles!96
2021-06-29 14:02:16 +00:00
nobohan
93a44d8303 address: automatic return when saving address 2021-06-29 15:06:35 +02:00
nobohan
8109415e05 address: wording button 2021-06-29 14:39:43 +02:00
nobohan
514fe6163a address: format address as the French format 2021-06-29 14:33:51 +02:00
9d9be7f831 vue activity: fix backend when hidden return empty string 2021-06-29 14:31:56 +02:00
nobohan
4cf3cc8353 address: add focus on selectAddress field 2021-06-29 14:25:26 +02:00
2a974e1269 fix socialActions selected visibility if all issues unchecked + cleaning 2021-06-29 12:23:11 +02:00
beca41774e vue activity: fix duplicates and add condition in initial loading 2021-06-29 11:49:23 +02:00
dd991e3572 add test 2021-06-28 23:02:15 +02:00
0640631821 implementation on 3party + quote keys in SearchApiQuery 2021-06-28 22:44:42 +02:00
e845d9ba90 replace search api by a first workin implementation 2021-06-28 22:01:33 +02:00
nobohan
6f8231f6f6 accompanying course: clean controller 2021-06-28 22:01:05 +02:00
nobohan
048161e300 accompanying course summary (WIP) 2021-06-28 22:00:07 +02:00
b705c5910f dispatch action change value of hidden fields, persisted when submited 2021-06-28 20:46:30 +02:00
6f1209eaf5 rename methods, improve logic 2021-06-28 20:20:54 +02:00
020759180b prepare dispatch 2021-06-28 20:07:53 +02:00
3748b4fbf4 vue activity: manage datas in store, not with data() 2021-06-28 19:05:26 +02:00
nobohan
4f49292178 address: fix saving of isNoAddress 2021-06-28 17:18:57 +02:00
6eaffcae49 fix redirection after changing household member 2021-06-28 16:22:17 +02:00
nobohan
f92d710a26 accompanying course summary: add social actions 2021-06-28 16:16:55 +02:00
nobohan
bc6ba88acd menu: fix code indentation 2021-06-28 16:04:24 +02:00
61c2934d64 activity: get social Actions, improve reactivity 2021-06-28 15:31:41 +02:00
28b4d9562c add socialAction checkbox list 2021-06-28 15:21:41 +02:00
e06aded1ec remove from multiselect when adding social issues + disable accompanyingcourse issues loading 2021-06-28 15:21:32 +02:00
3a81124e04 activity vue: list socialIssues checkbox from accompanyingCourse 2021-06-28 15:21:17 +02:00
nobohan
b72d45d9db accompanying period: hide activity entries from the menu if accompanyin period is DRAFT 2021-06-28 13:50:31 +02:00
2a1f5cbad1 add test for suggestion household by accompanying period 2021-06-28 13:18:00 +02:00
e8566fd006 change label 2021-06-28 12:36:19 +02:00
cd6b5c9a39 Add context for household member editor form:
* add context for accompanying period, if indicated in a query
parameter;
* expand automatically household suggestion, if indicated in a query
parameter
2021-06-28 11:23:30 +02:00
9aa3974071 show household suggestion in household members editor 2021-06-28 00:35:40 +02:00
a8bf478ee8 add endpoint for getting suggestion on household by accompanying period 2021-06-27 11:10:17 +02:00
a35f3363b2 Merge branch 'features/edit-accompanying-period-social-work' into 'master'
Features/edit accompanying period social work

See merge request Chill-Projet/chill-bundles!91
2021-06-26 13:15:34 +00:00
0b117e5158 Merge remote-tracking branch 'origin/features/edit-accompanying-period-social-work' into _vue_echanges 2021-06-26 12:14:32 +02:00
7cae9e1769 wip 2021-06-26 12:09:18 +02:00
a43e6c12a0 add tests for UserApiController 2021-06-26 11:08:26 +02:00
abe012de60 add endpoint whoami 2021-06-26 11:08:10 +02:00
aaa9a1ec7b create user _index and _entity endpoint 2021-06-26 11:07:03 +02:00
25f2d17097 documentation for api social actions 2021-06-25 21:34:36 +02:00
4fd43310c0 Merge remote-tracking branch 'origin/master' into features/edit-accompanying-period-social-work 2021-06-25 21:26:21 +02:00
c85ca894b8 layout for list accompanying work 2021-06-25 21:25:27 +02:00
fe22e6c7dc activity: create new vue subcomponent socialIssuesAcc 2021-06-25 15:44:20 +02:00
cb67605bd2 list accompanying course work 2021-06-25 14:59:10 +02:00
d26e08324c load banner accompanying course everywhere 2021-06-25 14:09:47 +02:00
e55a2691b7 move vue activity component app in subcomponent concernedGroups 2021-06-25 14:09:32 +02:00
nobohan
aa5c7f333e fix conflict + error on CommentType 2021-06-25 13:57:49 +02:00
0afe277b04 form acc work edit: save action 2021-06-25 00:06:26 +02:00
981b9299ad layout form 2021-06-24 16:21:52 +02:00
nobohan
36eb247995 address: add maxlength, upd default map center and add loader messages in addresses form 2021-06-24 16:02:57 +02:00
2cda6ceebf improve history details 2021-06-24 15:43:02 +02:00
1ceabec400 redesign timeline with css gridlayout 2021-06-24 15:17:34 +02:00
nobohan
91c5cd74f7 household address: remove group write on member collection 2021-06-24 14:01:15 +02:00
nobohan
b7f2e189dc small fix: use class name instead of string 2021-06-24 14:01:15 +02:00
nobohan
ab8deb036b accompanyingCourse: add activities in the accompanying course summary 2021-06-24 14:01:15 +02:00
nobohan
ac47a75a75 fix bug issue 163 Address JSON string not valid 2021-06-24 14:01:15 +02:00
nobohan
4e6454d7de translation Activity Envoyer/recevoir 2021-06-24 14:01:15 +02:00
92a95d4186 finish form behaviour 2021-06-24 12:44:24 +02:00
nobohan
c23ada3533 person: add show/hide on marital status date 2021-06-24 08:25:55 +02:00
b72b1bd4c7 Merge remote-tracking branch 'origin/master' into features/edit-accompanying-period-social-work 2021-06-23 22:48:09 +02:00
ca9ae3874c Merge branch 'fix/api-crud-improve-loading-for-existing-controller' into 'master'
Fix/api crud improve loading for existing controller

See merge request Chill-Projet/chill-bundles!92
2021-06-23 20:44:25 +00:00
7fb4616aa6 Merge branch 'features/create-social-action' into 'master'
Create social work

See merge request Chill-Projet/chill-bundles!90
2021-06-23 20:43:32 +00:00
ac4cf43753 add goals to work, and add results for each goal 2021-06-23 22:40:11 +02:00
5a4a0a3617 add results for actions in course edit form 2021-06-23 21:26:36 +02:00
2cc1274ee0 fix js alert with dom event listener 2021-06-23 18:04:06 +02:00
154fa4719d add involved persons to accompanying period work 2021-06-23 17:46:51 +02:00
193e7b4c7e cleaning 2021-06-23 13:20:50 +02:00
80c2bd285f vue component html anchor place 2021-06-23 11:50:09 +02:00
133c0ac1ac TOFIX temporary disable js alert that return an error:
## in JS console:

Uncaught ReferenceError: chill is not defined
    <anonymous> http://localhost:8001/fr/activity/1808/edit?person_id=1453:317
2021-06-23 11:25:54 +02:00
c8d5c2c67b resume interlocutors 3 columns 2021-06-23 09:31:26 +02:00
0754d20622 fix form create + WIP form edit for Accompanying Period Work 2021-06-22 22:59:34 +02:00
99d6e030ca ergonomic buttons details 2021-06-22 16:15:05 +02:00
5d38d193e2 add type of activity in title 2021-06-22 16:14:29 +02:00
86f7b116a9 adapt buttons when choosing type of ativity 2021-06-22 16:12:30 +02:00
bcab5a1211 remove old webpack entrypoint definitions 2021-06-22 16:11:24 +02:00
65a6607112 fix translations 2021-06-22 16:10:17 +02:00
1cd376bf86 Merge remote-tracking branch 'origin/master' into features/edit-accompanying-period-social-work 2021-06-22 16:09:58 +02:00
84f6f9b44d fix bug with new activity in context person 2021-06-22 16:09:20 +02:00
b53f6af8d8 add api for getting goal and results by social action and goal 2021-06-22 16:08:53 +02:00
34d6452286 fix: household api 2021-06-22 15:31:56 +02:00
c04ccef4cd improve loading for existing crud cotnroller 2021-06-22 14:40:56 +02:00
nobohan
d8afbc4a40 fix bug on PostalCodeChoiceLoader (not understood) 2021-06-22 11:56:25 +02:00
95edbc673c improve loading for crud controller 2021-06-22 10:08:19 +02:00
07ffdfab86 household banner context colors 2021-06-21 15:58:27 +02:00
50be29308d Merge remote-tracking branch 'origin/master' into features/create-social-action 2021-06-21 14:38:09 +02:00
d1e0d997ac Merge branch 'features/household-edit-members-forms-improve-household' into 'master'
Corrections ménage

See merge request Chill-Projet/chill-bundles!88
2021-06-21 12:25:22 +00:00
nobohan
504b2efacf person: fix death date type as date immutable 2021-06-21 13:58:14 +02:00
4c3c67344a fix merge old css styles 2021-06-21 13:43:15 +02:00
40fcb09082 layout for form 2021-06-21 13:38:43 +02:00
aa4a9e874a add a spinner 2021-06-19 11:15:27 +02:00
3abfdbf6fd first impl for create form 2021-06-18 19:41:58 +02:00
07cc394abd [bugfix] fix font in element ::before
See https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/162

[ci skip]
2021-06-18 14:24:53 +02:00
99337c4aa6 add form edit metadata in summary page 2021-06-18 14:11:02 +02:00
04ccca092b remove links to route 'household members' 2021-06-18 13:53:19 +02:00
nobohan
b3792f6714 person: update person view/show twig 2021-06-18 13:46:46 +02:00
e1f01a47f8 Merge remote-tracking branch 'origin/master' into features/household-edit-members-forms-improve-household 2021-06-18 13:42:37 +02:00
dc79e1a02d remove test for members page 2021-06-18 13:31:19 +02:00
7a6117a264 fix test which randomly failed 2021-06-18 13:30:31 +02:00
nobohan
1a204312f3 person: re-enable the creation of a person 2021-06-18 13:12:27 +02:00
1b1a25edc4 Merge branch 'features/sql-vue-from-household-address-to-person' into 'master'
Features/sql vue from household address to person

See merge request Chill-Projet/chill-bundles!86
2021-06-18 11:01:40 +00:00
5ab7f7a37b Merge remote-tracking branch 'origin/master' into features/sql-vue-from-household-address-to-person 2021-06-18 12:08:13 +02:00
b174397823 Merge branch 'features/household-validation' into 'master'
Validation for household and household members & list for household for a person

See merge request Chill-Projet/chill-bundles!79
2021-06-18 10:07:10 +00:00
9d35e4efbb Merge remote-tracking branch 'origin/master' into features/household-validation 2021-06-18 12:02:34 +02:00
0e9933ca41 improve banner 2021-06-18 11:57:27 +02:00
nobohan
3d14f00cac person: add show hide on some fields + remove unknown genderType 2021-06-18 11:28:02 +02:00
0add757230 remove members page, move to summary 2021-06-18 11:27:25 +02:00
2bf65dfadb Merge branch 'fix-test-accompanying-period' into 'master'
fix data provider which randomly generate invalid data in test

See merge request Chill-Projet/chill-bundles!87
2021-06-18 09:19:29 +00:00
a7ecd08d78 fix data provider which randomly generate invalid data in test 2021-06-18 09:19:28 +00:00
nobohan
bccaf5ad6e person: add createdBY, updatedBy fields on Person + migration 2021-06-18 10:15:30 +02:00
585a19e803 Apply 7 suggestion(s) to 1 file(s) 2021-06-18 07:17:32 +00:00
b801b75eb9 Merge remote-tracking branch 'origin/master' into features/sql-vue-from-household-address-to-person 2021-06-17 23:16:57 +02:00
f1120af59e add 'holder' in list of persons 2021-06-17 23:10:18 +02:00
9d58356b90 remove drag-and-drop behaviour 2021-06-17 22:54:46 +02:00
a7131653c9 finish merge (oups) + layout of household in members editor app 2021-06-17 22:47:33 +02:00
9f3cd943cb Merge branch 'features/household-validation' into features/household-edit-members-forms-improve-household 2021-06-17 22:46:47 +02:00
nobohan
970635dfad Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles
merge...
2021-06-17 17:03:42 +02:00
nobohan
c8297b2230 fix conflict when merging _77_address_household into master 2021-06-17 16:59:55 +02:00
994cce41e5 Merge branch 'master' into features/household-edit-members-forms-improve-household 2021-06-17 15:41:00 +02:00
86e41fb877 Merge remote-tracking branch 'origin/_77_address_household' into features/household-edit-members-forms-improve-household 2021-06-17 15:05:46 +02:00
f8b97b96f4 do not use arrayAccess for Collection in validator 2021-06-17 14:57:16 +02:00
27907e7558 fix modification of key members in household normalization 2021-06-17 14:02:29 +02:00
fd7c7388d9 fix prepending configuration of doctrine 2021-06-17 13:31:04 +02:00
ef55d2cf7f create view for current address and apply on Person/Household normalizer 2021-06-17 13:21:55 +02:00
nobohan
8406c30c8e person: Person FormType + edit twig 2021-06-17 12:27:47 +02:00
nobohan
901ae47ce6 person: correct migration + entity 2021-06-17 11:12:40 +02:00
nobohan
951160982d person: add more fields on Person + migration 2021-06-17 10:43:57 +02:00
nobohan
e2633a2a79 addresses: filter out postal code introduced by user 2021-06-16 16:20:37 +02:00
nobohan
5b72eeb147 addresses: edit address for household (WIP) 2021-06-16 15:05:32 +02:00
nobohan
06daf35e96 addresses: edit address for person 2021-06-16 14:39:02 +02:00
38bff2e42f add address to household in fixtures 2021-06-16 08:35:47 +02:00
e68a5405a1 Merge branch 'master' into features/sql-vue-from-household-address-to-person 2021-06-15 22:53:21 +02:00
8bd20c9c78 [WIP] add fixtures for household address 2021-06-15 22:52:48 +02:00
54997e5893 add test for household membership sequential validator 2021-06-15 21:31:02 +02:00
nobohan
35d35c9f40 addresses: edit address (WIP) 2021-06-15 21:03:50 +02:00
78ca61c82e fix showing person without birthdate 2021-06-15 17:22:03 +02:00
b68de1f2e9 fix parameters in list 2021-06-15 17:16:35 +02:00
68ecff253a fix validation when leaving any household 2021-06-15 17:15:45 +02:00
e95d8fbc7a Historique ménage pour une personne 2021-06-15 17:03:38 +02:00
efdfd10e49 replace comment by commentEmbeddable 2021-06-15 10:17:53 +02:00
nobohan
000ae6c2cb address: style (css) 2021-06-15 08:04:38 +02:00
nobohan
7c26f0a56c address: add a new address to a person 2021-06-15 07:49:35 +02:00
f827e50431 ajout métadonnées membres 2021-06-14 22:30:52 +02:00
41617295c1 fix validation groups and sequential validator msg 2021-06-14 16:39:27 +02:00
b2c1d75fc5 fix parameters in test MaxHolder 2021-06-14 12:58:23 +02:00
17c3ecbabe validation on simultaneous household membership for a person 2021-06-14 12:56:01 +02:00
51399b21b9 fix class for Person component 2021-06-14 11:37:20 +02:00
c40019da8f add alert for not-in-household and actions in page
accompanyingCourse/summary

* alert for people not in household for each accompanying course;
* style for action in alert
* form to members editor
2021-06-14 11:34:05 +02:00
1df759e970 fix JS error "chill is not defined"
This happens since js is loaded using "defer" tag
2021-06-14 10:19:18 +02:00
807d3674fc implements max holder and validation on UI 2021-06-11 17:58:09 +02:00
af740fd87d Merge branch 'features/household-editor' into features/household-validation 2021-06-11 17:08:38 +02:00
45dc8ed661 apply maxHolder constraint on houehold 2021-06-11 17:08:35 +02:00
ecc8b929ca create max holder validator 2021-06-11 17:03:48 +02:00
nobohan
c378f59f5a addresses: person address history 2021-06-11 16:20:40 +02:00
4fd6d38187 create Util for computing intersection 2021-06-11 15:53:32 +02:00
nobohan
f3a7556bca household address: update twig for household address 2021-06-11 15:44:49 +02:00
nobohan
14db7265fe addresses: fix POST if isNoAddress 2021-06-11 15:06:56 +02:00
nobohan
f22461af1b address: better default zoom when choosing an address 2021-06-11 14:42:08 +02:00
nobohan
1b709d39a4 addresses: enable POSTing Postal Code entities (back + front) 2021-06-11 14:33:22 +02:00
nobohan
5daf5cbe84 address: write new address (separate street and streetNumber) 2021-06-11 11:12:44 +02:00
nobohan
331cdf13ca household address: add validation on household->getAddresses 2021-06-11 09:34:27 +02:00
cbadcb4980 Merge remote-tracking branch 'origin/master' into features/household-validation 2021-06-11 09:05:48 +02:00
nobohan
1551ea796d household address: UI of the adress move form 2021-06-10 14:34:14 +02:00
nobohan
4c025184b4 household address: check if address are sequential (WIP) 2021-06-10 12:40:01 +02:00
nobohan
6f2c219e3b household address: add validTo computation in Address 2021-06-10 11:43:13 +02:00
nobohan
550aaacaab household address: fix POST api chaining 2021-06-10 11:42:39 +02:00
nobohan
dd78e9759c household address: show js error messages 2021-06-10 10:11:00 +02:00
nobohan
1b36d9b1ab household address: chain API call 2021-06-10 10:06:23 +02:00
nobohan
e14a125bf3 household address: format js date for POSTing 2021-06-09 12:24:15 +02:00
nobohan
9375c2cf86 household address: patch address + add validFrom input 2021-06-08 21:14:09 +02:00
nobohan
db77224b9f address: add swagger documentation 2021-06-08 20:36:20 +02:00
0e1bbbfee9 Merge remote-tracking branch 'origin/master' into features/household-validation 2021-06-08 19:19:30 +02:00
nobohan
6a42edd460 household address: remove address selection 2021-06-08 17:06:03 +02:00
nobohan
76d945f2a7 household address: address history : add button 2021-06-08 11:51:30 +02:00
nobohan
7d13bc7cfe household address: address history (twig + css) 2021-06-08 11:35:33 +02:00
nobohan
66b54bad31 household address: add swagger documentation 2021-06-08 08:55:41 +02:00
nobohan
9d32ccbcad address: swagger config for documenting API points 2021-06-08 08:30:54 +02:00
nobohan
63c3b5a970 address: add swagger doc on API point for address 2021-06-07 17:24:31 +02:00
nobohan
03a7ec389b household address: fix POST household address 2021-06-07 16:54:16 +02:00
nobohan
f39fe05dd5 household address: rely on ROLE_USER for household address api point role 2021-06-07 15:26:24 +02:00
nobohan
6c126b0581 household address: api POST on address for household (WIP) 2021-06-07 15:13:45 +02:00
nobohan
5ee824d1c2 household address: define a custom household-address API point (WIP) 2021-06-07 14:47:22 +02:00
nobohan
6d20476b81 household address: household address history (WIP) 2021-06-07 14:24:03 +02:00
nobohan
2b5ed5e5a3 household address: choose among available address 2021-06-07 12:18:03 +02:00
nobohan
6f68349f57 household address: post address to household (WIP) 2021-06-04 17:45:29 +02:00
nobohan
d513be2fce household address: post address to household (WIP) 2021-06-04 16:23:13 +02:00
nobohan
aff140230c household address: add a button for posting to household 2021-06-04 15:38:28 +02:00
nobohan
7e1b7b7e9f household address: init vue component for household address 2021-06-04 14:50:51 +02:00
nobohan
878ee5d9c7 household address: correct household api entry point 2021-06-04 12:34:44 +02:00
nobohan
7b67ef5db2 household address: add household api entry point 2021-06-04 12:13:15 +02:00
nobohan
d806a074a7 Merge branch 'master' into _77_address_household
merge master
2021-06-04 09:09:59 +02:00
nobohan
14a9d9c739 address-household: address move (WIP) 2021-06-04 08:48:08 +02:00
221 changed files with 10963 additions and 1467 deletions

View File

@@ -72,6 +72,7 @@ class ActivityController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$view = null;
// TODO: add pagination
[$person, $accompanyingPeriod] = $this->getEntity($request);
@@ -80,10 +81,9 @@ class ActivityController extends AbstractController
->getReachableCircles($this->getUser(), new Role('CHILL_ACTIVITY_SEE'),
$person->getCenter());
$activities = $em->getRepository('ChillActivityBundle:Activity')->findBy(
['person' => $person, 'scope' => $reachableScopes],
['date' => 'DESC'],
);
$activities = $em->getRepository(Activity::class)
->findByPersonImplied($person, $reachableScopes)
;
$event = new PrivacyEvent($person, array(
'element_class' => Activity::class,

View File

@@ -106,15 +106,16 @@ class Activity implements HasCenterInterface, HasScopeInterface
/**
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\SocialWork\SocialIssue")
* @ORM\JoinTable(name="chill_activity_activity_chill_person_socialissue")
* @Groups({"read"})
*/
private $socialIssues;
private Collection $socialIssues;
/**
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\SocialWork\SocialAction")
* @ORM\JoinTable(name="chill_activity_activity_chill_person_socialaction")
* @Groups({"read"})
*/
private $socialActions;
private Collection $socialActions;
/**
* @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\ActivityType")
@@ -281,7 +282,7 @@ class Activity implements HasCenterInterface, HasScopeInterface
return $this->socialIssues;
}
public function addSocialIssue(SocialIssue $socialIssue): self
public function addSocialIssue(?SocialIssue $socialIssue): self
{
if (!$this->socialIssues->contains($socialIssue)) {
$this->socialIssues[] = $socialIssue;
@@ -297,15 +298,12 @@ class Activity implements HasCenterInterface, HasScopeInterface
return $this;
}
/**
* @return Collection|SocialAction[]
*/
public function getSocialActions(): Collection
{
return $this->socialActions;
}
public function addSocialAction(SocialAction $socialAction): self
public function addSocialAction(?SocialAction $socialAction): self
{
if (!$this->socialActions->contains($socialAction)) {
$this->socialActions[] = $socialAction;

View File

@@ -35,6 +35,7 @@ use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\CallbackTransformer;
use Chill\PersonBundle\Form\DataTransformer\PersonToIdTransformer;
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
use Chill\PersonBundle\Templating\Entity\SocialActionRender;
class ActivityType extends AbstractType
{
@@ -46,6 +47,10 @@ class ActivityType extends AbstractType
protected TranslatableStringHelper $translatableStringHelper;
protected SocialIssueRender $socialIssueRender;
protected SocialActionRender $socialActionRender;
protected array $timeChoices;
public function __construct (
@@ -54,7 +59,8 @@ class ActivityType extends AbstractType
ObjectManager $om,
TranslatableStringHelper $translatableStringHelper,
array $timeChoices,
SocialIssueRender $socialIssueRender
SocialIssueRender $socialIssueRender,
SocialActionRender $socialActionRender
) {
if (!$tokenStorage->getToken()->getUser() instanceof User) {
throw new \RuntimeException("you should have a valid user");
@@ -66,6 +72,7 @@ class ActivityType extends AbstractType
$this->translatableStringHelper = $translatableStringHelper;
$this->timeChoices = $timeChoices;
$this->socialIssueRender = $socialIssueRender;
$this->socialActionRender = $socialActionRender;
}
public function buildForm(FormBuilderInterface $builder, array $options): void
@@ -105,31 +112,51 @@ class ActivityType extends AbstractType
}
if ($activityType->isVisible('socialIssues') && $accompanyingPeriod) {
$builder->add('socialIssues', EntityType::class, [
'label' => $activityType->getLabel('socialIssues'),
'required' => $activityType->isRequired('socialIssues'),
'class' => SocialIssue::class,
'choice_label' => function (SocialIssue $socialIssue) {
return $this->socialIssueRender->renderString($socialIssue, []);
},
'multiple' => true,
'choices' => $accompanyingPeriod->getRecursiveSocialIssues(),
'expanded' => true,
]);
$builder->add('socialIssues', HiddenType::class);
$builder->get('socialIssues')
->addModelTransformer(new CallbackTransformer(
function (iterable $socialIssuesAsIterable): string {
$socialIssueIds = [];
foreach ($socialIssuesAsIterable as $value) {
$socialIssueIds[] = $value->getId();
}
return implode(',', $socialIssueIds);
},
function (?string $socialIssuesAsString): array {
if (null === $socialIssuesAsString) {
return [];
}
return array_map(
fn(string $id): ?SocialIssue => $this->om->getRepository(SocialIssue::class)->findOneBy(['id' => (int) $id]),
explode(',', $socialIssuesAsString)
);
}
))
;
}
if ($activityType->isVisible('socialActions') && $accompanyingPeriod) {
$builder->add('socialActions', EntityType::class, [
'label' => $activityType->getLabel('socialActions'),
'required' => $activityType->isRequired('socialActions'),
'class' => SocialAction::class,
'choice_label' => function (SocialAction $socialAction) {
return $this->translatableStringHelper->localize($socialAction->getTitle());
},
'multiple' => true,
'choices' => $accompanyingPeriod->getRecursiveSocialActions(),
'expanded' => true,
]);
$builder->add('socialActions', HiddenType::class);
$builder->get('socialActions')
->addModelTransformer(new CallbackTransformer(
function (iterable $socialActionsAsIterable): string {
$socialActionIds = [];
foreach ($socialActionsAsIterable as $value) {
$socialActionIds[] = $value->getId();
}
return implode(',', $socialActionIds);
},
function (?string $socialActionsAsString): array {
if (null === $socialActionsAsString) {
return [];
}
return array_map(
fn(string $id): ?SocialAction => $this->om->getRepository(SocialAction::class)->findOneBy(['id' => (int) $id]),
explode(',', $socialActionsAsString)
);
}
))
;
}
if ($activityType->isVisible('date')) {
@@ -197,15 +224,14 @@ class ActivityType extends AbstractType
if ($activityType->isVisible('comment')) {
$builder->add('comment', CommentType::class, [
'label' => $activityType->getLabel('comment'),
'label' => empty($activityType->getLabel('comment'))
? 'activity.comment' : $activityType->getLabel('comment'),
'required' => $activityType->isRequired('comment'),
]);
}
if ($activityType->isVisible('persons')) {
$builder->add('persons', HiddenType::class, [
//'data_class' => Person::class,
]);
$builder->add('persons', HiddenType::class);
$builder->get('persons')
->addModelTransformer(new CallbackTransformer(
function (iterable $personsAsIterable): string {
@@ -226,9 +252,7 @@ class ActivityType extends AbstractType
}
if ($activityType->isVisible('thirdParties')) {
$builder->add('thirdParties', HiddenType::class, [
//'data_class' => ThirdParty::class,
]);
$builder->add('thirdParties', HiddenType::class);
$builder->get('thirdParties')
->addModelTransformer(new CallbackTransformer(
function (iterable $thirdpartyAsIterable): string {
@@ -260,9 +284,7 @@ class ActivityType extends AbstractType
}
if ($activityType->isVisible('users')) {
$builder->add('users', HiddenType::class, [
//'data_class' => User::class,
]);
$builder->add('users', HiddenType::class);
$builder->get('users')
->addModelTransformer(new CallbackTransformer(
function (iterable $usersAsIterable): string {

View File

@@ -4,6 +4,7 @@ namespace Chill\ActivityBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -34,21 +35,13 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
{
$period = $parameters['accompanyingCourse'];
$menu->addChild($this->translator->trans('Activity list'), [
'route' => 'chill_activity_activity_list',
'routeParameters' => [
'accompanying_period_id' => $period->getId(),
]])
->setExtras(['order' => 40]);
$menu->addChild($this->translator->trans('Add a new activity'), [
'route' => 'chill_activity_activity_select_type',
'routeParameters' => [
'accompanying_period_id' => $period->getId(),
]])
->setExtras(['order' => 41]);
if (AccompanyingPeriod::STEP_DRAFT !== $period->getStep()) {
$menu->addChild($this->translator->trans('Activity list'), [
'route' => 'chill_activity_activity_list',
'routeParameters' => [
'accompanying_period_id' => $period->getId(),
]])
->setExtras(['order' => 40]);
}
}
}

View File

@@ -1,88 +0,0 @@
<?php
/*
*
*/
namespace Chill\ActivityBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Role\Role;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class MenuBuilder implements LocalMenuBuilderInterface
{
/**
*
* @var TokenStorageInterface
*/
protected $tokenStorage;
/**
*
* @var TranslatorInterface
*/
protected $translator;
/**
*
* @var AuthorizationHelper
*/
protected $authorizationHelper;
public function __construct(
TokenStorageInterface $tokenStorage,
TranslatorInterface $translator,
AuthorizationHelper $authorizationHelper
) {
$this->tokenStorage = $tokenStorage;
$this->translator = $translator;
$this->authorizationHelper = $authorizationHelper;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/* @var $person \Chill\PersonBundle\Entity\Person */
$person = $parameters['person'];
$user = $this->tokenStorage->getToken()->getUser();
$roleSee = new Role(ActivityVoter::SEE);
$roleAdd = new Role(ActivityVoter::CREATE);
if ($this->authorizationHelper->userHasAccess($user, $person, $roleSee)) {
$menu->addChild($this->translator->trans('Activity list'), [
'route' => 'chill_activity_activity_list',
'routeParameters' => [
'person_id' => $person->getId()
]
])
->setExtras([
'order' => 201
]);
}
if ($this->authorizationHelper->userHasAccess($user, $person, $roleAdd)) {
$menu->addChild($this->translator->trans('Add a new activity'), [
'route' => 'chill_activity_activity_new',
'routeParameters' => [
'person_id' => $person->getId()
]
])
->setExtras([
'order' => 200
]);
}
}
public static function getMenuIds(): array
{
return [ 'person' ];
}
}

View File

@@ -15,8 +15,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\ActivityBundle\Menu;
namespace Chill\ActivityBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
@@ -65,15 +64,6 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
->setExtra('order', 201)
;
}
if ($this->authorizationChecker->isGranted(ActivityVoter::CREATE, $person)) {
$menu->addChild(
$this->translator->trans('Add a new activity'), [
'route' => 'chill_activity_activity_new',
'routeParameters' => [ 'person_id' => $person->getId() ],
])
->setExtra('order', 200)
;
}
}
public static function getMenuIds(): array

View File

@@ -38,5 +38,30 @@ class ActivityRepository extends ServiceEntityRepository
{
parent::__construct($registry, Activity::class);
}
public function findByPersonImplied($person, array $scopes, $orderBy = [ 'date' => 'DESC'], $limit = 100, $offset = 0)
{
$qb = $this->createQueryBuilder('a');
$qb->select('a');
$qb
// TODO add acl
//->where($qb->expr()->in('a.scope', ':scopes'))
//->setParameter('scopes', $scopes)
->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('a.person', ':person'),
':person MEMBER OF a.persons'
)
)
->setParameter('person', $person)
;
foreach ($orderBy as $k => $dir) {
$qb->addOrderBy('a.'.$k, $dir);
}
return $qb->getQuery()
->getResult();
}
}

View File

@@ -112,3 +112,19 @@ div.flex-table.list-records {
margin-top: 1em;
}
}
div.activity-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
div.bloc {
width: 200px;
align-self: flex-end;
height: 140px;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -1,170 +1,17 @@
<template>
<teleport to="#add-persons">
<div class="flex-bloc concerned-groups" :class="getContext">
<persons-bloc
v-for="bloc in contextPersonsBlocs"
v-bind:key="bloc.key"
v-bind:bloc="bloc"
v-bind:setPersonsInBloc="setPersonsInBloc">
</persons-bloc>
</div>
<add-persons
buttonTitle="activity.add_persons"
modalTitle="activity.add_persons"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons">
</add-persons>
</teleport>
<concerned-groups></concerned-groups>
<social-issues-acc></social-issues-acc>
</template>
<script>
import { mapState } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import PersonsBloc from './components/PersonsBloc.vue';
import ConcernedGroups from './components/ConcernedGroups.vue';
import SocialIssuesAcc from './components/SocialIssuesAcc.vue';
export default {
name: "App",
components: {
AddPersons,
PersonsBloc
},
data() {
return {
personsBlocs: [
{ key: 'persons',
title: 'activity.bloc_persons',
persons: [],
included: false
},
{ key: 'personsAssociated',
title: 'activity.bloc_persons_associated',
persons: [],
included: false
},
{ key: 'personsNotAssociated',
title: 'activity.bloc_persons_not_associated',
persons: [],
included: false
},
{ key: 'thirdparty',
title: 'activity.bloc_thirdparty',
persons: [],
included: true
},
{ key: 'users',
title: 'activity.bloc_users',
persons: [],
included: true
},
],
addPersons: {
key: 'activity',
options: {
type: ['person', 'thirdparty'], // TODO add 'user'
priority: null,
uniq: false,
}
}
}
},
computed: {
...mapState({
persons: state => state.activity.persons,
thirdParties: state => state.activity.thirdParties,
users: state => state.activity.users,
accompanyingCourse: state => state.activity.accompanyingPeriod
}),
getContext() {
return (this.accompanyingCourse) ? "accompanyingCourse" : "person";
},
contextPersonsBlocs() {
return this.personsBlocs.filter(bloc => bloc.included !== false);
}
},
mounted() {
this.setPersonsInBloc();
},
methods: {
setPersonsInBloc() {
let groups;
if (this.accompanyingCourse) {
groups = this.splitPersonsInGroups();
}
this.personsBlocs.forEach(bloc => {
if (this.accompanyingCourse) {
switch (bloc.key) {
case 'personsAssociated':
bloc.persons = groups.personsAssociated;
bloc.included = true;
break;
case 'personsNotAssociated':
bloc.persons = groups.personsNotAssociated;
bloc.included = true;
break;
}
} else {
switch (bloc.key) {
case 'persons':
bloc.persons = this.persons;
bloc.included = true;
break;
}
}
switch (bloc.key) {
case 'thirdparty':
bloc.persons = this.thirdParties;
break;
case 'users':
bloc.persons = this.users;
break;
}
}, groups);
},
splitPersonsInGroups() {
let personsAssociated = [];
let personsNotAssociated = this.persons;
let participations = this.getCourseParticipations();
this.persons.forEach(person => {
participations.forEach(participation => {
if (person.id === participation.id) {
console.log(person.id);
personsAssociated.push(person);
personsNotAssociated = personsNotAssociated.filter(p => p !== person);
}
});
});
return {
'personsAssociated': personsAssociated,
'personsNotAssociated': personsNotAssociated
};
},
getCourseParticipations() {
let participations = [];
this.accompanyingCourse.participations.forEach(participation => {
if (!participation.endDate) {
participations.push(participation.person);
}
});
return participations;
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
this.$store.dispatch('addPersonsInvolved', item);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
this.setPersonsInBloc();
}
ConcernedGroups,
SocialIssuesAcc
}
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,18 @@
import { getSocialIssues } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api.js';
/*
* Load socialActions by socialIssue (id)
*/
const getSocialActionByIssue = (id) => {
const url = `/api/1.0/person/social/social-action/by-social-issue/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
getSocialIssues,
getSocialActionByIssue
};

View File

@@ -0,0 +1,170 @@
<template>
<teleport to="#add-persons">
<div class="flex-bloc concerned-groups" :class="getContext">
<persons-bloc
v-for="bloc in contextPersonsBlocs"
v-bind:key="bloc.key"
v-bind:bloc="bloc"
v-bind:setPersonsInBloc="setPersonsInBloc">
</persons-bloc>
</div>
<add-persons
buttonTitle="activity.add_persons"
modalTitle="activity.add_persons"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons">
</add-persons>
</teleport>
</template>
<script>
import { mapState } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import PersonsBloc from './ConcernedGroups/PersonsBloc.vue';
export default {
name: "ConcernedGroups",
components: {
AddPersons,
PersonsBloc
},
data() {
return {
personsBlocs: [
{ key: 'persons',
title: 'activity.bloc_persons',
persons: [],
included: false
},
{ key: 'personsAssociated',
title: 'activity.bloc_persons_associated',
persons: [],
included: false
},
{ key: 'personsNotAssociated',
title: 'activity.bloc_persons_not_associated',
persons: [],
included: false
},
{ key: 'thirdparty',
title: 'activity.bloc_thirdparty',
persons: [],
included: true
},
{ key: 'users',
title: 'activity.bloc_users',
persons: [],
included: true
},
],
addPersons: {
key: 'activity',
options: {
type: ['person', 'thirdparty'], // TODO add 'user'
priority: null,
uniq: false,
}
}
}
},
computed: {
...mapState({
persons: state => state.activity.persons,
thirdParties: state => state.activity.thirdParties,
users: state => state.activity.users,
accompanyingCourse: state => state.activity.accompanyingPeriod
}),
getContext() {
return (this.accompanyingCourse) ? "accompanyingCourse" : "person";
},
contextPersonsBlocs() {
return this.personsBlocs.filter(bloc => bloc.included !== false);
}
},
mounted() {
this.setPersonsInBloc();
},
methods: {
setPersonsInBloc() {
let groups;
if (this.accompanyingCourse) {
groups = this.splitPersonsInGroups();
}
this.personsBlocs.forEach(bloc => {
if (this.accompanyingCourse) {
switch (bloc.key) {
case 'personsAssociated':
bloc.persons = groups.personsAssociated;
bloc.included = true;
break;
case 'personsNotAssociated':
bloc.persons = groups.personsNotAssociated;
bloc.included = true;
break;
}
} else {
switch (bloc.key) {
case 'persons':
bloc.persons = this.persons;
bloc.included = true;
break;
}
}
switch (bloc.key) {
case 'thirdparty':
bloc.persons = this.thirdParties;
break;
case 'users':
bloc.persons = this.users;
break;
}
}, groups);
},
splitPersonsInGroups() {
let personsAssociated = [];
let personsNotAssociated = this.persons;
let participations = this.getCourseParticipations();
this.persons.forEach(person => {
participations.forEach(participation => {
if (person.id === participation.id) {
//console.log(person.id);
personsAssociated.push(person);
personsNotAssociated = personsNotAssociated.filter(p => p !== person);
}
});
});
return {
'personsAssociated': personsAssociated,
'personsNotAssociated': personsNotAssociated
};
},
getCourseParticipations() {
let participations = [];
this.accompanyingCourse.participations.forEach(participation => {
if (!participation.endDate) {
participations.push(participation.person);
}
});
return participations;
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
this.$store.dispatch('addPersonsInvolved', item);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
this.setPersonsInBloc();
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,214 @@
<template>
<teleport to="#social-issues-acc">
<div class="container">
<div class="grid-4 clear">
<label>{{ $t('activity.social_issues') }}</label>
</div>
<div class="grid-8">
<check-social-issue
v-for="issue in socialIssuesList"
v-bind:key="issue.id"
v-bind:issue="issue"
v-bind:selection="socialIssuesSelected"
@updateSelected="updateIssuesSelected">
</check-social-issue>
<div class="my-3">
<VueMultiselect
name="otherIssues"
label="text"
track-by="id"
open-direction="bottom"
v-bind:close-on-select="true"
v-bind:preserve-search="false"
v-bind:reset-after="true"
v-bind:hide-selected="true"
v-bind:taggable="false"
v-bind:multiple="false"
v-bind:searchable="true"
v-bind:allow-empty="true"
v-bind:show-labels="false"
v-bind:loading="issueIsLoading"
v-bind:placeholder="$t('activity.choose_other_social_issue')"
v-bind:options="socialIssuesOther"
v-model="value"
@select="addIssueInList">
</VueMultiselect>
</div>
</div>
</div>
<div class="container">
<div class="grid-4 clear">
<label>{{ $t('activity.social_actions') }}</label>
</div>
<div class="grid-8">
<div v-if="actionIsLoading === true">
<i class="chill-green fa fa-circle-o-notch fa-spin fa-lg"></i>
</div>
<check-social-action
v-if="socialIssuesSelected.length || socialActionsSelected.length"
v-for="action in socialActionsList"
v-bind:key="action.id"
v-bind:action="action"
v-bind:selection="socialActionsSelected"
@updateSelected="updateActionsSelected">
</check-social-action>
<span v-else class="inline-choice chill-no-data-statement mt-3">
{{ $t('activity.select_first_a_social_issue') }}
</span>
</div>
</div>
</teleport>
</template>
<script>
import { readonly } from 'vue';
import VueMultiselect from 'vue-multiselect';
import CheckSocialIssue from './SocialIssuesAcc/CheckSocialIssue.vue';
import CheckSocialAction from './SocialIssuesAcc/CheckSocialAction.vue';
import { getSocialIssues, getSocialActionByIssue } from '../api.js';
export default {
name: "SocialIssuesAcc",
components: {
CheckSocialIssue,
CheckSocialAction,
VueMultiselect
},
data() {
return {
issueIsLoading: false,
actionIsLoading: false
}
},
computed: {
socialIssuesList() {
return this.$store.state.activity.accompanyingPeriod.socialIssues;
},
socialIssuesSelected() {
return this.$store.state.activity.socialIssues;
},
socialIssuesOther() {
return this.$store.state.socialIssuesOther;
},
socialActionsList() {
return this.$store.state.socialActionsList;
},
socialActionsSelected() {
return this.$store.state.activity.socialActions;
}
},
mounted() {
/* Load others issues in multiselect
*/
this.issueIsLoading = true;
getSocialIssues().then(response => new Promise((resolve, reject) => {
this.$store.commit('updateIssuesOther', response.results);
/* Add in list the issues already associated (if not yet listed)
*/
this.socialIssuesSelected.forEach(issue => {
if (this.socialIssuesList.filter(i => i.id === issue.id).length !== 1) {
this.$store.commit('addIssueInList', issue);
}
}, this);
/* Remove from multiselect the issues that are not yet in checkbox list
*/
this.socialIssuesList.forEach(issue => {
this.$store.commit('removeIssueInOther', issue);
}, this);
/* Filter issues
*/
this.$store.commit('filterList', 'issues');
/* Add in list the actions already associated (if not yet listed)
*/
this.socialActionsSelected.forEach(action => {
this.$store.commit('addActionInList', action);
}, this);
/* Filter issues
*/
this.$store.commit('filterList', 'actions');
this.issueIsLoading = false;
resolve();
}));
},
methods: {
/* When choosing an issue in multiselect, add it in checkboxes (as selected),
remove it from multiselect, and add socialActions concerned
*/
addIssueInList(value) {
//console.log('addIssueInList', value);
this.$store.commit('addIssueInList', value);
this.$store.commit('removeIssueInOther', value);
this.$store.dispatch('addIssueSelected', value);
this.updateActionsList();
},
/* Update value for selected issues checkboxes
*/
updateIssuesSelected(issues) {
//console.log('updateIssuesSelected', issues);
this.$store.dispatch('updateIssuesSelected', issues);
this.updateActionsList();
},
/* Update value for selected actions checkboxes
*/
updateActionsSelected(actions) {
//console.log('updateActionsSelected', actions);
this.$store.dispatch('updateActionsSelected', actions);
},
/* Add socialActions concerned: after reset, loop on each issue selected
to get social actions concerned
*/
updateActionsList() {
//console.log('updateActionsList');
this.resetActionsList();
this.socialIssuesSelected.forEach(item => {
this.actionIsLoading = true;
getSocialActionByIssue(item.id)
.then(actions => new Promise((resolve, reject) => {
actions.results.forEach(action => {
this.$store.commit('addActionInList', action);
}, this);
this.$store.commit('filterList', 'actions');
this.actionIsLoading = false;
resolve();
}));
}, this);
},
/* Reset socialActions List: flush list and restore selected actions
*/
resetActionsList() {
this.$store.commit('resetActionsList');
this.socialActionsSelected.forEach(item => {
this.$store.commit('addActionInList', item);
}, this);
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
span.multiselect__single {
display: none !important;
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<span class="inline-choice">
<input
type="checkbox"
v-model="selected"
name="action"
v-bind:id="action.id"
v-bind:value="action"
/>
<label class="inline" v-bind:for="action.id">
{{ action.text }}
</label>
</span><br>
</template>
<script>
export default {
name: "CheckSocialAction",
props: [ 'action', 'selection' ],
emits: [ 'updateSelected' ],
computed: {
selected: {
set(value) {
this.$emit('updateSelected', value);
},
get() {
return this.selection;
}
}
}
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<span class="inline-choice">
<input
type="checkbox"
v-model="selected"
name="issue"
v-bind:id="issue.id"
v-bind:value="issue"
/>
<label class="inline" v-bind:for="issue.id">
{{ issue.text }}
</label>
</span><br>
</template>
<script>
export default {
name: "CheckSocialIssue",
props: [ 'issue', 'selection' ],
emits: [ 'updateSelected' ],
computed: {
selected: {
set(value) {
this.$emit('updateSelected', value);
},
get() {
return this.selection;
}
}
}
}
</script>

View File

@@ -3,6 +3,13 @@ import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n'
const appMessages = {
fr: {
activity: {
//
social_issues: "Problématiques sociales",
choose_other_social_issue: "Ajouter une autre problématique sociale...",
social_actions: "Actions d'accompagnement",
select_first_a_social_issue: "Sélectionnez d'abord une problématique sociale",
//
add_persons: "Ajouter des personnes concernées",
bloc_persons: "Usagers",
bloc_persons_associated: "Usagers du parcours",

View File

@@ -21,11 +21,62 @@ const removeIdFromValue = (string, id) => {
const store = createStore({
strict: debug,
state: {
activity: window.activity
},
getters: {
activity: window.activity,
socialIssuesOther: [],
socialActionsList: [],
},
mutations: {
// SocialIssueAcc
addIssueInList(state, issue) {
//console.log('add issue list', issue.id);
state.activity.accompanyingPeriod.socialIssues.push(issue);
},
addIssueSelected(state, issue) {
//console.log('add issue selected', issue.id);
state.activity.socialIssues.push(issue);
},
updateIssuesSelected(state, issues) {
//console.log('update issues selected', issues);
state.activity.socialIssues = issues;
},
updateIssuesOther(state, payload) {
//console.log('update issues other');
state.socialIssuesOther = payload;
},
removeIssueInOther(state, issue) {
//console.log('remove issue other', issue.id);
state.socialIssuesOther = state.socialIssuesOther.filter(i => i.id !== issue.id);
},
resetActionsList(state) {
//console.log('reset list actions');
state.socialActionsList = [];
},
addActionInList(state, action) {
//console.log('add action list', action.id);
state.socialActionsList.push(action);
},
updateActionsSelected(state, actions) {
//console.log('update actions selected', actions);
state.activity.socialActions = actions;
},
filterList(state, list) {
const filterList = (list) => {
// remove duplicates entries
list = list.filter((value, index) => list.findIndex(array => array.id === value.id) === index);
// alpha sort
list.sort((a,b) => (a.text > b.text) ? 1 : ((b.text > a.text) ? -1 : 0));
return list;
};
if (list === 'issues') {
state.activity.accompanyingPeriod.socialIssues = filterList(state.activity.accompanyingPeriod.socialIssues);
}
if (list === 'actions') {
state.socialActionsList = filterList(state.socialActionsList);
}
},
// ConcernedGroups
addPersonsInvolved(state, payload) {
//console.log('### mutation addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
@@ -56,8 +107,29 @@ const store = createStore({
}
},
actions: {
addIssueSelected({ commit }, issue) {
let aSocialIssues = document.getElementById("chill_activitybundle_activity_socialIssues");
aSocialIssues.value = addIdToValue(aSocialIssues.value, issue.id);
commit('addIssueSelected', issue);
},
updateIssuesSelected({ commit }, payload) {
let aSocialIssues = document.getElementById("chill_activitybundle_activity_socialIssues");
aSocialIssues.value = '';
payload.forEach(item => {
aSocialIssues.value = addIdToValue(aSocialIssues.value, item.id);
});
commit('updateIssuesSelected', payload);
},
updateActionsSelected({ commit }, payload) {
let aSocialActions = document.getElementById("chill_activitybundle_activity_socialActions");
aSocialActions.value = '';
payload.forEach(item => {
aSocialActions.value = addIdToValue(aSocialActions.value, item.id);
});
commit('updateActionsSelected', payload);
},
addPersonsInvolved({ commit }, payload) {
console.log('### action addPersonsInvolved', payload.result.type);
//console.log('### action addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
case 'person':
let aPersons = document.getElementById("chill_activitybundle_activity_persons");
@@ -75,7 +147,7 @@ const store = createStore({
commit('addPersonsInvolved', payload);
},
removePersonInvolved({ commit }, payload) {
console.log('### action removePersonInvolved', payload);
//console.log('### action removePersonInvolved', payload);
switch (payload.type) {
case 'person':
let aPersons = document.getElementById("chill_activitybundle_activity_persons");

View File

@@ -1,4 +1,8 @@
<h1>{{ "Update activity"|trans }}</h1>
<h1>{{ "Update activity"|trans ~ ' :' }}
<span style="font-size: 70%; text-transform: lowercase; margin-left: 1em;">
{{ entity.type.name|localize_translatable_string }}
</span>
</h1>
{{ form_start(edit_form) }}
{{ form_errors(edit_form) }}
@@ -19,13 +23,15 @@
{{ form_row(edit_form.scope) }}
{% endif %}
{%- if edit_form.socialIssues is defined -%}
{{ form_row(edit_form.socialIssues) }}
{% endif %}
{%- if edit_form.socialActions is defined -%}
{{ form_row(edit_form.socialActions) }}
{% endif %}
{%- if edit_form.socialIssues is defined -%}
{{ form_row(edit_form.socialIssues) }}
{% endif %}
<div id="social-issues-acc"></div>
{%- if edit_form.reasons is defined -%}
{{ form_row(edit_form.reasons) }}
@@ -93,7 +99,7 @@
</a>
</li>
<li>
<button class="sc-button bt-update" type="submit">{{ 'Save activity'|trans }}</button>
<button class="sc-button bt-update" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(edit_form) }}

View File

@@ -10,16 +10,20 @@
{% endblock %}
{% block js %}
{{ parent() }}
{{ encore_entry_link_tags('async_upload') }}
<script type="text/javascript">
chill.displayAlertWhenLeavingModifiedForm('form[name="{{ edit_form.vars.form.vars.name }}"]',
window.addEventListener('DOMContentLoaded', function (e) {
chill.displayAlertWhenLeavingModifiedForm('form[name="{{ edit_form.vars.form.vars.name }}"]',
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
});
window.activity = {{ activity_json|json_encode|raw }};
</script>
{{ encore_entry_script_tags('vue_activity') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('async_upload') }}
{{ encore_entry_link_tags('vue_activity') }}
{% endblock %}

View File

@@ -21,15 +21,17 @@
{% block title 'Update activity'|trans %}
{% block personcontent %}
{% include 'ChillActivityBundle:Activity:edit.html.twig' %}
<div id="activity"></div> {# <=== vue component #}
{% include 'ChillActivityBundle:Activity:edit.html.twig' %}
{% endblock %}
{% block js %}
{{ encore_entry_link_tags('async_upload') }}
<script type="text/javascript">
chill.displayAlertWhenLeavingModifiedForm('form[name="{{ edit_form.vars.form.vars.name }}"]',
window.addEventListener('DOMContentLoaded', function (e) {
chill.displayAlertWhenLeavingModifiedForm('form[name="{{ edit_form.vars.form.vars.name }}"]',
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
});
window.activity = {{ activity_json|json_encode|raw }};
</script>
{{ encore_entry_script_tags('vue_activity') }}

View File

@@ -17,7 +17,7 @@
</p>
{% else %}
<div class="flex-table list-records {{ context }}">
<div class="flex-table list-records context-{{ context }}">
<!--
<thead>
<tr>
@@ -54,6 +54,20 @@
</p>
{% endif %}
</div>
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
<div class="accompanyingPeriodLink" style="margin-top: 1rem">
<a
href="{{ chill_path_add_return_path(
"chill_person_accompanying_course_index",
{ 'accompanying_period_id': activity.accompanyingPeriod.id }
) }}"
>
<i class="fa fa-random"></i>
{{ activity.accompanyingPeriod.id }}
</a>
</div>
{% endif %}
</div>
<div class="item-col">
@@ -147,13 +161,14 @@
</div>
{%
if activity.comment.comment is not empty
if activity.comment.comment is not empty
or activity.persons|length > 0
or activity.thirdParties|length > 0
or activity.users|length > 0
%}
<div class="item-row details">
<div class="item-col">
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'row', 'entity': activity } %}
</div>
@@ -170,10 +185,13 @@
</div>
{% endif %}
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activity_new', {'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="sc-button bt-create">
{{ 'Add a new activity' | trans }}
</a>
</li>
</ul>
{% if context != 'person' %}
{# TODO set this condition in configuration #}
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activity_new', {'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="sc-button bt-create">
{{ 'Add a new activity' | trans }}
</a>
</li>
</ul>
{% endif %}

View File

@@ -1,4 +1,8 @@
<h1>{{ "Activity creation"|trans }}</h1>
<h1>{{ "Activity creation"|trans ~ ' :' }}
<span style="font-size: 70%; text-transform: lowercase; margin-left: 1em;">
{{ entity.type.name|localize_translatable_string }}
</span>
</h1>
{{ form_start(form) }}
{{ form_errors(form) }}
@@ -20,14 +24,15 @@
{{ form_row(form.scope) }}
{% endif %}
{%- if form.socialActions is defined -%}
{{ form_row(form.socialActions) }}
{% endif %}
{%- if form.socialIssues is defined -%}
{{ form_row(form.socialIssues) }}
{% endif %}
{%- if form.socialActions is defined -%}
{{ form_row(form.socialActions) }}
{% endif %}
<div id="social-issues-acc"></div>
{%- if form.reasons is defined -%}
{{ form_row(form.reasons) }}
@@ -93,7 +98,7 @@
</li>
<li>
<button class="sc-button bt-create" type="submit">
{{ 'Add a new activity'|trans }}
{{ 'Create'|trans }}
</button>
</li>
</ul>

View File

@@ -10,16 +10,20 @@
{% endblock %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('async_upload') }}
<script type="text/javascript">
chill.displayAlertWhenLeavingUnsubmittedForm('form[name="{{ form.vars.form.vars.name }}"]',
window.addEventListener('DOMContentLoaded', function (e) {
chill.displayAlertWhenLeavingUnsubmittedForm('form[name="{{ form.vars.form.vars.name }}"]',
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
});
window.activity = {{ activity_json|json_encode|raw }};
</script>
{{ encore_entry_script_tags('vue_activity') }}
{% endblock %}
{% block css %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('build/async_upload.css') }}"/>
{{ encore_entry_link_tags('vue_activity') }}
{% endblock %}

View File

@@ -5,15 +5,17 @@
{% block title 'Activity creation' |trans %}
{% block personcontent %}
{% include 'ChillActivityBundle:Activity:new.html.twig' with {'context': 'person'} %}
<div id="activity"></div> {# <=== vue component #}
{% include 'ChillActivityBundle:Activity:new.html.twig' with {'context': 'person'} %}
{% endblock %}
{% block js %}
{{ encore_entry_link_tags('async_upload') }}
<script type="text/javascript">
chill.displayAlertWhenLeavingUnsubmittedForm('form[name="{{ form.vars.form.vars.name }}"]',
window.addEventListener('DOMContentLoaded', function (e) {
chill.displayAlertWhenLeavingUnsubmittedForm('form[name="{{ form.vars.form.vars.name }}"]',
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
});
window.activity = {{ activity_json|json_encode|raw }};
</script>
{{ encore_entry_script_tags('vue_activity') }}

View File

@@ -4,7 +4,7 @@
{% for row in data %}
<h3>{{ row.activityTypeCategory.name|localize_translatable_string }}</h3>
<div style="display:flex;justify-content:center;gap:12px;flex-wrap:wrap;">
<div class="activity-row">
{% for activityType in row.activityTypes %}
{% set person_id = null %}
@@ -19,7 +19,7 @@
<a href="{{ path('chill_activity_activity_new', {'person_id': person_id, 'activityType_id': activityType.id, 'accompanying_period_id': accompanying_course_id }) }}">
<div style="width:200px;height:200px;border:1px dotted red;display:flex;justify-content:center;align-items:center;align-content:center;">
<div class="bloc btn btn-primary btn-lg btn-block">
{{ activityType.name|localize_translatable_string }}
</div>
</a>

View File

@@ -117,7 +117,7 @@
</li>
<li>
<a class="sc-button bt-update" href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}">
{{ 'Edit the activity'|trans }}
{{ 'Edit'|trans }}
</a>
</li>
{# TODO

View File

@@ -32,6 +32,7 @@ services:
- "@chill.main.helper.translatable_string"
- "%chill_activity.form.time_duration%"
- '@Chill\PersonBundle\Templating\Entity\SocialIssueRender'
- '@Chill\PersonBundle\Templating\Entity\SocialActionRender'
tags:
- { name: form.type, alias: chill_activitybundle_activity }

View File

@@ -38,7 +38,7 @@ Required: Obligatoire
Persons: Personnes
Users: Utilisateurs
Emergency: Urgent
Sent received: Envoyer / Recevoir
Sent received: Entrant / Sortant
Sent: Envoyer
Received: Recevoir
by: 'Par '
@@ -74,6 +74,7 @@ Users concerned: T(M)S
activity:
Insert a document: Insérer un document
Remove a document: Supprimer le document
comment: Commentaire
#timeline

View File

@@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use Chill\MainBundle\Routing\MenuComposer;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Alias;
/**
*
@@ -49,29 +50,32 @@ class CRUDControllerCompilerPass implements CompilerPassInterface
private function configureCrudController(ContainerBuilder $container, array $crudEntry, string $apiOrCrud): void
{
$controllerClass = $crudEntry['controller'];
$controllerServiceName = 'cs'.$apiOrCrud.'_'.$crudEntry['name'].'_controller';
if ($container->hasDefinition($controllerClass)) {
$controller = $container->getDefinition($controllerClass);
$container->removeDefinition($controllerClass);
$alreadyDefined = true;
} else {
$controller = new Definition($controllerClass);
$alreadyDefined = false;
}
$controller->addTag('controller.service_arguments');
if (FALSE === $alreadyDefined) {
$controller->setAutoconfigured(true);
$controller->setPublic(true);
}
// create config parameter in container
$param = 'chill_main_'.$apiOrCrud.'_config_'.$crudEntry['name'];
$container->setParameter($param, $crudEntry);
$controller->addMethodCall('setCrudConfig', ['%'.$param.'%']);
$container->setDefinition($controllerServiceName, $controller);
if ($container->hasDefinition($controllerClass)) {
// create an alias to not to re-create the service
$alias = new Alias($controllerClass, true);
$container->setAlias($controllerServiceName, $alias);
// add the "addMethodCall"
$container->getDefinition($controllerClass)
->addMethodCall('setCrudConfig', ['%'.$param.'%']);
} else {
$controller = new Definition($controllerClass);
$controller->addTag('controller.service_arguments');
$controller->setAutoconfigured(true);
$controller->setPublic(true);
$controller->addMethodCall('setCrudConfig', ['%'.$param.'%']);
$container->setDefinition($controllerServiceName, $controller);
}
}
}

View File

@@ -8,6 +8,11 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\CRUD\Resolver\Resolver;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Translation\TranslatorInterface;
class AbstractCRUDController extends AbstractController
{
@@ -248,4 +253,24 @@ class AbstractCRUDController extends AbstractController
{
return $this->get('validator');
}
/**
* @return array
*/
public static function getSubscribedServices(): array
{
return \array_merge(
parent::getSubscribedServices(),
[
'chill_main.paginator_factory' => PaginatorFactory::class,
'translator' => TranslatorInterface::class,
AuthorizationHelper::class => AuthorizationHelper::class,
EventDispatcherInterface::class => EventDispatcherInterface::class,
Resolver::class => Resolver::class,
SerializerInterface::class => SerializerInterface::class,
'validator' => ValidatorInterface::class,
]
);
}
}

View File

@@ -359,9 +359,10 @@ class ApiController extends AbstractCRUDController
* 6. validate the base entity (not the deserialized one). Groups are fetched from getValidationGroups, validation is perform by `validate`
* 7. run onAfterValidation
* 8. if errors, return a 422 response with errors
* 9. flush the data
* 10. run onAfterFlush
* 11. return a 202 response for DELETE with empty body, or HTTP 200 for post with serialized posted entity
* 9. if $forcePersist === true, persist the entity
* 10. flush the data
* 11. run onAfterFlush
* 12. return a 202 response for DELETE with empty body, or HTTP 200 for post with serialized posted entity
*
* @param string action
* @param mixed id
@@ -370,11 +371,12 @@ class ApiController extends AbstractCRUDController
* @param string $property the name of the property. This will be used to make a `add+$property` and `remove+$property` method
* @param string $postedDataType the type of the posted data (the content)
* @param string $postedDataContext a context to deserialize posted data (the content)
* @param bool $forcePersist force to persist the created element (only for POST request)
* @throw BadRequestException if unable to deserialize the posted data
* @throw BadRequestException if the method is not POST or DELETE
*
*/
protected function addRemoveSomething(string $action, $id, Request $request, string $_format, string $property, string $postedDataType, $postedDataContext = []): Response
protected function addRemoveSomething(string $action, $id, Request $request, string $_format, string $property, string $postedDataType, array $postedDataContext = [], bool $forcePersist = false): Response
{
$entity = $this->getEntity($action, $id, $request);
@@ -429,6 +431,10 @@ class ApiController extends AbstractCRUDController
return $this->json($errors, 422);
}
if ($forcePersist && $request->getMethod() === Request::METHOD_POST) {
$this->getDoctrine()->getManager()->persist($postedData);
}
$this->getDoctrine()->getManager()->flush();

View File

@@ -142,11 +142,11 @@ class CRUDRoutesLoader extends Loader
protected function loadApi(array $crudConfig): RouteCollection
{
$collection = new RouteCollection();
$controller ='csapi_'.$crudConfig['name'].'_controller';
$controller = 'csapi_'.$crudConfig['name'].'_controller';
foreach ($crudConfig['actions'] as $name => $action) {
// filter only on single actions
$singleCollection = $action['single-collection'] ?? $name === '_entity' ? 'single' : NULL;
$singleCollection = $action['single_collection'] ?? $name === '_entity' ? 'single' : NULL;
if ('collection' === $singleCollection) {
// continue;
}
@@ -171,7 +171,7 @@ class CRUDRoutesLoader extends Loader
// path are rewritten
// if name === 'default', we rewrite it to nothing :-)
$localName = \in_array($name, [ '_entity', '_index' ]) ? '' : '/'.$name;
if ('collection' === $action['single-collection'] || '_index' === $name) {
if ('collection' === $action['single_collection'] || '_index' === $name) {
$localPath = $action['path'] ?? $localName.'.{_format}';
} else {
$localPath = $action['path'] ?? '/{id}'.$localName.'.{_format}';

View File

@@ -36,6 +36,7 @@ use Chill\MainBundle\Search\SearchProvider;
use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\SearchApi;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
/**
* Class SearchController
@@ -151,11 +152,14 @@ class SearchController extends AbstractController
{
//TODO this is an incomplete implementation
$query = $request->query->get('q', '');
$types = $request->query->get('type', []);
$results = $this->searchApi->getResults($query, 0, 150);
$paginator = $this->paginatorFactory->create(count($results));
if (count($types) === 0) {
throw new BadRequestException("The request must contains at "
." one type");
}
$collection = new Collection($results, $paginator);
$collection = $this->searchApi->getResults($query, $types, []);
return $this->json($collection);
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
class UserApiController extends ApiController
{
/**
* @Route(
* "/api/1.0/main/whoami.{_format}",
* name="chill_main_user_whoami",
* requirements={
* "_format": "json"
* }
* )
*/
public function whoami($_format): JsonResponse
{
return $this->json($this->getUser(), JsonResponse::HTTP_OK, [],
[ "groups" => [ "read" ] ]);
}
}

View File

@@ -195,7 +195,9 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
->prependExtensionConfig(
'doctrine',
[
'dbal' => [
'dbal' => [
// ignore views:
'schema_filter' => '~^(?!view_)~',
// This is mandatory since we are using postgis as database.
'mapping_types' => [
'geometry' => 'string',
@@ -277,7 +279,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_POST => true,
Request::METHOD_HEAD => true
Request::METHOD_HEAD => true,
Request::METHOD_PATCH => true
]
],
]
@@ -319,7 +322,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
Request::METHOD_HEAD => true,
Request::METHOD_POST => true,
]
],
]
@@ -343,7 +347,28 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
]
],
]
]
],
[
'class' => \Chill\MainBundle\Entity\User::class,
'controller' => \Chill\MainBundle\Controller\UserApiController::class,
'name' => 'user',
'base_path' => '/api/1.0/main/user',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
]
],
]
],
]
]);
}

View File

@@ -205,7 +205,7 @@ class Configuration implements ConfigurationInterface
->ignoreExtraKeys(false)
->info('the requirements for the route. Will be set to `[ \'id\' => \'\d+\' ]` if left empty.')
->end()
->enumNode('single-collection')
->enumNode('single_collection')
->values(['single', 'collection'])
->defaultValue('single')
->info('indicates if the returned object is a single element or a collection. '.

View File

@@ -116,7 +116,7 @@ class Address
* @ORM\Column(type="date")
* @groups({"write"})
*/
private $validFrom;
private \DateTime $validFrom;
/**
* Indicates when the address ends. Used to build an history
@@ -127,11 +127,12 @@ class Address
* @ORM\Column(type="date", nullable=true)
* @groups({"write"})
*/
private $validTo;
private ?\DateTime $validTo = null;
/**
* True if the address is a "no address", aka homeless person, ...
* @groups({"write"})
* @ORM\Column(type="boolean")
*
* @var bool
*/
@@ -298,7 +299,7 @@ class Address
* @param bool $isNoAddress
* @return $this
*/
public function setIsNoAddress(bool $isNoAddress)
public function setIsNoAddress(bool $isNoAddress): self
{
$this->isNoAddress = $isNoAddress;
return $this;

View File

@@ -26,7 +26,7 @@ class PostalCode
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @groups({"read"})
* @groups({"write", "read"})
*/
private $id;
@@ -34,7 +34,7 @@ class PostalCode
* @var string
*
* @ORM\Column(type="string", length=255, name="label")
* @groups({"read"})
* @groups({"write", "read"})
*/
private $name;
@@ -42,7 +42,7 @@ class PostalCode
* @var string
*
* @ORM\Column(type="string", length=100)
* @groups({"read"})
* @groups({"write", "read"})
*/
private $code;
@@ -50,10 +50,17 @@ class PostalCode
* @var Country
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country")
* @groups({"read"})
* @groups({"write", "read"})
*/
private $country;
/**
* @var integer
*
* @ORM\Column(name="origin", type="integer", nullable=true)
* @groups({"write", "read"})
*/
private $origin = 0;
/**
* Get id
@@ -65,6 +72,32 @@ class PostalCode
return $this->id;
}
/**
* Set origin
*
* @param int $origin
*
* @return PostalCode
*/
public function setOrigin($origin)
{
$this->origin = $origin;
return $this;
}
/**
* Get origin
*
* @return int
*/
public function getOrigin()
{
return $this->origin;
}
/**
* Set name
*

View File

@@ -45,13 +45,14 @@ class CommentType extends AbstractType
{
$builder
->add('comment', ChillTextareaType::class, [
'disable_editor' => $options['disable_editor']
'disable_editor' => $options['disable_editor'],
'label' => $options['label'],
])
;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getForm()->getData();
$comment = $event->getData();
$comment = $event->getData() ?? ['comment' => ''];
if ($data->getComment() !== $comment['comment']) {
$data->setDate(new \DateTime());

View File

@@ -43,6 +43,11 @@ final class ScopeRepository implements ObjectRepository
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function createQueryBuilder($alias, $indexBy = null)
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
public function getClassName() {
return Scope::class;

View File

@@ -11,13 +11,16 @@
*
* Do not take time into account
*
* **Experimental**
*/
const dateToISO = (date) => {
if (null === date) {
return null;
}
return [
this.$store.state.startDate.getFullYear(),
(this.$store.state.startDate.getMonth() + 1).toString().padStart(2, '0'),
this.$store.state.startDate.getDate().toString().padStart(2, '0')
date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, '0'),
date.getDate().toString().padStart(2, '0')
].join('-');
};
@@ -36,15 +39,17 @@ const ISOToDate = (str) => {
/**
* Return a date object from iso string formatted as YYYY-mm-dd:HH:MM:ss+01:00
*
* **Experimental**
*/
const ISOToDatetime = (str) => {
console.log(str);
if (null === str) {
return null;
}
let
[cal, times] = str.split('T'),
[year, month, date] = cal.split('-'),
[time, timezone] = cal.split(times.charAt(9)),
[hours, minutes, seconds] = cal.split(':')
[time, timezone] = times.split(times.charAt(8)),
[hours, minutes, seconds] = time.split(':')
;
return new Date(year, month-1, date, hours, minutes, seconds);

View File

@@ -35,6 +35,7 @@ $table-body-td-text-align: left;
// Tabs
$tabs-nav-margin-bottom: 0.2em;
$tabs-nav-bg-color: $yellow;
$tabs-nav-bg-color-light: lighten($yellow, 10%);
$tabs-nav-text-color: $blue;
$tabs-new-border: none;
$tabs-nav-hover-border: none;

View File

@@ -30,7 +30,7 @@
&.bt-cancel::before,
&.bt-view::before,
&.bt-show::before {
font: normal normal normal 14px/1 FontAwesome;
font: normal normal normal 14px/1 ForkAwesome;
margin-right: 0.5em;
}

View File

@@ -26,7 +26,13 @@ $global-bg-color: $yellow;
color: $tabs-nav-title-text-color;
}
> a {
&.sub-menu {
padding-left: 20px;
> a {
background-color: $tabs-nav-bg-color-light;
}
}
> a {
display: block;
width: auto;
padding: $tabs-nav-padding;
@@ -38,11 +44,11 @@ $global-bg-color: $yellow;
@include border-top-radius($base-border-radius);
@include border-bottom-radius($base-border-radius);
&:hover, &:active {
&:hover, &:active {
border: $tabs-nav-hover-border;
color: $tabs-nav-hover-text-color;
text-decoration: none;
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
/*
* when an alert is the first child of the page, with a banner, we do not want the alert to be merged with the banner
*/
div.container.content {
& > div {
div.alert:nth-child(2) {
margin-top: 1rem;
}
}
}

View File

@@ -0,0 +1,29 @@
div.alert.alert-with-actions {
display: flex;
flex-direction: row;
ul.record_actions {
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
li:nth-child(1n+2) {
margin-top: 0.5rem;
}
li {
margin-right: 0;
}
}
@media screen and (max-width: 1050px) {
flex-direction: column;
ul.record_actions {
margin-top: 1rem;
text-align: center;
}
}
}

View File

@@ -7,6 +7,10 @@
*/
@import 'alert-first-child';
@import 'alert-with-actions';
/* [hack] /!\ Contourne le positionnement problématique du div#content_conainter suivant,
* car sa position: relative le place au-dessus du bandeau et les liens sont incliquables */
div.subheader {
@@ -41,33 +45,6 @@ table {
}
}
/*
* ACCOMPANYING_COURSE
* Header custom for Accompanying Course
*/
div#header-accompanying_course-name {
background: none repeat scroll 0 0 #718596;
color: #FFF;
h1 {
margin: 0.4em 0;
}
span {
a {
color: white;
}
a:hover {
text-decoration: underline;
}
}
}
div#header-accompanying_course-details {
background: none repeat scroll 0 0 #718596ab;
color: #FFF;
padding-top: 1em;
padding-bottom: 1em;
}
/*
* FLEX RESPONSIVE TABLE/BLOCK PRESENTATION
*/
@@ -105,23 +82,13 @@ div.flex-bloc {
align-content: stretch;
div.item-bloc {
flex-grow: 0; flex-shrink: 1; flex-basis: 50%;
flex-grow: 0; flex-shrink: 1; flex-basis: 33%;
margin: 0;
padding: 1em;
border-top: 0;
&:nth-child(1), &:nth-child(2) {
border-top: 1px solid #000;
}
border-left: 0;
&:nth-child(odd) {
border-left: 1px solid #000;
}
//background-color: #e6e6e6;
display: flex;
flex-direction: column;
& > div.item-row {
div.item-row {
flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex;
flex-direction: column;
@@ -154,12 +121,6 @@ div.flex-bloc {
@media only screen and (max-width: 900px) {
flex-direction: column;
margin: auto 0;
div.item-bloc {
border-left: 1px solid #000;
&:nth-child(2) {
border-top: 0;
}
}
}
}
@@ -228,8 +189,6 @@ div.flex-table {
}
}
/*
* Address form
*/
@@ -248,6 +207,9 @@ div.address_form {
display: flex;
flex-direction: column;
flex-grow: 1;
div.custom-address, div.custom-postcode {
padding: 12px;
}
}
div.address_form__select__map {
@@ -255,13 +217,18 @@ div.address_form {
div#address_map {
height:400px;
width:400px;
input {
border: 1px solid #999;
}
}
}
}
div.address_form__more {
& > div {
display: flex;
& > label {
width: 30%;
}
}
}
}

View File

@@ -1,33 +1,85 @@
<template>
<div v-if="address.address">
{{ address.address.street }}, {{ address.address.streetNumber }}
<div class='person__address__create'>
<div>
<h2 v-if="!edit">{{ $t('create_a_new_address') }}</h2>
<h2 v-else>{{ $t('edit_a_new_address') }}</h2>
<add-address
@addNewAddress="addNewAddress">
</add-address>
</div>
<div>
<show-address
v-if="address"
v-bind:address="address">
</show-address>
</div>
</div>
<div v-if="address.city">
{{ address.city.code }} {{ address.city.name }}
</div>
<div v-if="address.country">
{{ address.country.name }}
<div v-if="!edit" class='person__address__valid'>
<h2>{{ $t('date') }}</h2>
<input
type="date"
name="validFrom"
:placeholder="$t('validFrom')"
v-model="validFrom"/>
<div v-if="errors.length > 0">
{{ errors }}
</div>
<div v-if="loading">
{{ $t('loading') }}
</div>
<div v-if="success">
{{ $t('person_address_creation_success') }}
</div>
</div>
<div>
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a :href=backUrl class="sc-button bt-cancel">{{ $t('back_to_the_list') }}</a>
</li>
<li v-if="!edit">
<button type="submit" class="sc-button bt-update centered" @click="addToPerson">
{{ $t('add_an_address_to_person') }}
</button>
</li>
</ul>
</div>
<add-address
@addNewAddress="addNewAddress">
</add-address>
</template>
<script>
import { mapState } from 'vuex';
import AddAddress from '../_components/AddAddress.vue';
import ShowAddress from '../_components/ShowAddress.vue';
export default {
name: 'App',
components: {
AddAddress
AddAddress,
ShowAddress
},
data() {
return {
edit: window.mode === 'edit',
personId: window.personId,
addressId: window.addressId,
backUrl: `/fr/person/${window.personId}/address/list`, //TODO better way to pass this
validFrom: new Date().toISOString().split('T')[0]
}
},
computed: {
address() {
return this.$store.state.address;
},
errors() {
return this.$store.state.errorMsg;
},
loading() {
return this.$store.state.loading;
},
success() {
return this.$store.state.success;
}
},
methods: {
@@ -36,9 +88,9 @@ export default {
let newAddress = {
'isNoAddress': address.isNoAddress,
'street': address.selected.address.street,
'streetNumber': address.selected.address.streetNumber,
'postcode': {'id': address.selected.city.id },
'street': address.isNoAddress ? '' : address.street,
'streetNumber': address.isNoAddress ? '' : address.streetNumber,
'postcode': {'id': address.selected.city.id},
'floor': address.floor,
'corridor': address.corridor,
'steps': address.steps,
@@ -49,12 +101,47 @@ export default {
};
if (address.selected.address.point !== undefined){
newAddress = Object.assign(newAddress, {'point': address.selected.address.point.coordinates});
newAddress = Object.assign(newAddress, {
'point': address.selected.address.point.coordinates
});
}
if (address.writeNewPostalCode){
let newPostalCode = address.newPostalCode;
newPostalCode = Object.assign(newPostalCode, {
'country': {'id': address.selected.country.id },
});
newAddress = Object.assign(newAddress, {
'newPostalCode': newPostalCode
});
}
if (this.edit){
this.$store.dispatch('updateAddress', {
addressId: this.addressId,
newAddress: newAddress
});
} else {
this.$store.dispatch('addAddress', newAddress);
}
this.$store.dispatch('addAddress', newAddress);
modal.showModal = false;
},
addToPerson() {
this.$store.dispatch('addDateToAddressAndAddressToPerson', {
personId: this.personId,
addressId: this.$store.state.address.address_id,
body: { validFrom: {datetime: `${this.validFrom}T00:00:00+0100`}}
})
},
getEditAddress() {
this.$store.dispatch('getEditAddress', this.addressId);
}
}
},
mounted() {
if (this.edit) {
this.getEditAddress();
}
},
};
</script>

View File

@@ -1,20 +1,34 @@
const addressMessages = {
fr: {
add_an_address_title: 'Ajouter une adresse',
add_an_address_title: 'Créer une adresse',
edit_an_address_title: 'Modifier une adresse',
create_a_new_address: 'Créer une nouvelle adresse',
edit_a_new_address: 'Modifier l\'adresse',
select_an_address_title: 'Sélectionner une adresse',
fill_an_address: 'Compléter l\'adresse',
select_country: 'Choisir le pays',
select_city: 'Choisir une localité',
select_address: 'Choisir une adresse',
create_address: 'Appuyer sur "Entrée" pour créer une nouvelle adresse',
create_address: 'Adresse inconnue. Cliquez ici pour créer une nouvelle adresse',
isNoAddress: 'Pas d\'adresse complète',
street: 'Nom de rue',
streetNumber: 'Numéro',
floor: 'Étage',
corridor: 'Couloir',
steps: 'Escalier',
flat: 'Appartement',
buildingName: 'Nom du batiment',
extra: 'Complément d\'adresse',
distribution: 'Service particulier de distribution'
distribution: 'Service particulier de distribution',
create_postal_code: 'Localité inconnue. Cliquez ici pour créer une nouvelle localité',
postalCode_name: 'Nom de la localité',
postalCode_code: 'Code postal de la localité',
date: 'Date de la nouvelle adresse',
add_an_address_to_person: 'Ajouter l\'adresse à la personne',
validFrom: 'Date de la nouvelle adresse',
back_to_the_list: 'Retour à la liste',
person_address_creation_success: 'La nouvelle adresse de la personne est enregistrée',
loading: 'chargement en cours...'
}
};

View File

@@ -1,7 +1,7 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postAddress } from '../../_api/AddAddress'
import { patchAddress, postAddress, postPostalCode, postAddressToPerson, getAddress } from '../../_api/AddAddress'
const debug = process.env.NODE_ENV !== 'production';
@@ -9,7 +9,11 @@ const store = createStore({
strict: debug,
state: {
address: {},
errorMsg: []
editAddress: {}, //TODO or should be address?
person: {},
errorMsg: [],
loading: false,
success: false
},
getters: {
},
@@ -20,25 +24,133 @@ const store = createStore({
addAddress(state, address) {
console.log('@M addAddress address', address);
state.address = address;
},
updateAddress(state, address) {
console.log('@M updateAddress address', address);
state.address = address;
},
addAddressToPerson(state, person) {
console.log('@M addAddressToPerson person', person);
state.person = person;
},
addDateToAddress(state, validFrom) {
console.log('@M addDateToAddress address.validFrom', validFrom);
state.validFrom = validFrom;
},
getEditAddress(state, address) {
console.log('@M getEditAddress address', address);
state.editAddress = address;
},
setLoading(state, b) {
state.loading = b;
},
setSuccess(state, b) {
state.success = b;
}
},
actions: {
addAddress({ commit }, payload) {
console.log('@A addAddress payload', payload);
//commit('addAddress', payload); // à remplacer par la suite
commit('setLoading', true);
if('newPostalCode' in payload){
let postalCodeBody = payload.newPostalCode;
postalCodeBody = Object.assign(postalCodeBody, {'origin': 3});
postPostalCode(postalCodeBody)
.then(postalCode => {
let body = payload;
body.postcode = {'id': postalCode.id},
postAddress(body)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
commit('setLoading', false);
}))
.catch((error) => {
commit('catchError', error);
commit('setLoading', false);
});
})
//fetch POST qui envoie l'adresse, et récupère la confirmation que c'est ok.
//La confirmation est l'adresse elle-même.
postAddress(payload)
} else {
postAddress(payload)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
commit('setLoading', false);
}))
.catch((error) => {
commit('catchError', error);
commit('setLoading', false);
});
}
},
addDateToAddressAndAddressToPerson({ commit }, payload) {
console.log('@A addDateToAddressAndAddressToPerson payload', payload);
commit('setLoading', true);
patchAddress(payload.addressId, payload.body)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
commit('addDateToAddress', address.validFrom);
resolve();
}))
}).then(
postAddressToPerson(payload.personId, payload.addressId)
.then(person => new Promise((resolve, reject) => {
commit('addAddressToPerson', person);
resolve();
commit('setLoading', false);
commit('setSuccess', true);
}))
.catch((error) => {
commit('catchError', error);
commit('setLoading', false);
})
))
.catch((error) => {
commit('catchError', error);
commit('setLoading', false);
});
}
},
updateAddress({ commit }, payload) {
console.log('@A updateAddress payload', payload);
if('newPostalCode' in payload.newAddress){ // TODO change the condition because it writes new postal code in edit mode now: !writeNewPostalCode
let postalCodeBody = payload.newAddress.newPostalCode;
postalCodeBody = Object.assign(postalCodeBody, {'origin': 3});
postPostalCode(postalCodeBody)
.then(postalCode => {
let body = payload.newAddress;
body.postcode = {'id': postalCode.id },
patchAddress(payload.addressId, body)
.then(address => new Promise((resolve, reject) => {
commit('updateAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
})
} else {
patchAddress(payload.addressId, payload.newAddress)
.then(address => new Promise((resolve, reject) => {
commit('updateAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
}
},
getEditAddress({ commit }, payload) {
console.log('@A getEditAddress payload', payload);
getAddress(payload).then(address => new Promise((resolve, reject) => {
commit('getEditAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
},
}
});

View File

@@ -36,7 +36,6 @@ const fetchCities = (country) => {
*/
const fetchReferenceAddresses = (postalCode) => {
console.log('<<< fetching references addresses for', postalCode);
//TODO deal with huge number of addresses... we should do suggestion...
const url = `/api/1.0/main/address-reference.json?item_per_page=1000&postal_code=${postalCode.id}`;
return fetch(url)
.then(response => {
@@ -45,16 +44,75 @@ const fetchReferenceAddresses = (postalCode) => {
});
};
/*
* Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference objects filtered with postal code
*/
const fetchAddresses = () => {
console.log('<<< fetching addresses');
//TODO deal with huge number of addresses... we should do suggestion...
const url = `/api/1.0/main/address.json?item_per_page=1000`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_address__entity__create
* method POST, post Address Object
* @returns {Promise}
*/
const postAddress = (address) => {
console.log(address);
const url = `/api/1.0/main/address.json?`;
const body = address;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_address__entity__create
* method PATCH, patch Address Instance
*
* @id integer - id of address
* @body Object - dictionary with changes to post
*/
const patchAddress = (id, body) => {
const url = `/api/1.0/main/address/${id}.json`;
return fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_postal_code__entity_create
* method POST, post Postal Code Object
* @returns {Promise}
*/
const postPostalCode = (postalCode) => {
const url = `/api/1.0/main/postal-code.json?`;
const body = postalCode;
return fetch(url, {
method: 'POST',
headers: {
@@ -67,9 +125,55 @@ const postAddress = (address) => {
});
};
/*
* Endpoint chill_api_single_person_address
* method POST, post Person instance
*
* @id integer - id of Person
* @body Object - dictionary with changes to post
*/
const postAddressToPerson = (personId, addressId) => {
console.log(personId);
console.log(addressId);
const body = {
'id': addressId
};
const url = `/api/1.0/person/person/${personId}/address.json`
return fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json;charset=utf-8'},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_address__index
* method GET, get Address Object
* @params {id} the address id
* @returns {Promise} a promise containing a Address object
*/
const getAddress = (id) => {
console.log('<<< get address');
const url = `/api/1.0/main/address/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
fetchCountries,
fetchCities,
fetchReferenceAddresses,
postAddress
fetchAddresses,
postAddress,
patchAddress,
postPostalCode,
postAddressToPerson,
getAddress
};

View File

@@ -1,7 +1,10 @@
<template>
<button class="sc-button bt-create centered mt-4" @click="openModal">
<button v-if="!edit" class="sc-button bt-create mt-4" @click="openModal">
{{ $t('add_an_address_title') }}
</button>
<button v-else class="sc-button bt-create mt-4" @click="openModal">
{{ $t('edit_an_address_title') }}
</button>
<teleport to="body">
<modal v-if="modal.showModal"
@@ -9,12 +12,17 @@
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t('add_an_address_title') }}</h3>
<h3 v-if="!edit" class="modal-title">{{ $t('add_an_address_title') }}</h3>
<h3 v-if="edit" class="modal-title">{{ $t('edit_an_address_title') }}</h3>
</template>
<template v-slot:body>
<div class="address_form">
<div v-if="loading">
{{ $t('loading') }}
</div>
<div class="address_form__header">
<h4>{{ $t('select_an_address_title') }}</h4>
</div>
@@ -38,10 +46,12 @@
<city-selection
v-bind:address="address"
v-bind:focusOnAddress="focusOnAddress"
v-bind:getReferenceAddresses="getReferenceAddresses">
</city-selection>
<address-selection
v-if="!isNoAddress"
v-bind:address="address"
v-bind:updateMapCenter="updateMapCenter">
</address-selection>
@@ -100,11 +110,15 @@ export default {
emits: ['addNewAddress'],
data() {
return {
edit: window.mode === 'edit',
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
loading: false,
address: {
writeNewAddress: false,
writeNewPostalCode: false,
loaded: {
countries: [],
cities: [],
@@ -115,11 +129,17 @@ export default {
city: {},
address: {},
},
newPostalCode: {
code: null,
name: null
},
addressMap: {
center : [48.8589, 2.3469], // Note: LeafletJs demands [lat, lon] cfr https://macwright.com/lonlat/
zoom: 12
},
isNoAddress: false,
street: null,
streetNumber: null,
floor: null,
corridor: null,
steps: null,
@@ -129,7 +149,7 @@ export default {
extra: null,
distribution: null,
},
errorMsg: {}
errorMsg: {},
}
},
computed: {
@@ -154,35 +174,52 @@ export default {
// this.$refs.search.focus(); // positionner le curseur à l'ouverture de la modale
//})
},
focusOnCity() {
const citySelector = document.getElementById('citySelector');
citySelector.focus();
},
focusOnAddress() {
const addressSelector = document.getElementById('addressSelector');
addressSelector.focus();
},
getCountries() {
console.log('getCountries');
this.loading = true;
fetchCountries().then(countries => new Promise((resolve, reject) => {
this.address.loaded.countries = countries.results;
resolve()
this.loading = false;
}))
.catch((error) => {
this.errorMsg.push(error.message);
this.loading = false;
});
},
getCities(country) {
console.log('getCities for', country.name);
this.loading = true;
fetchCities(country).then(cities => new Promise((resolve, reject) => {
this.address.loaded.cities = cities.results;
resolve()
this.address.loaded.cities = cities.results.filter(c => c.origin !== 3); // filter out user-defined cities
resolve();
this.loading = false;
}))
.catch((error) => {
this.errorMsg.push(error.message);
this.loading = false;
});
},
getReferenceAddresses(city) {
this.loading = true;
console.log('getReferenceAddresses for', city.name);
fetchReferenceAddresses(city).then(addresses => new Promise((resolve, reject) => {
console.log('addresses', addresses);
this.address.loaded.addresses = addresses.results;
resolve();
this.loading = false;
}))
.catch((error) => {
this.errorMsg.push(error.message);
this.loading = false;
});
},
updateMapCenter(point) {
@@ -198,6 +235,23 @@ export default {
this.address.loaded.cities = [];
this.address.selected.city = {};
this.address.selected.country = {};
this.address.isNoAddress = this.edit ? this.$store.state.editAddress.isNoAddress: false;;
this.address.street = this.edit ? this.$store.state.editAddress.street: null;
this.address.streetNumber = this.edit ? this.$store.state.editAddress.streetNumber: null;
this.address.floor = this.edit ? this.$store.state.editAddress.floor: null;
this.address.corridor = this.edit ? this.$store.state.editAddress.corridor: null;
this.address.steps = this.edit ? this.$store.state.editAddress.steps: null;
this.address.flat = this.edit ? this.$store.state.editAddress.flat: null;
this.address.buildingName = this.edit ? this.$store.state.editAddress.buildingName: null;
this.address.distribution = this.edit ? this.$store.state.editAddress.distribution: null;
this.address.extra = this.edit ? this.$store.state.editAddress.extra: null;
this.address.writeNewAddress = this.edit;
this.address.writeNewPostalCode = this.edit;
this.address.newPostalCode = this.edit ?
{
code: this.$store.state.editAddress.postcode !== undefined ? this.$store.state.editAddress.postcode.code : null,
name: this.$store.state.editAddress.postcode !== undefined ? this.$store.state.editAddress.postcode.name : null
} : {};
console.log('cities and addresses', this.address.loaded.cities, this.address.loaded.addresses);
}
}

View File

@@ -23,7 +23,7 @@ export default {
},
methods:{
init() {
map = L.map('address_map').setView([48.8589, 2.3469], 12);
map = L.map('address_map').setView([46.67059, -1.42683], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
@@ -40,7 +40,7 @@ export default {
update() {
console.log('update map with : ', this.address.addressMap.center)
marker.setLatLng(this.address.addressMap.center);
map.setView(this.address.addressMap.center, 12);
map.setView(this.address.addressMap.center, 15);
}
},
mounted(){

View File

@@ -1,39 +1,65 @@
<template>
<h4>{{ $t('fill_an_address') }}</h4>
<div>
<h4>{{ $t('fill_an_address') }}</h4>
<input
<label for="floor">{{ $t('floor') }}</label>
<input
type="text"
name="floor"
maxlength=16
:placeholder="$t('floor')"
v-model="floor"/>
<input
</div>
<div>
<label for="corridor">{{ $t('corridor') }}</label>
<input
type="text"
name="corridor"
maxlength=16
:placeholder="$t('corridor')"
v-model="corridor"/>
<input
</div>
<div>
<label for="steps">{{ $t('steps') }}</label>
<input
type="text"
name="steps"
maxlength=16
:placeholder="$t('steps')"
v-model="steps"/>
<input
</div>
<div>
<label for="flat">{{ $t('flat') }}</label>
<input
type="text"
name="flat"
maxlength=16
:placeholder="$t('flat')"
v-model="flat"/>
<input
</div>
<div>
<label for="buildingName">{{ $t('buildingName') }}</label>
<input
type="text"
name="buildingName"
maxlength=255
:placeholder="$t('buildingName')"
v-model="buildingName"/>
<input
</div>
<div>
<label for="extra">{{ $t('extra') }}</label>
<input
type="text"
name="extra"
maxlength=255
:placeholder="$t('extra')"
v-model="extra"/>
<input
</div>
<div>
<label for="distribution">{{ $t('distribution') }}</label>
<input
type="text"
name="distribution"
maxlength=255
:placeholder="$t('distribution')"
v-model="distribution"/>
</div>

View File

@@ -1,6 +1,7 @@
<template>
<div class="container">
<VueMultiselect
id="addressSelector"
v-model="value"
@select="selectAddress"
name="field"
@@ -15,6 +16,18 @@
:options="addresses">
</VueMultiselect>
</div>
<div class="custom-address" v-if="writeNewAddress || writeNewPostalCode">
<input
type="text"
name="street"
:placeholder="$t('street')"
v-model="street"/>
<input
type="text"
name="streetNumber"
:placeholder="$t('streetNumber')"
v-model="streetNumber"/>
</div>
</template>
<script>
@@ -30,24 +43,44 @@ export default {
}
},
computed: {
writeNewAddress() {
return this.address.writeNewAddress;
},
writeNewPostalCode() {
return this.address.writeNewPostalCode;
},
addresses() {
return this.address.loaded.addresses;
}
},
street: {
set(value) {
this.address.street = value;
},
get() {
return this.address.street;
}
},
streetNumber: {
set(value) {
this.address.streetNumber = value;
},
get() {
return this.address.streetNumber;
}
},
},
methods: {
transName(value) {
return value.streetNumber === undefined ? value.street : `${value.street}, ${value.streetNumber}`
return value.streetNumber === undefined ? value.street : `${value.streetNumber}, ${value.street}`
},
selectAddress(value) {
this.address.selected.address = value;
this.address.street = value.street;
this.address.streetNumber = value.streetNumber;
this.updateMapCenter(value.point);
},
addAddress (newAddress) {
const address = {
street: newAddress
};
this.value = address;
this.address.selected.address = address;
addAddress() {
this.address.writeNewAddress = true;
}
}
};

View File

@@ -1,6 +1,7 @@
<template>
<div class="container">
<VueMultiselect
id="citySelector"
v-model="value"
@select="selectCity"
name="field"
@@ -8,9 +9,25 @@
label="value"
:custom-label="transName"
:placeholder="$t('select_city')"
:taggable="true"
:multiple="false"
@tag="addPostalCode"
:tagPlaceholder="$t('create_postal_code')"
:options="cities">
</VueMultiselect>
</div>
<div class="custom-postcode" v-if="writeNewPostalCode">
<input
type="text"
name="name"
:placeholder="$t('postalCode_name')"
v-model="name"/>
<input
type="text"
name="code"
:placeholder="$t('postalCode_code')"
v-model="code"/>
</div>
</template>
<script>
@@ -19,24 +36,49 @@ import VueMultiselect from 'vue-multiselect';
export default {
name: 'CitySelection',
components: { VueMultiselect },
props: ['address', 'getReferenceAddresses'],
props: ['address', 'getReferenceAddresses', 'focusOnAddress'],
data() {
return {
value: null
}
},
computed: {
writeNewPostalCode() {
return this.address.writeNewPostalCode;
},
cities() {
return this.address.loaded.cities;
},
name: {
set(value) {
this.address.newPostalCode.name = value;
},
get() {
return this.address.newPostalCode.name;
}
},
code: {
set(value) {
this.address.newPostalCode.code= value;
},
get() {
return this.address.newPostalCode.code;
}
},
},
methods: {
transName(value) {
return `${value.code}-${value.name}`
},
selectCity(value) {
this.address.selected.city = value;
this.address.newPostalCode.name = value.name;
this.address.newPostalCode.code = value.code;
this.getReferenceAddresses(value);
this.focusOnAddress();
},
},
computed: {
cities() {
return this.address.loaded.cities;
addPostalCode() {
this.address.writeNewPostalCode = true;
}
}
};

View File

@@ -22,20 +22,24 @@ export default {
props: ['address', 'getCities'],
data() {
return {
value: this.address.loaded.countries.filter(c => c.countryCode === 'FR')[0]
edit: window.mode === 'edit',
defaultCountry: this.edit ? this.$store.state.editAddress.country.code : 'FR',
value: this.address.loaded.countries.filter(c => c.countryCode === this.defaultCountry)[0]
}
},
methods: {
init() {
this.value = this.edit ?
this.address.loaded.countries.filter(c => c.countryCode === this.$store.state.editAddress.country.code)[0]:
this.address.loaded.countries.filter(c => c.countryCode === 'FR')[0]
if (this.value !== undefined) {
this.getCities(this.value);
this.selectCountry(this.value);
}
},
transName ({ name }) {
return name.fr //TODO multilang
},
selectCountry(value) {
console.log(value);
this.address.selected.country = value;
this.getCities(value);
},

View File

@@ -0,0 +1,46 @@
<template>
<div class="chill_address_address chill_address_address--multiline">
<div v-if="address.text">
{{ address.text }}
</div>
<div v-if="address.postcode">
{{ address.postcode.name }}
</div>
<div v-if="address.country">
{{ address.country.name.fr }}
</div>
<div v-if="address.floor">
<span>{{ $t('floor') }}</span>: {{ address.floor }}
</div>
<div v-if="address.corridor">
<span>{{ $t('corridor') }}</span>: {{ address.corridor }}
</div>
<div v-if="address.steps">
<span>{{ $t('steps') }}</span>: {{ address.steps }}
</div>
<div v-if="address.flat">
<span>{{ $t('flat') }}</span>: {{ address.flat }}
</div>
<div v-if="address.buildingName">
<span>{{ $t('buildingName') }}</span>: {{ address.buildingName }}
</div>
<div v-if="address.extra">
<span>{{ $t('extra') }}</span>: {{ address.extra }}
</div>
<div v-if="address.distribution">
<span>{{ $t('distribution') }}</span>: {{ address.distribution }}
</div>
</div>
</template>
<script>
export default {
name: 'ShowAddress',
props: ['address'],
data() {
return {
}
},
};
</script>

View File

@@ -58,9 +58,9 @@ const messages = {
},
create: {
button: "Créer \"{q}\"",
title: "Créer à la volée…",
title: "Création d'un nouvel usager ou d'un tiers professionnel",
person: "un nouvel usager",
thirdparty: "un nouveau tiers"
thirdparty: "un nouveau tiers professionnel"
},
},
}

View File

@@ -164,10 +164,12 @@
{{ encore_entry_script_tags('ckeditor5') }}
{% endif %}
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', function(e) {
chill.checkOtherValueOnChange();
$('.select2').select2({allowClear: true});
chill.categoryLinkParentChildSelect();
</script>
{% block js%}<!-- nothing added to js -->{% endblock %}
</body>
</html>
});
</script>
{% block js%}<!-- nothing added to js -->{% endblock %}
</body>
</html>

View File

@@ -2,88 +2,169 @@
namespace Chill\MainBundle\Search;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Search\SearchPersonApiProvider;
use Chill\ThirdPartyBundle\Search\ThirdPartyApiSearch;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Chill\MainBundle\Search\SearchProvider;
use Symfony\Component\VarDumper\Resources\functions\dump;
/**
* ***Warning*** This is an incomplete implementation ***Warning***
*/
class SearchApi
{
private EntityManagerInterface $em;
private SearchProvider $search;
private PaginatorFactory $paginator;
public function __construct(EntityManagerInterface $em, SearchProvider $search)
private array $providers = [];
public function __construct(
EntityManagerInterface $em,
SearchPersonApiProvider $searchPerson,
ThirdPartyApiSearch $thirdPartyApiSearch,
PaginatorFactory $paginator
)
{
$this->em = $em;
$this->search = $search;
$this->providers[] = $searchPerson;
$this->providers[] = $thirdPartyApiSearch;
$this->paginator = $paginator;
}
/**
* @return Model/Result[]
*/
public function getResults(string $query, int $offset, int $maxResult): array
public function getResults(string $pattern, array $types, array $parameters): Collection
{
// **warning again**: this is an incomplete implementation
$results = [];
$queries = $this->findQueries($pattern, $types, $parameters);
foreach ($this->getPersons($query) as $p) {
$results[] = new Model\Result((float)\rand(0, 100) / 100, $p);
}
foreach ($this->getThirdParties($query) as $t) {
$results[] = new Model\Result((float)\rand(0, 100) / 100, $t);
}
$total = $this->countItems($queries, $types, $parameters);
$paginator = $this->paginator->create($total);
\usort($results, function(Model\Result $a, Model\Result $b) {
return ($a->getRelevance() <=> $b->getRelevance()) * -1;
});
$rawResults = $this->fetchRawResult($queries, $types, $parameters, $paginator);
return $results;
$this->prepareProviders($rawResults);
$results = $this->buildResults($rawResults);
$collection = new Collection($results, $paginator);
return $collection;
}
public function countResults(string $query): int
private function findQueries($pattern, array $types, array $parameters): array
{
return 0;
return \array_map(
fn($p) => $p->provideQuery($pattern, $parameters),
$this->findProviders($pattern, $types, $parameters),
);
}
private function getThirdParties(string $query)
private function findProviders(string $pattern, array $types, array $parameters): array
{
$thirdPartiesIds = $this->em->createQuery('SELECT t.id FROM '.ThirdParty::class.' t')
->getScalarResult();
$nbResults = rand(0, 15);
if ($nbResults === 1) {
$nbResults++;
} elseif ($nbResults === 0) {
return [];
}
$ids = \array_map(function ($e) use ($thirdPartiesIds) { return $thirdPartiesIds[$e]['id'];},
\array_rand($thirdPartiesIds, $nbResults));
$a = $this->em->getRepository(ThirdParty::class)
->findById($ids);
return $a;
return \array_filter(
$this->providers,
fn($p) => $p->supportsTypes($pattern, $types, $parameters)
);
}
private function getPersons(string $query)
private function countItems($providers, $types, $parameters): int
{
$params = [
SearchInterface::SEARCH_PREVIEW_OPTION => false
];
$search = $this->search->getResultByName($query, 'person_regular', 0, 50, $params, 'json');
$ids = \array_map(function($r) { return $r['id']; }, $search['results']);
list($countQuery, $parameters) = $this->buildCountQuery($providers, $types, $parameters);
$rsmCount = new ResultSetMappingBuilder($this->em);
$rsmCount->addScalarResult('count', 'count');
$countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
$countNq->setParameters($parameters);
return $countNq->getSingleScalarResult();
}
private function buildCountQuery(array $queries, $types, $parameters)
{
$query = "SELECT COUNT(sq.key) AS count FROM ({union_unordered}) AS sq";
$unions = [];
$parameters = [];
if (count($ids) === 0) {
return [];
foreach ($queries as $q) {
$unions[] = $q->buildQuery();
$parameters = \array_merge($parameters, $q->buildParameters());
}
return $this->em->getRepository(Person::class)
->findById($ids)
$unionUnordered = \implode(" UNION ", $unions);
return [
\strtr($query, [ '{union_unordered}' => $unionUnordered ]),
$parameters
];
}
private function buildUnionQuery(array $queries, $types, $parameters)
{
$query = "{unions} ORDER BY pertinence DESC";
$unions = [];
$parameters = [];
foreach ($queries as $q) {
$unions[] = $q->buildQuery();
$parameters = \array_merge($parameters, $q->buildParameters());
}
$union = \implode(" UNION ", $unions);
return [
\strtr($query, [ '{unions}' => $union]),
$parameters
];
}
private function fetchRawResult($queries, $types, $parameters, $paginator): array
{
list($union, $parameters) = $this->buildUnionQuery($queries, $types, $parameters, $paginator);
$rsm = new ResultSetMappingBuilder($this->em);
$rsm->addScalarResult('key', 'key', Types::STRING)
->addScalarResult('metadata', 'metadata', Types::JSON)
->addScalarResult('pertinence', 'pertinence', Types::FLOAT)
;
$nq = $this->em->createNativeQuery($union, $rsm);
$nq->setParameters($parameters);
return $nq->getResult();
}
private function prepareProviders($rawResults)
{
$metadatas = [];
foreach ($rawResults as $r) {
foreach ($this->providers as $k => $p) {
if ($p->supportsResult($r['key'], $r['metadata'])) {
$metadatas[$k][] = $r['metadata'];
break;
}
}
}
foreach ($metadatas as $k => $m) {
$this->providers[$k]->prepare($m);
}
}
private function buildResults($rawResults)
{
foreach ($rawResults as $r) {
foreach ($this->providers as $k => $p) {
if ($p->supportsResult($r['key'], $r['metadata'])) {
$items[] = (new SearchApiResult($r['pertinence']))
->setResult(
$p->getResult($r['key'], $r['metadata'], $r['pertinence'])
);
break;
}
}
}
return $items ?? [];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Chill\MainBundle\Search;
use Chill\MainBundle\Search\SearchApiQuery;
interface SearchApiInterface
{
public function provideQuery(string $pattern, array $parameters): SearchApiQuery;
public function supportsTypes(string $pattern, array $types, array $parameters): bool;
public function prepare(array $metadatas): void;
public function supportsResult(string $key, array $metadatas): bool;
public function getResult(string $key, array $metadata, float $pertinence);
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Chill\MainBundle\Search;
class SearchApiQuery
{
private ?string $selectKey;
private array $selectKeyParams = [];
private ?string $jsonbMetadata;
private array $jsonbMetadataParams = [];
private ?string $pertinence;
private array $pertinenceParams = [];
private ?string $fromClause;
private array $fromClauseParams = [];
private ?string $whereClause;
private array $whereClauseParams = [];
public function setSelectKey(string $selectKey, array $params = []): self
{
$this->selectKey = $selectKey;
$this->selectKeyParams = $params;
return $this;
}
public function setSelectJsonbMetadata(string $jsonbMetadata, array $params = []): self
{
$this->jsonbMetadata = $jsonbMetadata;
$this->jsonbMetadataParams = $params;
return $this;
}
public function setSelectPertinence(string $pertinence, array $params = []): self
{
$this->pertinence = $pertinence;
$this->pertinenceParams = $params;
return $this;
}
public function setFromClause(string $fromClause, array $params = []): self
{
$this->fromClause = $fromClause;
$this->fromClauseParams = $params;
return $this;
}
public function setWhereClause(string $whereClause, array $params = []): self
{
$this->whereClause = $whereClause;
$this->whereClauseParams = $params;
return $this;
}
public function buildQuery(): string
{
return \strtr("SELECT
'{key}' AS key,
{metadata} AS metadata,
{pertinence} AS pertinence
FROM {from}
WHERE {where}
", [
'{key}' => $this->selectKey,
'{metadata}' => $this->jsonbMetadata,
'{pertinence}' => $this->pertinence,
'{from}' => $this->fromClause,
'{where}' => $this->whereClause
]);
}
public function buildParameters(): array
{
return \array_merge(
$this->selectKeyParams,
$this->jsonbMetadataParams,
$this->pertinenceParams,
$this->fromClauseParams,
$this->whereClauseParams
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Chill\MainBundle\Search;
use Symfony\Component\Serializer\Annotation as Serializer;
class SearchApiResult
{
private float $pertinence;
private $result;
public function __construct(float $relevance)
{
$this->relevance = $relevance;
}
public function setResult($result): self
{
$this->result = $result;
return $this;
}
/**
* @Serializer\Groups({"read"})
*/
public function getResult()
{
return $this->result;
}
/**
* @Serializer\Groups({"read"})
*/
public function getRelevance(): float
{
return $this->relevance;
}
}

View File

@@ -14,8 +14,20 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
public function normalize($address, string $format = null, array $context = [])
{
$data['address_id'] = $address->getId();
$data['text'] = $address->getStreet().', '.$address->getBuildingName();
$data['text'] = $address->isNoAddress() ? '' : $address->getStreetNumber().', '.$address->getStreet();
$data['street'] = $address->getStreet();
$data['streetNumber'] = $address->getStreetNumber();
$data['postcode']['name'] = $address->getPostCode()->getName();
$data['postcode']['code'] = $address->getPostCode()->getCode();
$data['country']['name'] = $address->getPostCode()->getCountry()->getName();
$data['country']['code'] = $address->getPostCode()->getCountry()->getCountryCode();
$data['floor'] = $address->getFloor();
$data['corridor'] = $address->getCorridor();
$data['steps'] = $address->getSteps();
$data['flat'] = $address->getBuildingName();
$data['buildingName'] = $address->getFlat();
$data['distribution'] = $address->getDistribution();
$data['extra'] = $address->getExtra();
return $data;
}
@@ -25,5 +37,5 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
return $data instanceof Address;
}
}

View File

@@ -19,7 +19,6 @@ class CollectionNormalizer implements NormalizerInterface, NormalizerAwareInterf
public function normalize($collection, string $format = null, array $context = [])
{
/** @var $collection Collection */
/** @var $collection Chill\MainBundle\Pagination\PaginatorInterface */
$paginator = $collection->getPaginator();
$data['count'] = $paginator->getTotalItems();

View File

@@ -0,0 +1,36 @@
<?php
namespace Bundle\ChillMainBundle\Tests\Controller;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
class SearchApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @dataProvider generateSearchData
*/
public function testSearch(string $pattern, array $types)
{
$client = $this->getClientAuthenticated();
$client->request(
Request::METHOD_GET,
'/api/1.0/search.json',
[ 'q' => $pattern, 'type' => $types ]
);
$this->assertResponseIsSuccessful();
}
public function generateSearchData()
{
yield ['per', ['person', 'thirdparty'] ];
yield ['per', ['thirdparty'] ];
yield ['per', ['person'] ];
yield ['fjklmeqjfkdqjklrmefdqjklm', ['person', 'thirdparty'] ];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Chill\MainBundle\Tests\Controller;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class UserApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
public function testIndex()
{
$client = $this->getClientAuthenticated();
$client->request(Request::METHOD_GET, '/api/1.0/main/user.json');
$this->assertResponseIsSuccessful();
$data = \json_decode($client->getResponse()->getContent(), true);
$this->assertTrue(\array_key_exists('count', $data));
$this->assertGreaterThan(0, $data['count']);
$this->assertTrue(\array_key_exists('results', $data));
return $data['results'][0];
}
/**
* @depends testIndex
*/
public function testEntity($existingUser)
{
$client = $this->getClientAuthenticated();
$client->request(Request::METHOD_GET, '/api/1.0/main/user/'.$existingUser['id'].'.json');
$this->assertResponseIsSuccessful();
}
public function testWhoami()
{
$client = $this->getClientAuthenticated();
$client->request(Request::METHOD_GET, '/api/1.0/main/whoami.json');
$this->assertResponseIsSuccessful();
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace Chill\MainBundle\Tests\Util;
use Chill\MainBundle\Util\DateRangeCovering;
use PHPUnit\Framework\TestCase;
class DateRangeCoveringTest extends TestCase
{
public function testCoveringWithMinCover1()
{
$cover = new DateRangeCovering(1, new \DateTimeZone('Europe/Brussels'));
$cover
->add(new \DateTime('2010-01-01'), new \DateTime('2010-12-01'), 1)
->add(new \DateTime('2010-06-01'), new \DateTime('2011-06-01'), 2)
->add(new \DateTime('2019-06-01'), new \DateTime('2019-06-01'), 3)
->compute()
;
$this->assertTrue($cover->hasIntersections());
$this->assertIsArray($cover->getIntersections());
$this->assertCount(1, $cover->getIntersections());
$this->assertEquals(
new \DateTime('2010-06-01'),
$cover->getIntersections()[0][0],
"assert date start are the intersection"
);
$this->assertEquals(
new \DateTime('2010-12-01'),
$cover->getIntersections()[0][1],
"assert date end are the intersection"
);
$this->assertIsArray($cover->getIntersections()[0][2]);
$this->assertContains(1, $cover->getIntersections()[0][2]);
$this->assertContains(2, $cover->getIntersections()[0][2]);
$this->assertNotContains(3, $cover->getIntersections()[0][2]);
}
public function testCoveringWithMinCover1WithTwoIntersections()
{
$cover = new DateRangeCovering(1, new \DateTimeZone('Europe/Brussels'));
$cover
->add(new \DateTime('2010-01-01'), new \DateTime('2010-12-01'), 1)
->add(new \DateTime('2010-06-01'), new \DateTime('2011-06-01'), 2)
->add(new \DateTime('2019-01-01'), new \DateTime('2019-12-01'), 3)
->add(new \DateTime('2019-06-01'), new \DateTime('2020-06-01'), 4)
->compute()
;
$this->assertTrue($cover->hasIntersections());
$this->assertIsArray($cover->getIntersections());
$this->assertCount(2, $cover->getIntersections());
$intersections = $cover->getIntersections();
// sort the intersections to compare them in expected order
\usort($intersections, function($a, $b) {
if ($a[0] === $b[0]) {
return $a[1] <=> $b[1];
}
return $a[0] <=> $b[0];
});
// first intersection
$this->assertEquals(
new \DateTime('2010-06-01'),
$intersections[0][0],
"assert date start are the intersection"
);
$this->assertEquals(
new \DateTime('2010-12-01'),
$intersections[0][1],
"assert date end are the intersection"
);
$this->assertIsArray($intersections[0][2]);
$this->assertContains(1, $intersections[0][2]);
$this->assertContains(2, $intersections[0][2]);
$this->assertNotContains(3, $intersections[0][2]);
$this->assertNotContains(4, $intersections[0][2]);
// second intersection
$this->assertEquals(
new \DateTime('2019-06-01'),
$intersections[1][0],
"assert date start are the intersection"
);
$this->assertEquals(
new \DateTime('2019-12-01'),
$intersections[1][1],
"assert date end are the intersection"
);
$this->assertIsArray($intersections[1][2]);
$this->assertContains(3, $intersections[1][2]);
$this->assertContains(4, $intersections[1][2]);
$this->assertNotContains(1, $intersections[1][2]);
$this->assertNotContains(2, $intersections[1][2]);
}
public function testCoveringWithMinCover2()
{
$cover = new DateRangeCovering(2, new \DateTimeZone('Europe/Brussels'));
$cover
->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), 1)
->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), 2)
->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), 3)
->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), 4)
->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), 5)
->compute()
;
$this->assertTrue($cover->hasIntersections());
$this->assertIsArray($cover->getIntersections());
$this->assertCount(1, $cover->getIntersections());
$this->assertEquals(
new \DateTime('2010-06-01'),
$cover->getIntersections()[0][0],
"assert date start are the intersection"
);
$this->assertEquals(
new \DateTime('2010-09-01'),
$cover->getIntersections()[0][1],
"assert date end are the intersection"
);
$this->assertIsArray($cover->getIntersections()[0][2]);
$this->assertContains(1, $cover->getIntersections()[0][2]);
$this->assertContains(2, $cover->getIntersections()[0][2]);
$this->assertContains(3, $cover->getIntersections()[0][2]);
$this->assertNotContains(4, $cover->getIntersections()[0][2]);
$this->assertNotContains(5, $cover->getIntersections()[0][2]);
}
public function testCoveringWithMinCover2AndThreePeriodsCovering()
{
$cover = new DateRangeCovering(2, new \DateTimeZone('Europe/Brussels'));
$cover
->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), 1)
->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), 2)
->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), 3)
->add(new \DateTime('2009-01-01'), new \DateTime('2010-09-15'), 4)
->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), 5)
->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), 6)
->compute()
;
$this->assertTrue($cover->hasIntersections());
$this->assertIsArray($cover->getIntersections());
$this->assertCount(1, $cover->getIntersections());
$this->assertEquals(
new \DateTime('2010-04-01'),
$cover->getIntersections()[0][0],
"assert date start are the intersection"
);
$this->assertEquals(
new \DateTime('2010-09-15'),
$cover->getIntersections()[0][1],
"assert date end are the intersection"
);
$this->assertIsArray($cover->getIntersections()[0][2]);
$this->assertContains(1, $cover->getIntersections()[0][2]);
$this->assertContains(2, $cover->getIntersections()[0][2]);
$this->assertContains(3, $cover->getIntersections()[0][2]);
$this->assertContains(4, $cover->getIntersections()[0][2]);
$this->assertNotContains(5, $cover->getIntersections()[0][2]);
$this->assertNotContains(6, $cover->getIntersections()[0][2]);
}
public function testCoveringWithMinCover2AndThreePeriodsCoveringWithNullMetadata()
{
$cover = new DateRangeCovering(2, new \DateTimeZone('Europe/Brussels'));
$cover
->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), null)
->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), null)
->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), null)
->add(new \DateTime('2009-01-01'), new \DateTime('2010-09-15'), null)
->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), null)
->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), null)
->compute()
;
$this->assertTrue($cover->hasIntersections());
$this->assertIsArray($cover->getIntersections());
$this->assertCount(1, $cover->getIntersections());
$this->assertEquals(
new \DateTime('2010-04-01'),
$cover->getIntersections()[0][0],
"assert date start are the intersection"
);
$this->assertEquals(
new \DateTime('2010-09-15'),
$cover->getIntersections()[0][1],
"assert date end are the intersection"
);
$this->assertIsArray($cover->getIntersections()[0][2]);
}
public function testCoveringWithMinCover3Absent()
{
$cover = new DateRangeCovering(3, new \DateTimeZone('Europe/Brussels'));
$cover
->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), 1)
->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), 2)
->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), 3)
->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), 4)
->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), 5)
->compute()
;
$this->assertFalse($cover->hasIntersections());
$this->assertIsArray($cover->getIntersections());
$this->assertCount(0, $cover->getIntersections());
}
}

View File

@@ -0,0 +1,224 @@
<?php
namespace Chill\MainBundle\Util;
/**
* Utilities to compare date periods
*
* This class allow to compare periods when there are period covering. The
* argument `minCovers` allow to find also when there are more than 2 period
* which intersects.
*
* Example: a team may have maximum 2 leaders on a same period: you will
* find here all periods where there are more than 2 leaders.
*
* Usage:
*
* ```php
* $cover = new DateRangeCovering(2); // 2 means we will have periods
* // when there are 2+ periods intersecting
* $cover
* ->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), null)
* ->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), null)
* ->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), null)
* ->add(new \DateTime('2009-01-01'), new \DateTime('2010-09-15'), null)
* ->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), null)
* ->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), null)
* ->compute()
* ;
* $cover->getIntersections();
* ```
*/
class DateRangeCovering
{
private bool $computed = false;
private array $intersections = [];
private array $intervals = [];
private int $minCover;
private int $uniqueKeyCounter = 0;
private array $metadatas = [];
private array $sequence = [];
private \DateTimeZone $tz;
/**
* @param int $minCover the minimum of covering required
*/
public function __construct(int $minCover, \DateTimeZone $tz)
{
if ($minCover < 0) {
throw new \LogicException("argument minCover cannot be lower than 0");
}
$this->minCover = $minCover;
$this->tz = $tz;
}
public function add(\DateTimeInterface $start, \DateTimeInterface $end = null, $metadata = null): self
{
if ($this->computed) {
throw new \LogicException("You cannot add intervals to a computed instance");
}
$k = $this->uniqueKeyCounter++;
$this->intervals[$k] = [$start, $end];
$this->metadatas[$k] = $metadata;
$this->addToSequence($start->getTimestamp(), $k, null);
$this->addToSequence(
NULL === $end ? PHP_INT_MAX : $end->getTimestamp(), null, $k
);
return $this;
}
private function addToSequence($timestamp, int $start = null, int $end = null)
{
if (!\array_key_exists($timestamp, $this->sequence)) {
$this->sequence[$timestamp] = [ 's' => [], 'e' => [] ];
}
if (NULL !== $start) {
$this->sequence[$timestamp]['s'][] = $start;
}
if (NULL !== $end) {
$this->sequence[$timestamp]['e'][] = $end;
}
}
public function compute(): self
{
\ksort($this->sequence);
$currentPeriod = [];
$currents = [];
$isOpen = false;
$overs = [];
foreach ($this->sequence as $ts => $moves) {
$currents = \array_merge($currents, $moves['s']);
$currents = \array_diff($currents, $moves['e']);
if (count($currents) > $this->minCover && !$isOpen) {
$currentPeriod[0] = $ts;
$currentPeriod[2] = $currents;
$isOpen = true;
} elseif ($isOpen && count($currents) <= $this->minCover) {
$currentPeriod[1] = $ts;
$overs[] = $currentPeriod;
$currentPeriod = [];
$isOpen = false;
} elseif ($isOpen) {
$currentPeriod[2] = \array_merge($currentPeriod[2], $currents);
}
}
// process metadata
foreach ($overs as list($start, $end, $metadata)) {
$this->intersections[] = [
(new \DateTimeImmutable('@'.$start))
->setTimezone($this->tz),
$end === PHP_INT_MAX ? null : (new \DateTimeImmutable('@'.$end))
->setTimezone($this->tz),
\array_values(
\array_intersect_key(
$this->metadatas,
\array_flip(\array_unique($metadata))
)
)
];
}
$this->computed = true;
return $this;
}
private function process(array $intersections): array
{
$result = [];
$starts = [];
$ends = [];
$metadatas = [];
while (null !== ($current = \array_pop($intersections))) {
list($cStart, $cEnd, $cMetadata) = $current;
$n = count($cMetadata);
foreach ($intersections as list($iStart, $iEnd, $iMetadata)) {
$start = max($cStart, $iStart);
$end = min($cEnd, $iEnd);
if ($start <= $end) {
if (FALSE !== ($key = \array_search($start, $starts))) {
if ($ends[$key] === $end) {
$metadatas[$key] = \array_unique(\array_merge($metadatas[$key], $iMetadata));
continue;
}
}
$starts[] = $start;
$ends[] = $end;
$metadatas[] = \array_unique(\array_merge($iMetadata, $cMetadata));
}
}
}
// recompose results
foreach ($starts as $k => $start) {
$result[] = [$start, $ends[$k], \array_unique($metadatas[$k])];
}
return $result;
}
private function addToIntersections(array $intersections, array $intersection)
{
$foundExisting = false;
list($nStart, $nEnd, $nMetadata) = $intersection;
\array_walk($intersections,
function(&$i, $key) use ($nStart, $nEnd, $nMetadata, $foundExisting) {
if ($foundExisting) {
return;
};
if ($i[0] === $nStart && $i[1] === $nEnd) {
$foundExisting = true;
$i[2] = \array_merge($i[2], $nMetadata);
}
}
);
if (!$foundExisting) {
$intersections[] = $intersection;
}
return $intersections;
}
public function hasIntersections(): bool
{
if (!$this->computed) {
throw new \LogicException(sprintf("You cannot call the method %s before ".
"'process'", __METHOD));
}
return count($this->intersections) > 0;
}
public function getIntersections(): array
{
if (!$this->computed) {
throw new \LogicException(sprintf("You cannot call the method %s before ".
"'process'", __METHOD));
}
return $this->intersections;
}
}

View File

@@ -10,6 +10,19 @@ servers:
components:
schemas:
User:
type: object
properties:
id:
type: integer
type:
type: string
enum:
- user
username:
type: string
text:
type: string
Center:
type: object
properties:
@@ -17,6 +30,93 @@ components:
type: integer
name:
type: string
Address:
type: object
properties:
address_id:
type: integer
text:
type: string
postcode:
type: object
properties:
name:
type: string
Country:
type: object
properties:
id:
type: integer
name:
type: object
countryCode:
type: string
PostalCode:
type: object
properties:
id:
type: integer
name:
type: string
code:
type: string
country:
type: object
properties:
id:
type: integer
name:
type: object
countryCode:
type: string
AddressReference:
type: object
properties:
id:
type: integer
refId:
type: string
street:
type: string
streetNumber:
type: string
postcode:
type: object
properties:
id:
type: integer
name:
type: string
code:
type: string
country:
type: object
properties:
id:
type: integer
name:
type: object
countryCode:
type: string
municipalityCode:
type: string
source:
type: string
point:
type: object
properties:
type:
type: string
coordinates:
type: array
items:
type: number
minItems: 2
maxItems: 2
paths:
/1.0/search.json:
@@ -29,7 +129,7 @@ paths:
description: >
**Warning**: This is currently a stub (not really implemented
The search is performed across multiple entities. The entities must be listed into
The search is performed across multiple entities. The entities must be listed into
`type` parameters.
The results are ordered by relevance, from the most to the lowest relevant.
@@ -55,4 +155,329 @@ paths:
responses:
200:
description: "OK"
/1.0/main/address.json:
get:
tags:
- address
summary: Return a list of all Chill addresses
responses:
200:
description: "ok"
post:
tags:
- address
summary: create a new address
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
buildingName:
type: string
corridor:
type: string
distribution:
type: string
extra:
type: string
flat:
type: string
floor:
type: string
isNoAddress:
type: boolean
point:
type: array
items:
type: number
minItems: 2
maxItems: 2
postcode:
$ref: '#/components/schemas/PostalCode'
steps:
type: string
street:
type: string
streetNumber:
type: string
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
patch:
tags:
- address
summary: patch an address
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
buildingName:
type: string
corridor:
type: string
distribution:
type: string
extra:
type: string
flat:
type: string
floor:
type: string
isNoAddress:
type: boolean
point:
type: array
items:
type: number
minItems: 2
maxItems: 2
postcode:
$ref: '#/components/schemas/PostalCode'
steps:
type: string
street:
type: string
streetNumber:
type: string
validFrom:
type: string
validTo:
type: string
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/main/address/{id}.json:
get:
tags:
- address
summary: Return an address by id
parameters:
- name: id
in: path
required: true
description: The address id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Address'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/address-reference.json:
get:
tags:
- address
summary: Return a list of all reference addresses
parameters:
- in: query
name: postal_code
required: false
schema:
type: integer
description: The id of a postal code to filter the reference addresses
responses:
200:
description: "ok"
/1.0/main/address-reference/{id}.json:
get:
tags:
- address
summary: Return a reference address by id
parameters:
- name: id
in: path
required: true
description: The reference address id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/AddressReference'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/postal-code.json:
get:
tags:
- address
summary: Return a list of all postal-code
parameters:
- in: query
name: country
required: false
schema:
type: integer
description: The id of a country to filter the postal code
responses:
200:
description: "ok"
post:
tags:
- address
summary: create a new PostalCode
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
code:
type: string
country:
$ref: '#/components/schemas/Country'
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/main/postal-code/{id}.json:
get:
tags:
- address
summary: Return a postal code by id
parameters:
- name: id
in: path
required: true
description: The postal code id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/PostalCode'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/country.json:
get:
tags:
- address
summary: Return a list of all countries
responses:
200:
description: "ok"
/1.0/main/country/{id}.json:
get:
tags:
- address
summary: Return a country by id
parameters:
- name: id
in: path
required: true
description: The country id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Country'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/user.json:
get:
tags:
- user
summary: Return a list of all user
responses:
200:
description: "ok"
/1.0/main/whoami.json:
get:
tags:
- user
summary: Return the currently authenticated user
responses:
200:
description: "ok"
/1.0/main/user/{id}.json:
get:
tags:
- user
summary: Return a user by id
parameters:
- name: id
in: path
required: true
description: The user id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/User'
404:
description: "not found"
401:
description: "Unauthorized"

View File

@@ -56,7 +56,6 @@ module.exports = function(encore, entries)
// Chill2 new assets
encore.addEntry('forkawesome', __dirname + '/Resources/public/modules/forkawesome/index.js');
encore.addEntry('bootstrap', __dirname + '/Resources/public/modules/bootstrap/index.js');
//encore.addEntry('vuejs', __dirname + '/Resources/public/modules/vue/index.js');
// CKEditor5
buildCKEditor(encore);

View File

@@ -58,13 +58,14 @@ services:
arguments:
- "@chill.main.helper.translatable_string"
- '@Symfony\Component\Routing\Generator\UrlGeneratorInterface'
- '@chill.main.form.choice_loader.postal_code'
- '@Chill\MainBundle\Form\ChoiceLoader\PostalCodeChoiceLoader'
- '@Symfony\Component\Translation\TranslatorInterface'
tags:
- { name: form.type }
chill.main.form.choice_loader.postal_code:
class: Chill\MainBundle\Form\ChoiceLoader\PostalCodeChoiceLoader
Chill\MainBundle\Form\ChoiceLoader\PostalCodeChoiceLoader:
autowire: true
autoconfigure: true
chill.main.form.type.export:
class: Chill\MainBundle\Form\Type\Export\ExportType

View File

@@ -5,6 +5,5 @@ services:
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
Chill\MainBundle\Search\SearchApi:
arguments:
$em: '@Doctrine\ORM\EntityManagerInterface'
$search: '@Chill\MainBundle\Search\SearchProvider'
autowire: true
autoconfigure: true

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210616134328 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_postal_code ADD origin INT DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_postal_code DROP origin');
}
}

View File

@@ -6,6 +6,7 @@ use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -116,6 +117,20 @@ $workflow = $this->registry->get($accompanyingPeriod);
return $this->addRemoveSomething('socialissue', $id, $request, $_format, 'socialIssue', SocialIssue::class, [ 'groups' => [ 'read' ] ]);
}
public function workApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething(
'work',
$id,
$request,
$_format,
'work',
AccompanyingPeriodWork::class,
[ 'groups' => [ 'accompanying_period_work:create' ] ],
true // force persist
);
}
public function requestorApi($id, Request $request, string $_format): Response
{
/** @var AccompanyingPeriod $accompanyingPeriod */

View File

@@ -2,10 +2,12 @@
namespace Chill\PersonBundle\Controller;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
@@ -34,14 +36,18 @@ class AccompanyingCourseController extends Controller
protected ValidatorInterface $validator;
private AccompanyingPeriodWorkRepository $workRepository;
public function __construct(
SerializerInterface $serializer,
EventDispatcherInterface $dispatcher,
ValidatorInterface $validator
ValidatorInterface $validator,
AccompanyingPeriodWorkRepository $workRepository
) {
$this->serializer = $serializer;
$this->dispatcher = $dispatcher;
$this->validator = $validator;
$this->workRepository = $workRepository;
}
/**
@@ -75,7 +81,7 @@ class AccompanyingCourseController extends Controller
return $this->redirectToRoute('chill_person_accompanying_course_edit', [
'accompanying_period_id' => $period->getId()
]);
}
/**
@@ -86,8 +92,32 @@ class AccompanyingCourseController extends Controller
*/
public function indexAction(AccompanyingPeriod $accompanyingCourse): Response
{
// compute some warnings
// get persons without household
$withoutHousehold = [];
foreach ($accompanyingCourse->getParticipations() as
$p) {
if (FALSE === $p->getPerson()->isSharingHousehold()) {
$withoutHousehold[] = $p->getPerson();
}
}
$activities = $this->getDoctrine()->getManager()->getRepository(Activity::class)->findBy(
['accompanyingPeriod' => $accompanyingCourse],
['date' => 'DESC'],
);
$works = $this->workRepository->findByAccompanyingPeriod(
$accompanyingCourse,
['startDate' => 'DESC', 'endDate' => 'DESC'],
3
);
return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [
'accompanyingCourse' => $accompanyingCourse
'accompanyingCourse' => $accompanyingCourse,
'withoutHousehold' => $withoutHousehold,
'works' => $works,
'activities' => $activities
]);
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
class AccompanyingCourseWorkApiController extends ApiController
{
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
{
switch($action) {
case '_entity':
switch ($request->getMethod()) {
case Request::METHOD_PUT:
return [ 'groups' => [ 'accompanying_period_work:edit' ] ];
}
}
return parent::getContextForSerialization($action, $request, $_format, $entity);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
class AccompanyingCourseWorkController extends AbstractController
{
private TranslatorInterface $trans;
private SerializerInterface $serializer;
private AccompanyingPeriodWorkRepository $workRepository;
private PaginatorFactory $paginator;
public function __construct(
TranslatorInterface $trans,
SerializerInterface $serializer,
AccompanyingPeriodWorkRepository $workRepository,
PaginatorFactory $paginator
) {
$this->trans = $trans;
$this->serializer = $serializer;
$this->workRepository = $workRepository;
$this->paginator = $paginator;
}
/**
* @Route(
* "{_locale}/person/accompanying-period/{id}/work/new",
* name="chill_person_accompanying_period_work_new",
* methods={"GET"}
* )
*/
public function createWork(AccompanyingPeriod $period): Response
{
// TODO ACL
if ($period->getSocialIssues()->count() === 0) {
$this->addFlash('error', $this->trans->trans(
"accompanying_work.You must add at least ".
"one social issue on accompanying period")
);
return $this->redirectToRoute('chill_person_accompanying_course_index', [
'accompanying_period_id' => $period->getId()
]);
}
$json = $this->serializer->normalize($period, 'json', [ "groups" => [ "read" ]]);
return $this->render('@ChillPerson/AccompanyingCourseWork/create.html.twig', [
'accompanyingCourse' => $period,
'json' => $json
]);
}
/**
* @Route(
* "{_locale}/person/accompanying-period/work/{id}/edit",
* name="chill_person_accompanying_period_work_edit",
* methods={"GET"}
* )
*/
public function editWork(AccompanyingPeriodWork $work): Response
{
// TODO ACL
$json = $this->serializer->normalize($work, 'json', [ "groups" => [ "read" ] ]);
return $this->render('@ChillPerson/AccompanyingCourseWork/edit.html.twig', [
'accompanyingCourse' => $work->getAccompanyingPeriod(),
'work' => $work,
'json' => $json
]);
}
/**
* @Route(
* "{_locale}/person/accompanying-period/{id}/work",
* name="chill_person_accompanying_period_work_list",
* methods={"GET"}
* )
*/
public function listWorkByAccompanyingPeriod(AccompanyingPeriod $period): Response
{
// TODO ACL
$totalItems = $this->workRepository->countByAccompanyingPeriod($period);
$paginator = $this->paginator->create($totalItems);
$works = $this->workRepository->findByAccompanyingPeriod(
$period,
['startDate' => 'DESC', 'endDate' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render('@ChillPerson/AccompanyingCourseWork/list_by_accompanying_period.html.twig', [
'accompanyingCourse' => $period,
'works' => $works,
'paginator' => $paginator
]);
}
}

View File

@@ -47,12 +47,12 @@ class AccompanyingPeriodController extends AbstractController
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* @var ValidatorInterface
*/
protected $validator;
/**
* AccompanyingPeriodController constructor.
*
@@ -64,23 +64,25 @@ class AccompanyingPeriodController extends AbstractController
$this->eventDispatcher = $eventDispatcher;
$this->validator = $validator;
}
public function listAction(int $person_id): Response
{
$person = $this->_getPerson($person_id);
$event = new PrivacyEvent($person, [
'element_class' => AccompanyingPeriod::class,
'action' => 'list'
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
$accompanyingPeriods = $person->getAccompanyingPeriodsOrdered();
return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [
'accompanying_periods' => $person->getAccompanyingPeriodsOrdered(),
'accompanying_periods' => $accompanyingPeriods,
'person' => $person
]);
}
public function createAction(int $person_id, Request $request): Response
{
$person = $this->_getPerson($person_id);
@@ -90,17 +92,17 @@ class AccompanyingPeriodController extends AbstractController
$accompanyingPeriod = new AccompanyingPeriod(new \DateTime('now'));
$accompanyingPeriod->setClosingDate(new \DateTime('now'));
$accompanyingPeriod->addPerson($person);
//or $person->addAccompanyingPeriod($accompanyingPeriod);
$form = $this->createForm(
AccompanyingPeriodType::class,
$accompanyingPeriod, [
'period_action' => 'create',
'center' => $person->getCenter()
]);
if ($request->getMethod() === 'POST') {
$form->handleRequest($request);
$errors = $this->_validatePerson($person);
@@ -120,7 +122,7 @@ class AccompanyingPeriodController extends AbstractController
$this->generateUrl('chill_person_accompanying_period_list', [
'person_id' => $person->getId()
]));
} else {
$flashBag->add('error', $this->get('translator')
->trans('Error! Period not created!'));
@@ -137,7 +139,7 @@ class AccompanyingPeriodController extends AbstractController
'accompanying_period' => $accompanyingPeriod
]);
}
/**
* @throws Exception
*/
@@ -154,7 +156,7 @@ class AccompanyingPeriodController extends AbstractController
/** @var Person $person */
$person = $this->_getPerson($person_id);
// CHECK
if (! $accompanyingPeriod->containsPerson($person)) {
throw new Exception("Accompanying period " . $period_id . " does not contain person " . $person_id);
@@ -176,7 +178,7 @@ class AccompanyingPeriodController extends AbstractController
if ($form->isValid(['Default', 'closed'])
&& count($errors) === 0) {
$em->flush();
$flashBag->add('success',
@@ -186,9 +188,9 @@ class AccompanyingPeriodController extends AbstractController
$this->generateUrl('chill_person_accompanying_period_list', [
'person_id' => $person->getId()
]));
} else {
$flashBag->add('error', $this->get('translator')
->trans('Error when updating the period'));
@@ -204,19 +206,19 @@ class AccompanyingPeriodController extends AbstractController
'accompanying_period' => $accompanyingPeriod
]);
}
/**
* @throws \Exception
*/
public function closeAction(int $person_id, Request $request): Response
{
$person = $this->_getPerson($person_id);
$this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person, 'You are not allowed to update this person');
if ($person->isOpen() === false) {
$this->get('session')->getFlashBag()
->add('error', $this->get('translator')
->trans('Beware period is closed', ['%name%' => $person->__toString()]
@@ -229,7 +231,7 @@ class AccompanyingPeriodController extends AbstractController
}
$current = $person->getCurrentAccompanyingPeriod();
$form = $this->createForm(AccompanyingPeriodType::class, $current, [
'period_action' => 'close',
'center' => $person->getCenter()
@@ -256,7 +258,7 @@ class AccompanyingPeriodController extends AbstractController
'person_id' => $person->getId()
])
);
} else {
$this->get('session')->getFlashBag()
->add('error', $this->get('translator')
@@ -267,7 +269,7 @@ class AccompanyingPeriodController extends AbstractController
->add('info', $error->getMessage());
}
}
} else { //if form is not valid
$this->get('session')->getFlashBag()
->add('error',
@@ -288,7 +290,7 @@ class AccompanyingPeriodController extends AbstractController
'accompanying_period' => $current
]);
}
private function _validatePerson(Person $person): ConstraintViolationListInterface
{
$errors = $this->validator->validate($person, null,
@@ -296,10 +298,10 @@ class AccompanyingPeriodController extends AbstractController
// Can be disabled with config
if (false === $this->container->getParameter('chill_person.allow_multiple_simultaneous_accompanying_periods')) {
$errors_accompanying_period = $this->validator->validate($person, null,
['accompanying_period_consistent']);
foreach($errors_accompanying_period as $error ) {
$errors->add($error);
}
@@ -307,7 +309,7 @@ class AccompanyingPeriodController extends AbstractController
return $errors;
}
public function openAction(int $person_id, Request $request): Response
{
$person = $this->_getPerson($person_id);
@@ -384,7 +386,7 @@ class AccompanyingPeriodController extends AbstractController
'accompanying_period' => $accompanyingPeriod
]);
}
public function reOpenAction(int $person_id, int $period_id, Request $request): Response
{
/** @var Person $person */
@@ -392,7 +394,7 @@ class AccompanyingPeriodController extends AbstractController
/* @var $period AccompanyingPeriod */
$period = \array_filter(
$person->getAccompanyingPeriods(),
$person->getAccompanyingPeriods(),
function (AccompanyingPeriod $p) use ($period_id) {
return $p->getId() === ($period_id);
}
@@ -417,13 +419,13 @@ class AccompanyingPeriodController extends AbstractController
return $this->redirectToRoute('chill_person_accompanying_period_list', [
'person_id' => $person->getId()
]);
} elseif ($confirm === false && $period->canBeReOpened($person)) {
return $this->render('ChillPersonBundle:AccompanyingPeriod:re_open.html.twig', [
'period' => $period,
'person' => $person
]);
} else {
return (new Response())
->setStatusCode(Response::HTTP_BAD_REQUEST)

View File

@@ -0,0 +1,53 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class HouseholdApiController extends ApiController
{
private HouseholdRepository $householdRepository;
public function __construct(HouseholdRepository $householdRepository)
{
$this->householdRepository = $householdRepository;
}
public function householdAddressApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]);
}
/**
* Find Household of people participating to the same AccompanyingPeriod
*
* @ParamConverter("person", options={"id" = "person_id"})
*/
public function suggestHouseholdByAccompanyingPeriodParticipationApi(Person $person, string $_format)
{
// TODO add acl
$count = $this->householdRepository->countByAccompanyingPeriodParticipation($person);
$paginator = $this->getPaginatorFactory()->create($count);
if ($count === 0) {
$households = [];
} else {
$households = $this->householdRepository->findByAccompanyingPeriodParticipation($person,
$paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
}
$collection = new Collection($households, $paginator);
return $this->json($collection, Response::HTTP_OK, [],
[ "groups" => ["read"]]);
}
}

View File

@@ -2,19 +2,35 @@
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Form\HouseholdType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatorInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Repository\Household\PositionRepository;
/**
* @Route("/{_locale}/person/household")
*/
class HouseholdController extends AbstractController
{
private TranslatorInterface $translator;
private PositionRepository $positionRepository;
public function __construct(TranslatorInterface $translator, PositionRepository $positionRepository)
{
$this->translator = $translator;
$this->positionRepository = $positionRepository;
}
/**
* @Route(
* "/{household_id}/summary",
@@ -27,48 +43,51 @@ class HouseholdController extends AbstractController
{
// TODO ACL
$positions = $this->getDoctrine()->getManager()
->getRepository(Position::class)
->findAll()
$positions = $this->positionRepository
->findByActiveOrdered()
;
// little performance improvement:
// little performance improvement:
// initialize members collection, which will avoid
// some queries
$household->getMembers()->initialize();
if ($request->query->has('edit')) {
$form = $this->createMetadataForm($household);
} else {
$form = null;
}
return $this->render('@ChillPerson/Household/summary.html.twig',
[
'household' => $household,
'positions' => $positions
'positions' => $positions,
'form' => NULL !== $form ? $form->createView() : null,
]
);
}
/**
* @Route(
* "/{household_id}/members",
* name="chill_person_household_members",
* "/{household_id}/accompanying-period",
* name="chill_person_household_accompanying_period",
* methods={"GET", "HEAD"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function members(Request $request, Household $household)
public function accompanyingPeriod(Request $request, Household $household)
{
// TODO ACL
$positions = $this->getDoctrine()->getManager()
->getRepository(Position::class)
->findAll()
;
// little performance improvement:
// initialize members collection, which will avoid
// some queries
$household->getMembers()->initialize();
$members = $household->getMembers();
foreach($members as $m) {
$accompanyingPeriods = $m->getPerson()->getAccompanyingPeriods();
}
return $this->render('@ChillPerson/Household/members.html.twig',
return $this->render('@ChillPerson/Household/accompanying_period.html.twig',
[
'household' => $household,
'positions' => $positions
'accompanying_periods' => $accompanyingPeriods
]
);
}
@@ -84,6 +103,16 @@ class HouseholdController extends AbstractController
public function addresses(Request $request, Household $household)
{
// TODO ACL
//TODO put these lines into a validator constraint on household->getAddress
$addresses = $household->getAddresses();
$cond = True;
for ($i=0; $i < count($addresses) - 1; $i++) {
if ($addresses[$i]->getValidFrom() != $addresses[$i + 1]->getValidTo()) {
$cond = False;
}
}
return $this->render('@ChillPerson/Household/addresses.html.twig',
[
'household' => $household
@@ -91,6 +120,7 @@ class HouseholdController extends AbstractController
);
}
/**
* @Route(
* "/{household_id}/address/move",
@@ -102,10 +132,83 @@ class HouseholdController extends AbstractController
public function addressMove(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/address_move.html.twig',
[
'household' => $household
]
);
}
/**
* @Route(
* "/{household_id}/address/edit",
* name="chill_person_household_address_edit",
* methods={"GET", "HEAD", "POST"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function addressEdit(Request $request, Household $household)
{
// TODO ACL
//$address = $this->findAddressById($household, $address_id); //TODO
return $this->render('@ChillPerson/Household/address_edit.html.twig',
[
'household' => $household,
//'address' => $address,
]
);
}
/**
* @Route(
* "/{household_id}/members/metadata/edit",
* name="chill_person_household_members_metadata_edit",
* methods={"GET", "POST"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function editHouseholdMetadata(Request $request, Household $household)
{
// TODO ACL
$form = $this->createMetadataForm($household);
$form->handleRequest($request);
if ($form->isSubmitted() and $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', $this->translator->trans('household.data_saved'));
return $this->redirectToRoute('chill_person_household_summary', [
'household_id' => $household->getId()
]);
}
return $this->render('@ChillPerson/Household/edit_member_metadata.html.twig', [
'household' => $household,
'form' => $form->createView()
]);
}
private function createMetadataForm(Household $household): FormInterface
{
$form = $this->createForm(
HouseholdType::class,
$household,
[
'action' => $this->generateUrl(
'chill_person_household_members_metadata_edit',
[
'household_id' => $household->getId()
]
)
]
);
return $form;
}
}

View File

@@ -3,6 +3,7 @@
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
@@ -25,11 +26,17 @@ class HouseholdMemberController extends ApiController
private TranslatorInterface $translator;
private AccompanyingPeriodRepository $periodRepository;
public function __construct(UrlGeneratorInterface $generator, TranslatorInterface $translator)
public function __construct(
UrlGeneratorInterface $generator,
TranslatorInterface $translator,
AccompanyingPeriodRepository $periodRepository
)
{
$this->generator = $generator;
$this->translator = $translator;
$this->periodRepository = $periodRepository;
}
/**
@@ -50,8 +57,12 @@ class HouseholdMemberController extends ApiController
// TODO ACL
//
// TODO validation
//
$errors = $editor->validate();
if (count($errors) > 0) {
return $this->json($errors, 422);
}
$em = $this->getDoctrine()->getManager();
// if new household, persist it
@@ -140,8 +151,23 @@ class HouseholdMemberController extends ApiController
'allowLeaveWithoutHousehold' => $allowLeaveWithoutHousehold ?? $request->query->has('allow_leave_without_household'),
];
// context
if ($request->query->has('accompanying_period_id')) {
$period = $this->periodRepository->find(
$request->query->getInt('accompanying_period_id')
);
if ($period === null) {
throw $this->createNotFoundException('period not found');
}
// TODO add acl on accompanying Course
}
return $this->render('@ChillPerson/Household/members_editor.html.twig', [
'data' => $data
'data' => $data,
'expandSuggestions' => (int) $request->query->getBoolean('expand_suggestions', false),
'accompanyingCourse' => $period ?? null,
]);
}
@@ -155,7 +181,9 @@ class HouseholdMemberController extends ApiController
{
// TODO ACL
$form = $this->createForm(HouseholdMemberType::class, $member);
$form = $this->createForm(HouseholdMemberType::class, $member, [
'validation_groups' => [ 'household_memberships' ]
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
@@ -167,7 +195,7 @@ class HouseholdMemberController extends ApiController
return $this->redirect(
$request->get('returnPath', null) ??
$this->generator->generate('chill_person_household_members', [ 'household_id' =>
$this->generator->generate('chill_person_household_summary', [ 'household_id' =>
$member->getHousehold()->getId() ])
);
}

View File

@@ -45,7 +45,7 @@ class PersonAddressController extends AbstractController
* @var ValidatorInterface
*/
protected $validator;
/**
* PersonAddressController constructor.
*
@@ -55,7 +55,7 @@ class PersonAddressController extends AbstractController
{
$this->validator = $validator;
}
public function listAction($person_id)
{
$person = $this->getDoctrine()->getManager()
@@ -297,18 +297,16 @@ class PersonAddressController extends AbstractController
*/
protected function findAddressById(Person $person, $address_id)
{
// filtering address
$criteria = Criteria::create()
->where(Criteria::expr()->eq('id', $address_id))
->setMaxResults(1);
$addresses = $person->getAddresses()->matching($criteria);
$address = $this->getDoctrine()->getManager()
->getRepository(Address::class)
->find($address_id)
;
if (count($addresses) === 0) {
throw $this->createNotFoundException("Address with id $address_id "
. "matching person $person_id not found ");
if (!$person->getAddresses()->contains($address)) {
throw $this->createAccessDeniedException("Not allowed to see this address");
}
return $addresses->first();
return $address;
}
/**

View File

@@ -22,7 +22,8 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Role\Role;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Chill\MainBundle\Entity\Address;
class PersonApiController extends ApiController
{
@@ -35,7 +36,7 @@ class PersonApiController extends ApiController
{
$this->authorizationHelper = $authorizationHelper;
}
protected function createEntity(string $action, Request $request): object
{
$person = parent::createEntity($action, $request);
@@ -47,4 +48,10 @@ class PersonApiController extends ApiController
return $person;
}
public function personAddressApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]);
}
}

View File

@@ -41,6 +41,8 @@ use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Repository\PersonNotDuplicateRepository;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
final class PersonController extends AbstractController
{
@@ -416,4 +418,29 @@ final class PersonController extends AbstractController
return $person;
}
/**
*
* @Route(
* "/{_locale}/person/household/{person_id}/history",
* name="chill_person_household_person_history",
* methods={"GET", "POST"}
* )
* @ParamConverter("person", options={"id" = "person_id"})
*/
public function householdHistoryByPerson(Request $request, Person $person): Response
{
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person,
"You are not allowed to see this person.");
$event = new PrivacyEvent($person);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
return $this->render(
'@ChillPerson/Person/household_history.html.twig',
[
'person' => $person
]
);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class SocialIssueApiController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $query): void
{
$query->where(
$query->expr()->orX(
$query->expr()->gt('e.desactivationDate', ':now'),
$query->expr()->isNull('e.desactivationDate')
)
);
$query->setParameter('now', new \DateTimeImmutable());
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Repository\SocialWork\GoalRepository;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class SocialWorkGoalApiController extends ApiController
{
private GoalRepository $goalRepository;
private PaginatorFactory $paginator;
public function __construct(GoalRepository $goalRepository, PaginatorFactory $paginator)
{
$this->goalRepository = $goalRepository;
$this->paginator = $paginator;
}
public function listBySocialAction(Request $request, SocialAction $action): Response
{
$totalItems = $this->goalRepository->countBySocialActionWithDescendants($action);
$paginator = $this->getPaginatorFactory()->create($totalItems);
$entities = $this->goalRepository->findBySocialActionWithDescendants($action, ["id" => "ASC"],
$paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
$model = new Collection($entities, $paginator);
return $this->json($model, Response::HTTP_OK, [], [ "groups" => [ "read" ]]);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Repository\SocialWork\ResultRepository;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class SocialWorkResultApiController extends ApiController
{
private ResultRepository $resultRepository;
public function __construct(ResultRepository $resultRepository)
{
$this->resultRepository = $resultRepository;
}
public function listBySocialAction(Request $request, SocialAction $action): Response
{
$totalItems = $this->resultRepository->countBySocialActionWithDescendants($action);
$paginator = $this->getPaginatorFactory()->create($totalItems);
$entities = $this->resultRepository->findBySocialActionWithDescendants($action, ["id" => "ASC"],
$paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
$model = new Collection($entities, $paginator);
return $this->json($model, Response::HTTP_OK, [], [ "groups" => [ "read" ]]);
}
public function listByGoal(Request $request, Goal $goal): Response
{
$totalItems = $this->resultRepository->countByGoal($goal);
$paginator = $this->getPaginatorFactory()->create($totalItems);
$entities = $this->resultRepository->findByGoal($goal, ["id" => "ASC"],
$paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
$model = new Collection($entities, $paginator);
return $this->json($model, Response::HTTP_OK, [], [ "groups" => [ "read" ]]);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
class SocialWorkSocialActionApiController extends ApiController
{
private SocialIssueRepository $socialIssueRepository;
private PaginatorFactory $paginator;
/**
* @param SocialIssueRepository $socialIssueRepository
*/
public function __construct(SocialIssueRepository $socialIssueRepository, PaginatorFactory $paginator)
{
$this->socialIssueRepository = $socialIssueRepository;
$this->paginator = $paginator;
}
public function listBySocialIssueApi($id, Request $request)
{
$socialIssue = $this->socialIssueRepository
->find($id);
if (NULL === $socialIssue) {
throw $this->createNotFoundException("socialIssue not found");
}
$socialActions = $socialIssue->getRecursiveSocialActions();
$pagination = $this->paginator->create(count($socialActions));
// max one page
$pagination->setItemsPerPage(count($socialActions));
$collection = new Collection($socialActions, $pagination);
return $this->json($collection, JsonResponse::HTTP_OK, [], [ "groups" => [ "read" ]]);
}
}

View File

@@ -4,11 +4,15 @@ namespace Chill\PersonBundle\DataFixtures\ORM;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Household\MembersEditorFactory;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Nelmio\Alice\Loader\NativeLoader;
use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes;
class LoadHousehold extends Fixture implements DependentFixtureInterface
{
@@ -16,12 +20,15 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
private EntityManagerInterface $em;
private NativeLoader $loader;
private CONST NUMBER_OF_HOUSEHOLD = 10;
public function __construct(MembersEditorFactory $editorFactory, EntityManagerInterface $em)
{
$this->editorFactory = $editorFactory;
$this->em = $em;
$this->loader = new NativeLoader();
}
public function load(ObjectManager $manager)
@@ -53,6 +60,8 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
$household = new Household();
$manager->persist($household);
$this->addAddressToHousehold($household, clone $startDate, $manager);
$movement = $this->editorFactory->createEditor($household);
// load adults
@@ -89,6 +98,55 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
}
}
private function addAddressToHousehold(Household $household, \DateTimeImmutable $date, ObjectManager $manager)
{
if (\random_int(0, 10) > 8) {
// 20% of household without address
return;
}
$nb = \random_int(1, 6);
$i = 0;
while ($i < $nb) {
$address = $this->createAddress();
$address->setValidFrom(\DateTime::createFromImmutable($date));
if (\random_int(0, 20) < 1) {
$date = $date->add(new \DateInterval('P'.\random_int(8, 52).'W'));
$address->setValidTo(\DateTime::createFromImmutable($date));
}
$household->addAddress($address);
$manager->persist($address);
$date = $date->add(new \DateInterval('P'.\random_int(8, 52).'W'));
$i++;
}
}
private function createAddress(): Address
{
$objectSet = $this->loader->loadData([
Address::class => [
'address1' => [
'street' => '<fr_FR:streetName()>',
'streetNumber' => '<fr_FR:buildingNumber()>',
'postCode' => $this->getPostalCode()
]
]
]);
return $objectSet->getObjects()['address1'];
}
private function getPostalCode(): PostalCode
{
$ref = LoadPostalCodes::$refs[\array_rand(LoadPostalCodes::$refs)];
return $this->getReference($ref);
}
private function preparePersonIds()
{
$this->personIds = $this->em

View File

@@ -201,7 +201,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'workflows' => [
'accompanying_period_lifecycle' => [
'type' => 'state_machine',
'audit_trail' => [
'audit_trail' => [
'enabled' => true
],
'marking_store' => [
@@ -354,15 +354,15 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class,
'actions' => [
'_entity' => [
'roles' => [
'roles' => [
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_PATCH => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_PUT => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
],
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true,
Request::METHOD_GET => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true,
]
],
'participation' => [
@@ -379,73 +379,86 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
],
'resource' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'comment' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'requestor' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'scope' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'socialissue' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'socialIssueApi',
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'work' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => false,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'workApi',
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE => 'ALWAYS_FAILS',
]
],
'confirm' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
]
],
@@ -475,6 +488,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
[
'class' => \Chill\PersonBundle\Entity\SocialWork\SocialIssue::class,
'name' => 'social_work_social_issue',
'controller' => \Chill\PersonBundle\Controller\SocialIssueApiController::class,
'base_path' => '/api/1.0/person/social-work/social-issue',
'base_role' => 'ROLE_USER',
'actions' => [
@@ -510,14 +524,61 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_HEAD => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE,
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE,
]
],
],
'address' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'personAddressApi'
],
]
],
[
'class' => \Chill\PersonBundle\Entity\Household\Household::class,
'controller' => \Chill\PersonBundle\Controller\HouseholdApiController::class,
'name' => 'household',
'base_path' => '/api/1.0/person/household',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
]
],
'address' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'householdAddressApi'
],
'suggestHouseholdByAccompanyingPeriodParticipation' => [
'path' => '/suggest/by-person/{person_id}/through-accompanying-period-participation.{_format}',
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
]
]
]
],
[
'class' => \Chill\PersonBundle\Entity\SocialWork\SocialAction::class,
'name' => 'social_action',
'base_path' => '/api/1.0/person/social/social-action',
'controller' => \Chill\PersonBundle\Controller\SocialWorkSocialActionApiController::class,
// TODO: acl
'base_role' => 'ROLE_USER',
'actions' => [
@@ -531,6 +592,150 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_HEAD => 'ROLE_USER',
]
],
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
'listBySocialIssue' => [
'single-collection' => 'collection',
'path' => '/by-social-issue/{id}.{_format}',
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
]
]
],
[
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork::class,
'name' => 'accompanying_period_work',
'base_path' => '/api/1.0/person/accompanying-course/work',
'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseWorkApiController::class,
// TODO: acl
'base_role' => 'ROLE_USER',
'actions' => [
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
Request::METHOD_PATCH => true,
Request::METHOD_PUT => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
Request::METHOD_PATCH => 'ROLE_USER',
Request::METHOD_PUT => 'ROLE_USER',
]
],
]
],
[
'class' => \Chill\PersonBundle\Entity\SocialWork\Result::class,
'controller' => \Chill\PersonBundle\Controller\SocialWorkResultApiController::class,
'name' => 'social_work_result',
'base_path' => '/api/1.0/person/social-work/result',
'base_role' => 'ROLE_USER',
'actions' => [
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
'by-social-action' => [
'single-collection' => 'collection',
'path' => '/by-social-action/{id}.{_format}',
'controller_action' => 'listBySocialAction',
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
'by-goal' => [
'single-collection' => 'collection',
'path' => '/by-goal/{id}.{_format}',
'controller_action' => 'listByGoal',
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
]
],
[
'class' => \Chill\PersonBundle\Entity\SocialWork\Goal::class,
'controller' => \Chill\PersonBundle\Controller\SocialWorkGoalApiController::class,
'name' => 'social_work_goal',
'base_path' => '/api/1.0/person/social-work/goal',
'base_role' => 'ROLE_USER',
'actions' => [
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
'by-social-action' => [
'single-collection' => 'collection',
'path' => '/by-social-action/{id}.{_format}',
'controller_action' => 'listBySocialAction',
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
'roles' => [
Request::METHOD_GET => 'ROLE_USER',
Request::METHOD_HEAD => 'ROLE_USER',
]
],
]
],
]

View File

@@ -78,6 +78,7 @@ class Configuration implements ConfigurationInterface
->append($this->addFieldNode('address'))
->append($this->addFieldNode('accompanying_period'))
->append($this->addFieldNode('memo'))
->append($this->addFieldNode('number_of_children'))
->arrayNode('alt_names')
->defaultValue([])
->arrayPrototype()
@@ -130,7 +131,7 @@ class Configuration implements ConfigurationInterface
{
$tree = new TreeBuilder($key,'enum');
$node = $tree->getRootNode($key);
switch($key) {
case 'accompanying_period':
$info = "If the accompanying periods are shown";

View File

@@ -25,6 +25,7 @@ namespace Chill\PersonBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Entity\Scope;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
@@ -39,6 +40,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Validator\Constraints as Assert;
/**
* AccompanyingPeriod Class
@@ -280,6 +282,15 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
*/
private \DateTimeInterface $updatedAt;
/**
* @ORM\OneToMany(
* targetEntity=AccompanyingPeriodWork::class,
* mappedBy="accompanyingPeriod"
* )
* @Assert\Valid(traverse=true)
*/
private Collection $works;
/**
* AccompanyingPeriod constructor.
*
@@ -292,6 +303,7 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
$this->scopes = new ArrayCollection();
$this->socialIssues = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->works = new ArrayCollection();
}
/**
@@ -836,7 +848,6 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
}
}
}
return $recursiveSocialIssues;
}
@@ -896,4 +907,28 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
/**
* @return AccompanyingPeriodWork[]
*/
public function getWorks(): Collection
{
return $this->works;
}
public function addWork(AccompanyingPeriodWork $work): self
{
$this->works[] = $work;
$work->setAccompanyingPeriod($this);
return $this;
}
public function removeWork(AccompanyingPeriodWork $work): self
{
$this->work->removeElement($work);
$work->setAccompanyingPeriod(null);
return $this;
}
}

View File

@@ -4,103 +4,168 @@ namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\SocialWork\Result;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
* @ORM\Table(name="chill_person_accompanying_period_work")
* @Serializer\DiscriminatorMap(
* typeProperty="type",
* mapping={
* "accompanying_period_work":AccompanyingPeriodWork::class
* }
* )
*/
class AccompanyingPeriodWork
class AccompanyingPeriodWork implements TrackCreationInterface, TrackUpdateInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private $id;
private ?int $id;
/**
* @ORM\Column(type="text")
* @Serializer\Groups({"read", "accompanying_period_work:edit"})
*/
private $note;
private string $note = "";
/**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class)
* @Serializer\Groups({"read"})
*/
private $accompanyingPeriod;
private ?AccompanyingPeriod $accompanyingPeriod = null;
/**
* @ORM\ManyToOne(targetEntity=SocialAction::class)
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work:create"})
*/
private $socialAction;
private ?SocialAction $socialAction = null;
/**
* @ORM\Column(type="datetime")
* @ORM\Column(type="datetime_immutable")
* @Serializer\Groups({"read"})
*/
private $createdAt;
private ?\DateTimeImmutable $createdAt = null;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
* @Serializer\Groups({"read"})
*/
private $createdBy;
private ?User $createdBy = null;
/**
* @ORM\Column(type="datetime")
* @ORM\Column(type="datetime_immutable")
* @Serializer\Groups({"read"})
*/
private $startDate;
private ?\DateTimeImmutable $updatedAt = null;
/**
* @ORM\Column(type="datetime")
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
* @Serializer\Groups({"read"})
*/
private $endDate;
private ?User $updatedBy = null;
/**
* @ORM\Column(type="date_immutable")
* @Serializer\Groups({"accompanying_period_work:create"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read"})
*/
private \DateTimeImmutable $startDate;
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default":null})
* @Serializer\Groups({"accompanying_period_work:create"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read"})
* @Assert\GreaterThan(propertyPath="startDate",
* message="accompanying_course_work.The endDate should be greater than the start date"
* )
*/
private ?\DateTimeImmutable $endDate = null;
/**
* @ORM\ManyToOne(targetEntity=ThirdParty::class)
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work:edit"})
*
* In schema : traitant
*/
private $handlingThierParty;
private ?ThirdParty $handlingThierParty = null;
/**
* @ORM\Column(type="boolean")
*/
private $createdAutomatically;
private bool $createdAutomatically = false;
/**
* @ORM\Column(type="text")
*/
private $createdAutomaticallyReason;
private string $createdAutomaticallyReason = "";
/**
* @ORM\OneToMany(targetEntity=AccompanyingPeriodWorkGoal::class, mappedBy="accompanyingPeriodWork")
* @ORM\OneToMany(
* targetEntity=AccompanyingPeriodWorkGoal::class,
* mappedBy="accompanyingPeriodWork",
* cascade={"persist"},
* orphanRemoval=true
* )
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work:edit"})
*/
private $goals;
private Collection $goals;
/**
* @ORM\ManyToMany(targetEntity=Result::class, inversedBy="accompanyingPeriodWorks")
* @ORM\JoinTable(name="chill_person_accompanying_period_work_result")
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work:edit"})
*/
private $results;
private Collection $results;
/**
* @ORM\ManyToMany(targetEntity=ThirdParty::class)
* @ORM\JoinTable(name="chill_person_accompanying_period_work_third_party")
*
* In schema : intervenants
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work:edit"})
*/
private $thirdParties;
private Collection $thirdParties;
/**
*
* @ORM\ManyToMany(targetEntity=Person::class)
* @ORM\JoinTable(name="chill_person_accompanying_period_work_person")
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"accompanying_period_work:create"})
*/
private Collection $persons;
public function __construct()
{
$this->goals = new ArrayCollection();
$this->results = new ArrayCollection();
$this->thirdParties = new ArrayCollection();
$this->persons = new ArrayCollection();
}
public function getId(): ?int
@@ -125,8 +190,17 @@ use Doctrine\ORM\Mapping as ORM;
return $this->accompanyingPeriod;
}
/**
* Internal: you should use `$accompanyingPeriod->removeWork($work);` or
* `$accompanyingPeriod->addWork($work);`
*/
public function setAccompanyingPeriod(?AccompanyingPeriod $accompanyingPeriod): self
{
if ($this->accompanyingPeriod instanceof AccompanyingPeriod &&
$accompanyingPeriod !== $this->accompanyingPeriod) {
throw new \LogicException("A work cannot change accompanyingPeriod");
}
$this->accompanyingPeriod = $accompanyingPeriod;
return $this;
@@ -144,7 +218,7 @@ use Doctrine\ORM\Mapping as ORM;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
@@ -168,6 +242,30 @@ use Doctrine\ORM\Mapping as ORM;
return $this;
}
public function setUpdatedBy(User $user): TrackUpdateInterface
{
$this->updatedBy = $user;
return $this;
}
public function setUpdatedAt(DateTimeInterface $datetime): TrackUpdateInterface
{
$this->updatedAt = $datetime;
return $this;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function getUpdatedBy(): ?User
{
return $this->updatedBy;
}
public function getStartDate(): ?\DateTimeInterface
{
return $this->startDate;
@@ -185,7 +283,7 @@ use Doctrine\ORM\Mapping as ORM;
return $this->endDate;
}
public function setEndDate(\DateTimeInterface $endDate): self
public function setEndDate(?\DateTimeInterface $endDate = null): self
{
$this->endDate = $endDate;
@@ -240,7 +338,7 @@ use Doctrine\ORM\Mapping as ORM;
{
if (!$this->goals->contains($goal)) {
$this->goals[] = $goal;
$goal->setAccompanyingPeriodWork2($this);
$goal->setAccompanyingPeriodWork($this);
}
return $this;
@@ -248,10 +346,10 @@ use Doctrine\ORM\Mapping as ORM;
public function removeGoal(AccompanyingPeriodWorkGoal $goal): self
{
if ($this->goals->removeElement($goal)) {
if ($this->goals->removeElement($goal)) {
// set the owning side to null (unless already changed)
if ($goal->getAccompanyingPeriodWork2() === $this) {
$goal->setAccompanyingPeriodWork2(null);
if ($goal->getAccompanyingPeriodWork() === $this) {
$goal->setAccompanyingPeriodWork(null);
}
}
@@ -305,4 +403,25 @@ use Doctrine\ORM\Mapping as ORM;
return $this;
}
public function getPersons(): Collection
{
return $this->persons;
}
public function addPerson(Person $person): self
{
if (!$this->persons->contains($person)) {
$this->persons[] = $person;
}
return $this;
}
public function removePerson(Person $person): self
{
$this->persons->removeElement($person);
return $this;
}
}

View File

@@ -7,10 +7,17 @@ use Chill\PersonBundle\Entity\SocialWork\Result;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Entity
* @ORM\Table(name="chill_person_accompanying_period_work_goal")
* @Serializer\DiscriminatorMap(
* typeProperty="type",
* mapping={
* "accompanying_period_work_goal":AccompanyingPeriodWorkGoal::class
* }
* )
*/
class AccompanyingPeriodWorkGoal
{
@@ -18,11 +25,14 @@ class AccompanyingPeriodWorkGoal
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private $id;
/**
* @ORM\Column(type="text")
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read"})
*/
private $note;
@@ -33,12 +43,16 @@ class AccompanyingPeriodWorkGoal
/**
* @ORM\ManyToOne(targetEntity=Goal::class)
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read"})
*/
private $goal;
/**
* @ORM\ManyToMany(targetEntity=Result::class, inversedBy="accompanyingPeriodWorkGoals")
* @ORM\JoinTable(name="chill_person_accompanying_period_work_goal_result")
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read"})
*/
private $results;
@@ -71,6 +85,13 @@ class AccompanyingPeriodWorkGoal
public function setAccompanyingPeriodWork(?AccompanyingPeriodWork $accompanyingPeriodWork): self
{
if ($this->accompanyingPeriodWork instanceof AccompanyingPeriodWork
&& $accompanyingPeriodWork !== $this->accompanyingPeriodWork
&& $accompanyingPeriodWork !== null
) {
throw new \LogicException("Change accompanying period work is not allowed");
}
$this->accompanyingPeriodWork = $accompanyingPeriodWork;
return $this;

View File

@@ -7,8 +7,12 @@ use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Validator\Constraints\Household\MaxHolder;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
/**
* @ORM\Entity
@@ -18,6 +22,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember;
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "household"=Household::class
* })
* @MaxHolder(groups={"household_memberships"})
*/
class Household
{
@@ -37,6 +42,7 @@ class Household
* cascade={"persist", "remove", "merge", "detach"})
* @ORM\JoinTable(name="chill_person_household_to_addresses")
* @ORM\OrderBy({"validFrom" = "DESC"})
* @Serializer\Groups({"write"})
*/
private Collection $addresses;
@@ -49,10 +55,26 @@ class Household
*/
private Collection $members;
/**
* @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_members_")
*/
private CommentEmbeddable $commentMembers;
/**
* @ORM\Column(type="boolean", name="waiting_for_birth", options={"default": false})
*/
private bool $waitingForBirth = false;
/**
* @ORM\Column(type="date_immutable", name="waiting_for_birth_date", nullable=true, options={"default": null})
*/
private ?\DateTimeImmutable $waitingForBirthDate = null;
public function __construct()
{
$this->addresses = new ArrayCollection();
$this->members = new ArrayCollection();
$this->commentMembers = new CommentEmbeddable();
}
public function getId(): ?int
@@ -66,6 +88,12 @@ class Household
*/
public function addAddress(Address $address)
{
foreach ($this->getAddresses() as $a) {
if ($a->getValidFrom() < $address->getValidFrom() && $a->getValidTo() === NULL) {
$a->setValidTo($address->getValidFrom());
}
}
$this->addresses[] = $address;
return $this;
@@ -83,6 +111,7 @@ class Household
* By default, the addresses are ordered by date, descending (the most
* recent first)
*
* @Assert\Callback(methods={"validate"})
* @return \Chill\MainBundle\Entity\Address[]
*/
public function getAddresses()
@@ -90,6 +119,27 @@ class Household
return $this->addresses;
}
/**
* @Serializer\Groups({ "read" })
* @Serializer\SerializedName("current_address")
*/
public function getCurrentAddress(\DateTime $at = null): ?Address
{
$at = $at === null ? new \DateTime('today') : $at;
$addrs = $this->getAddresses()->filter(function (Address $a) use ($at) {
return $a->getValidFrom() < $at && (
NULL === $a->getValidTo() || $at < $a->getValidTo()
);
});
if ($addrs->count() > 0) {
return $addrs->first();
} else {
return null;
}
}
/**
* @return Collection|HouseholdMember[]
*/
@@ -98,7 +148,59 @@ class Household
return $this->members;
}
public function getMembersOnRange(\DateTimeImmutable $from, ?\DateTimeImmutable $to): Collection
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where(
$expr->gte('startDate', $from)
);
if (NULL !== $to) {
$criteria->andWhere(
$expr->orX(
$expr->lte('endDate', $to),
$expr->eq('endDate', NULL)
),
);
}
return $this->getMembers()
->matching($criteria)
;
}
public function getMembersDuringMembership(HouseholdMember $membership)
{
return $this->getMembersOnRange(
$membership->getStartDate(),
$membership->getEndDate()
)->filter(
function(HouseholdMember $m) use ($membership) {
return $m !== $membership;
}
);
}
public function getMembersHolder(): Collection
{
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where(
$expr->eq('holder', true)
);
return $this->getMembers()->matching($criteria);
}
public function getCurrentMembers(?\DateTimeImmutable $now = null): Collection
{
return $this->getMembers()->matching($this->buildCriteriaCurrentMembers($now));
}
private function buildCriteriaCurrentMembers(?\DateTimeImmutable $now = null): Criteria
{
$criteria = new Criteria();
$expr = Criteria::expr();
@@ -114,7 +216,53 @@ class Household
$expr->gte('endDate', $date)
));
return $this->getMembers()->matching($criteria);
return $criteria;
}
/**
* @return HouseholdMember[]
*/
public function getCurrentMembersOrdered(?\DateTimeImmutable $now = null): Collection
{
$members = $this->getCurrentMembers($now);
$members->getIterator()
->uasort(
function (HouseholdMember $a, HouseholdMember $b) {
if ($a->getPosition()->getOrdering() < $b->getPosition()->getOrdering()) {
return -1;
}
if ($a->getPosition()->getOrdering() > $b->getPosition()->getOrdering()) {
return 1;
}
if ($a->isHolder() && !$b->isHolder()) {
return 1;
}
if (!$a->isHolder() && $b->isHolder()) {
return -1;
}
return 0;
}
);
return $members;
}
/**
* get current members ids
*
* Used in serialization
*
* @Serializer\Groups({"read"})
* @Serializer\SerializedName("current_members_id")
*
*/
public function getCurrentMembersIds(?\DateTimeImmutable $now = null): Collection
{
return $this->getCurrentMembers($now)->map(
fn (HouseholdMember $m) => $m->getId()
);
}
/**
@@ -192,4 +340,54 @@ class Household
return $this;
}
public function getCommentMembers(): CommentEmbeddable
{
return $this->commentMembers;
}
public function setCommentMembers(CommentEmbeddable $commentMembers): self
{
$this->commentMembers = $commentMembers;
return $this;
}
public function getWaitingForBirth(): bool
{
return $this->waitingForBirth;
}
public function setWaitingForBirth(bool $waitingForBirth): self
{
$this->waitingForBirth = $waitingForBirth;
return $this;
}
public function getWaitingForBirthDate(): ?\DateTimeImmutable
{
return $this->waitingForBirthDate;
}
public function setWaitingForBirthDate(?\DateTimeImmutable $waitingForBirthDate): self
{
$this->waitingForBirthDate = $waitingForBirthDate;
return $this;
}
public function validate(ExecutionContextInterface $context, $payload)
{
$addresses = $this->getAddresses();
$cond = True;
for ($i=0; $i < count($addresses) - 1; $i++) {
if ($addresses[$i]->getValidFrom() != $addresses[$i + 1]->getValidTo()) {
$cond = False;
$context->buildViolation('The address are not sequentials. The validFrom date of one address should be equal to the validTo date of the previous address.')
->atPath('addresses')
->addViolation();
}
}
dump($cond);
}
}

View File

@@ -7,6 +7,7 @@ use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -28,18 +29,25 @@ class HouseholdMember
/**
* @ORM\ManyToOne(targetEntity=Position::class)
* @Serializer\Groups({"read"})
* @Assert\NotNull(groups={"household_memberships"})
*/
private ?Position $position = null;
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Serializer\Groups({"read"})
* @Assert\NotNull(groups={"household_memberships"})
*/
private ?\DateTimeImmutable $startDate = null;
/**
* @ORM\Column(type="date_immutable", nullable= true, options={"default": null})
* @Serializer\Groups({"read"})
* @Assert\GreaterThan(
* propertyPath="startDate",
* message="household_membership.The end date must be after start date",
* groups={"household_memberships"}
* )
*/
private ?\DateTimeImmutable $endDate = null;
@@ -67,6 +75,8 @@ class HouseholdMember
* targetEntity="\Chill\PersonBundle\Entity\Person"
* )
* @Serializer\Groups({"read"})
* @Assert\Valid(groups={"household_memberships"})
* @Assert\NotNull(groups={"household_memberships"})
*/
private ?Person $person = null;
@@ -76,6 +86,8 @@ class HouseholdMember
* @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Household\Household"
* )
* @Assert\Valid(groups={"household_memberships"})
* @Assert\NotNull(groups={"household_memberships"})
*/
private ?Household $household = null;
@@ -194,4 +206,13 @@ class HouseholdMember
{
return $this->holder;
}
public function isCurrent(\DateTimeImmutable $at = null): bool
{
$at = NULL === $at ? new \DateTimeImmutable('now'): $at;
return $this->getStartDate() < $at && (
NULL === $this->getEndDate() || $at < $this->getEndDate()
);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Chill\PersonBundle\Entity\Household;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(readOnly=true)
* @ORM\Table(name="view_chill_person_household_address")
*/
class PersonHouseholdAddress
{
/**
* @ORM\Column(type="date_immutable")
*/
private $validFrom;
/**
* @ORM\Column(type="date_immutable", nullable=true)
*/
private $validTo;
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(nullable=false)
*/
private $person;
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity=Household::class)
* @ORM\JoinColumn(nullable=false)
*/
private $household;
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity=Address::class)
* @ORM\JoinColumn(nullable=false)
*/
private $address;
public function getValidFrom(): ?\DateTimeInterface
{
return $this->validFrom;
}
public function getValidTo(): ?\DateTimeImmutable
{
return $this->validTo;
}
public function getPerson(): ?Person
{
return $this->person;
}
public function getHousehold(): ?Household
{
return $this->relation;
}
public function getAddress(): ?Address
{
return $this->address;
}
}

Some files were not shown because too many files have changed in this diff Show More