Merge remote-tracking branch 'origin/master' into testing

This commit is contained in:
Julien Fastré 2023-06-18 22:02:14 +02:00
commit f7be53f790
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
114 changed files with 3389 additions and 549 deletions

6
.changes/header.tpl.md Normal file
View File

@ -0,0 +1,6 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).

View File

677
.changes/v2.0.0.md Normal file
View File

@ -0,0 +1,677 @@
## 2.0.0
* this is a release to relaunch our proceess of release with semantic versioning
## Test releases
### 2.0.0-beta3
* [person][export] Fixed: rename the alias for `accompanying_period` to `acp` in filter associated with person
* [activity][export] Feature: improve label for aliases in "Filter by activity type"
* [activity][export] DX/Feature: use of an `ActivityTypeRepositoryInterface` instead of the old-style EntityRepository
* [person][export] Fixed: some inconsistency with date filter on accompanying courses
* [person][export] Fixed: use left join for related entities in accompanying course aggregators
* [workflow] Feature: allow user to copy and send manually the access link for the workflow
* [workflow] Feature: show the email addresses that received an access link for the workflow
### 2.0.0-beta2
* [workflow]: Fixed: the notification is sent when the user is added to the first step.
* [budget] Feature: allow to desactivate some charges and resources, adding an `active` key in the configuration
* [person] Feature: on Evaluation, allow to configure an URL from the admin
### 2022-06
* [workflow]: added pagination to workflow list page
* [homepage_widget]: null error on tasks widget fixed
* [person-thirdparty]: fix quick-add of names that consist of multiple parts (eg. De Vlieger) within onthefly modal person/thirdparty
* [search]: Order of birthdate fields changed in advanced search to avoid confusion.
* [workflow]: Constraint added to workflow (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/675)
* [social_action]: only show active objectives (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/625)
* [household]: Reposition and cut button for enfant hors menage have been deleted (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/620)
* [admin]: Add crud for composition type in admin (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/611)
* [social_action]: only show active objectives (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/625)
## Test releases
### 2022-05-30
* fix creating a new AccompanyingPeriodWorkEvaluationDocument when replacing the document (the workflow was lost)
### 2022-05-27
* [storedobject] add title field on StoredObject entity + use it in activity documents (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/604)
* [main] add a "read more..." on comment embeddable when overflown (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/604)
* [person] add closing motive to closed acc course (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/603)
* [person] household filiation: fetch person info when unfolding person (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/586)
* [admin] repair edit of social action in the admin (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/601)
* [admin]: add select2 to Goal form type entity fields (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/702)
* [main] allow hide permissions group list menu (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/577)
* [main] allow hide change user password menu (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/577)
* [main] filter user jobs by active jobs (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/577)
* [main] add civility to User (entity, migration and form type) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/577)
* [admin] refactorisation of the admin section: reorganisation of the menu, translations, form types, new entities (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/592)
* [admin] add admin section for languages and countries (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/596)
* [activity] activity admin: translations + remove label field for comment on admin activity type (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/587)
* [main] admin user_job: improvements (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/588)
* [address] can add extra address info even if noAddress (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/576)
### 2022-05-06
* [person] add civility when creating a person (with the on-the-fly component or in the php form) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557)
* [person] add address when creating a person (with the on-the-fly component or in the php form) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557)
* [person] add household creation API point (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557)
### 2021-04-29
* [person] prevent circular references in PersonDocGenNormalizer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/527)
* [person] add maritalStatusComment to PersonDocGenNormalizer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/582)
* Load relationships without gender in french fixtures
* Add command to remove old draft accompanying periods
* [parcours]: If users assings him/herself as referrer and job is not null. Update parcours job (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/578)
### 2021-04-28
* [address] fix bug when editing address: update location and addressreferenceId + better update of the map in edition (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/593)
* [main] avoid address reference search on undefined post code (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/561)
* [person] prevent duplicate relationship in filiation/household graph (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/560)
* [Documents] Validate storedObject and allow for null data (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/565)
* [parcours]: Comments can be unpinned + edit/delete for all users that are allowed to edit parcours (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/566)
### 2021-04-26
* [Datepickers] datepickers fixed when using keyboard to enter date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/545)
* [social_action] Display 'agents traitants' in parcours resumé and social action list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/568)
* [Person_search] Closed parcours shown within an accordeon that can be opened/closed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/574)
### 2021-04-24
* [notification email on course designation] allow raw string in email content generation
* [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older
* [Documents] Change wording 'créer' to 'enregistrer' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/634)
* [Parcours]: The number of 'mes parcours' displayed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/572)
* [Hompage_widget]: Renaming of tabs and removal of social actions tab (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/570)
* [activity]: Ignore thirdparties when creating a social action via an activity (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/573)
* [parcours]: change wording of warning message and button when user is not associated to a household yet (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/590#note_918370943)
* [Accompanying period work evaluations] list documents associated to a work by creation date, and then by id, from the most recent to older
* [Course comment] add validationConstraint NotNull and NotBlank on comment content, to avoid sql error
* [Notifications] delay the sending of notificaiton to kernel.terminate
* [Notifications / Period user change] fix the sending of notification when user changes
* [Activity form] invert 'incoming' and 'receiving' in Activity form
* [Activity form] keep the same order for 'attendee' field in new and edit form
* [list with period] use "sameas" test operator to introduce requestor in list
* [notification email on course designation] allow raw string in email content generation
* [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older
* [evaluation_document] changing date to datetime in order to display the time at which document was created (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/569)
### 2021-04-13
* [person] household address: add a form for editing the validFrom date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/541)
* [person] householdmemberseditor: fix composition type bug in select form (vuejs) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/543)
* [docgen] add more persons choices in docgen for course: amongst requestor (if person), resources of course (if person), and PersonResource (if person);
* [docgen] add a new context with a list of activities in course
* [docgen] add a comment in budget lines
* [notifications] allow to send a notification to an email address. The address receive an access link
* [adresses] add constraints in database to avoid errors later: postcode not null, and validfrom <= validto
* [accompanying work editor] add a label on document title input
### 2021-04-07
* notification list: move action buttons outside of the toggle
* fix detecting of non-read notification
* filter users which are disabled in search user api
* order query for location and add pagination in list
* allow every person which has part for a workflow to see the workflow page
* able to see the workflow if the evaluation document has been deleted
* hardcode the list of supported mime types for edition with collabora
* list of accompanying course: allow to see the pinned comment in list_item
### 2021-04-06
* [main] notification toggle read: correct js syntax for compilation in production (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/548)
* [parcours] Display of interlocuteurs changed to flex-table in parcours edit page to prevent cut-off of information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/535)
* [activity] espace entre les boutons pour supprimer les documents
### continuous release in February and March
* Creation of PickCivilityType, and implementation in PersonType and ThirdpartyType
* [person] Accompanying course evaluation documents: disable the WOPI edit link if mimetype not supported and if no keyInfos
(https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/585)
* [activity] display error messages above the form in creating a new location (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/481)
* [activity] show required field in activity edit/new by an asterix in the vuejs fields (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/494)
* [ACL] fix allow to see the course, event if the scope'course does not contains the scope's user
* [search] enforce limit of results for fetching rsults by search api https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/576
* [activity] Fix delete button for document (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/554)
* [activity] Add return path the document generation (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/553)
* [person] add person ressource to person docgen normaliser (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/517)
* [person] AccompanyingCourseWorkEdit: fix deleting evaluation documents (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/546)
* [person] AccompanyingCourseWorkEdit: download existing documents (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/512)
* [person] AccompanyingCourseWorkEdit: replace document by a new one (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/511)
* [person] AccompanyingPeriodWork: add referrers to work, add doctrine event listener to add logged user to referrers collection and display a referrers list in work list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/502)
* [person] AccompanyingPeriodWorkEvaluation: fix circular reference when serialising (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/495)
* [person] order accompanying period by opening date in search persons, person and household period lists (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/493)
* [parcours] autosave of the pinned comment for draft accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/477)
* [main] filter user job in undispatch acc period to assign (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/472)
* [main] filter user job in undispatch acc period to assign (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/472)
* [person] Add url in accompanying period work evaluations entity and form (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/476)
* [person] Add document generation in admin and in person/{id}/document (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/464)
* [activity] do not override location if already exist (when validating new activity) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/470)
* [parcours] Toggle emergency/intensity only by referrer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/442)
* [docstore] Add an API entrypoint for StoredObject (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/466)
* [person] Add the possibility of uploading existing documents to AccPeriodWorkEvaluationDocument (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/466)
* [person] Add title to AccPeriodWorkEvaluationDocument (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/466)
* [person] Order social issues by the field "ordering" (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/388)
* [Person/Household list] when listing other simultaneous members of an household, exclude the members on person, not on members (avoid to show two membersship with the same person)
* [draft periods] add a delete button (if acl granted) on each draft period listed on draft period page (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/463)
* [Person] Display suffixText in RenderPerson, PersonText.vue, RenderPersonBox.vue (was made for displaying "enfant confie") (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/441)
* [budget]: budget enabled for persons and households (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/469)
* [person] residential address: show residential address or info in PersonRenderBox, refactor Residential Address (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/439)
* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
* [documents] Improve flex-table item-col placement when long buttons and long metadata
* [thirdparty] Fix display of multiple contact badges so they wrap onto next line (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/482)
* [confidential] Fix position of toggle button so it does not cover text nor fall outside of box (no issue)
* [parcours] Fix edit of both thirdparty and contact name (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/474)
* [template] do not list inactive templates (for doc generator)
* [household] bugfix if position of member is null, renderbox no longer throws an error (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/480)
* [parcours] location cannot be removed if linked to a user (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/478)
* [person] email added to twig personRenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/490)
* [activity] Only youngest descendant is kept for social issues and actions (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/471)
* [person] Add link to current household in person banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/484)
* [address] person badge in address history changed to open OnTheFly with all person info (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/489)
* [person] Change 'personne' with 'usager' and '&' with 'ET' (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/499)
* [thirdparty] Add parameter condition to display centers or not (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/500)
* [phonenumber] Remove placeholder in phonenumber field (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/496)
* [person_resource] separate create page created to avoid confusion (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/504)
* [contact] add contact button color changed plus the pipe at the side removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/506)
* [thirdparty] For contacts show current civility/profession in edit form + fix saving of edited information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/491)
* [household] create-edit household composition placed in separate page to avoid confusion (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/505)
* [blur] Improved positioning of toggle icon (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/486)
* [thirdparty] add firstname field to thirdparty 'child' or 'contact' types (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/508)
* [household] create-edit household composition placed in separate page to avoid confusion (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/505)
* [blur] Improved positioning of toggle icon (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/486)
* [parcours] List of parcours for a specific user so they can be reassigned in case of absence (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/509)
* [thirdparty] Thirdparty view page, english text translated (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/534)
* [social_action] Translation changed in evaluation section (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/512)
* [filiation] Possible to add person (or create onthefly) to add to filiation graph + add relation (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/519)
* [household] Within parcours listing page of household add create button (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/560)
* [person_resource] bugfix when adding thirdparty or freetext resource + prevent personOwner themselves to be added. (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/526)
* [aside_activity] style correction + sticky-form create button (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/529)
* [budget] order within the menu adjusted (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/592)
* [onthefly] fix create person. Bug was noticed in filiation (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/591)
* [parcours] Create document buttons made sticky (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/532)
* [person] Trailing guillemet removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/530)
* [notification] Display of social action within workflow notification set to display block (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/537)
* [onthefly] trim trailing whitespace in email of person and thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/542)
* [action] Only youngest descendant is kept for social issues and actions (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/471)
## Test releases
### test release 2022-02-21
* [notifications] Word 'un' changed to number '1' for notifications in user menu (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/483)
* [documents] 'gabarit' changed to 'modèle' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/405)
* [person_resources] Menu name and order changed (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/460)
* workflow: fix sending notifications
* [thirdparty] Extend the thirdparty search to thirdparty children (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/448)
* [person]: AddPersons: allow creation of person or thirdparty only (no users) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/422)
* [person]: AddPersons: allow creation of person or thirdparty depending on allowed types (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/422)
* [person]: AddPersons: add suggestion of name when creating new person or thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/422)
* [main] Address: fix small bug: when modifying an address without street (isNoAddress), also check errors if street is an empty string as back-end change null value to empty string for street (and streetNumber)
* [main] Address: stronger client-side validation of addresses (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/449)
* [person] accompanying course: filter suggested entities by open participations (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/415)
[activity] can click through the cross icon for removing person in concerned group (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/476)
[activity] correct associated persons by considering only open participations (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/476)
* [person_resources]: Renderboxes used to display person/thirdparty info (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/465)
* [Household]: Add end date in HouseholdMember form for 'enfant hors menage' (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/434)
* [homepage_widget]: If no sender then display as 'notification automatique' (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/435)
* [parcours]: Order social activities and only display most recent three in parcours resumé (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/481)
* [3party]: 3party: redirect to parent when contact (child) is opened in view page
* [parcours / addresses]: launch an event when a person change address (either through changing household or because the household is associated to a new address). If the person is localising a course, the course location go back to a temporarily address.
* [thirdparty]: address/phonenumber/email/fonction displayed in thirdpartyrenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/401)
* [thirdparty_contact]: in search results the 'qualité' is displayed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/465)
* [bug]: fix confidential toggle of address in thirdpartyrenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/460)
### test release 2022-02-14
* AddPersons: remove ul-li html tags from AddPersons (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/419)
* [doc-generator] do not set required fields for mainPerson, person1, person2 (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement#456)
* [doc-generation] add age and obele in the mainPerson, person1 and person2 list + add obele in person renderString if addAge (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/370)
* [person] accompanying course work: fix on-the-fly update of thirdParty
* fix normalisation of accompanying course requestor api (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/378)
* [person] add a returnPath when clicking on some Person or ThirdParty badge (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/427)
* [person] accompanying course work: fix on-the-fly update of thirdParty
* [on-the-fly] close modal only after validation
* [person] correct thirdparty PATCH url + add email and altnames in AddPerson and serializer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/433)
* change order for accompanying course work list
* [parcours]: Mes parcours brouillon added to user menu (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/440)
* [Documents]: List view adapted to display more information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/414)
* [person]: style fix in parcours listing per person. (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/432)
* [parcours]: Only the referrer can toggle the intensity of the parcours (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/442)
* [household]: display address of current household (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/415)
* ajoute un ordre dans les localisation (api)
* [pick entity]: fix translations in modal (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/419)
* [homepage_widget]: fix translation on emergency badge (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/440)
* [person]: create person and household added to button dropdown (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/454)
* display full address in address.text in normalization. Adapt AddressRenderBox
* [address]: Correction residential address 'depuis le' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/459)
* [Documents]: List view adapted to display more information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/414)
* [Thirdparty_contact]: address blurred if confidential in view page (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/450)
* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
### test release 2021-02-01
* renommer "dossier numéro" en "parcours numéro" dans les résultats de recherche
* renomme date de début en date d'ouverture dans le formulaire parcours
* [homepage widget] improve content tables, improve counter pluralization with style on number
* [notification lists] add comments counter information
* [workflows] fix popover header with previous transition
* [parcours]: validation + message for closing parcours adjusted.
* [household]: household composition double edit button replaced by a delete action (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/426)
[fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413
[homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [person]: Comment on marital status is possible even if marital status is not defined (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/421)
* [parcours]: In the list of person results the requestor is not displayed if defined as anonymous (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/424)
* [bugfix]: modal closes and newly created person/thirdparty is selected when multiple persons/thirdparties are created through the modal (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/429)
* [person_resource]: Onthefly button added to view person/thirdparty and badge differentiation for a contact-thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/428)
* [workflow][notification] improve how notifications and workflows are 'attached' to entities: contextual list, counter, buttons and vue modal
* [AddAddress] disable multiselect search, and rely only on most pertinent Cities and Street computed backend
* [fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413
* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
### test release 2021-01-31
* [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409)
* [person] accompanying course: close modal when edit participation (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420)
* [person] accompanying course: treat validation error when editing on-the-fly entities (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/420)
* [activity] show activity attendee (présence) in the activity list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/412)
* [activity] admin: change validation rule for social action visible field (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/413)
* [parcours]: component added to change the opening date of a parcours (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/411)
* [search]: listing of parcours display changed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/410)
* [user]: page with accompanying periods to which is user is referent (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/408)
* [person] age added to renderstring + renderbox/ vue component created to display person text (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/389)
* [household member editor] allow to push to existing household
### test release 2021-01-28
* [person] improve filiations vis graph: disable physics, use chill colors for persons-households-course, increase label of relations, remove labels on household arrows and other improvements (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/286, https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/362)
* [activity] Order activity by date and by id (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/364)
* [main] increase length of 4 Address fields (change to TEXT, no size limits) (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/277)
* [main] Add confidential option for address, in edit and view (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/165)
* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377)
* [person] Add residential address entity, form and list for each person
* [aside_activity]: dynamicUserPickerType used (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/399)
* dispatching list
### test release 2021-01-26
* [parcours] comments truncated if too long + link added (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/406)
* [person]: possibility to add person resources (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/382)
* [person ressources]: module added
### test release 2022-01-24
* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377)
* [notification: formulaire création] descend la box avec la description dans le bas du formulaire
* [notification for activity]: fix link to activity
* [notification] add "URGENT" before accompanying course with emergency = true
* [notification] add a "read more" button on system notification
* [notification] add `[Chill]` in the subject of each notification, automatically
* [notification] add a counter for notification in activity list and accompanying period list, and search results
* [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before.
* [workflow] add breadcrumb to show steps
* [popover] add popover html popup mechanism (used by workflow breadcrumb)
* [templates] improve updatedBy macro in item metadatas
* [parcours]: bug fix when comment is pinned all other comments remain in the collection (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/385)
* [workflow]
* add My workflow section with my opened subscriptions
* apply workflow on documents, accompanyingCourseWork and Evaluations
* [wopi-link] a new vue component allow to open wopi link in a fullscreen chill-themed modal
### test release 2022-01-19
* vuejs: add dead information on all on-the-fly person render boxes, in vis graph and other templates (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/271)
* [thirdparty] fix bug in 3rd party view: types was replaced by thirdPartyTypes
* [main] location form type: fix unmapped address field (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/246)
* [activity] fix wrong import of js assets for adding and viewing documents in activity (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/83 & https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/176)
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
* [forms] dynamic picker types for user/person/thirdparty types created (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/386)
### test release 2022-01-17
* [main] Add editableByUser field to locationType entity, adapt the admin template and add this condition in the location-type endpoint (see https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/297)
* [main] Add mainLocation field to User entity and add it in user form type
* rewrite page which allow to select activity
* [main] Add mainLocation field to User entity and add it in user form type
* [course list in person context] show full username/label for ref
* [accompanying period work] remove the possibility to generate document from an accompanying period work
* vuejs: add validation on required fields for AddPerson, Address and Location components
* vuejs: treat 422 validation errors in locations and AddPerson components
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
## Test releases
* vuejs: add validation on required fields for AddPerson, Address and Location components
* vuejs: treat 422 validation errors in locations and AddPerson components
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
### test release 2022-01-12
* fix thirdparty normalizer on telephone field: https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/322
### test release 2022-01-11
* vuejs: translate in French all multiselect widgets
* [address] define address lines according postal standards for France and Belgium (default) and change AddressRender, chill_entity_render_box and AddressRenderBox.vue
* [household] change translations (champs-libres/departement-de-la-vendee/accent-suivi-developpement#109)
* [household] add address i18n in household component (champs-libres/departement-de-la-vendee/accent-suivi-developpement#158)
* [household] add on the fly i18n in household component
* [household] redirect to the household page when a household is created from a person (champs-libres/departement-de-la-vendee/accent-suivi-developpement#175)
* [household] household member editor: display alert if some members have already an household (champs-libres/departement-de-la-vendee/accent-suivi-developpement#172)
* [household] household member editor: do not add in new members if the member is included in the members of household (champs-libres/departement-de-la-vendee/accent-suivi-developpement#123)
* [household] household member editor: remove markNoAddress button (champs-libres/departement-de-la-vendee/accent-suivi-developpement#109)
* [person]: ordering fields in add person (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/61)
* [person]: Add email and alt names in add person (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/61)
* [accompanyingCourse] Add a delete action and delete buttons to delete a accompanying course when step = DRAFT (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/64)
* [accompanyingCourse] Add a administrative location in the accompanying course, set the user current location as default, allow to select a location in a select field and do not allow to confirm the accompanying course if location is empty.
* [accompanyingCourse] Add the administrative location in the available variables for document generation
* AddAddress: optimize loading: wait for the user finish typing;
* UserPicker: fix bug with deprecated role
* docgen: add base context + tests
* docgen: add age for person
* [household menu] fix filiation order https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/265
* [AddAddress]: optimize loading: wait for the user finish typing;
* [UserPicker]: fix bug with deprecated role
* [docgen]: add base context + tests
* [docgen]: add age for person
* [task]: fix dropdown menu style + fix bug in singleTaskController (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/338)
* Household: fix bug when moving person on the same day (see https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/281)
* Household: show date validFrom and validTo when moving
* address reference: add index for refid
* [accompanyingCourse_work] fix styles conflicts + fix bug with remove goal (remove goals one at a time)
* [accompanyingCourse] improve masonry on resume page, add origin
* [notification] new notification interface, can be associated to AccompanyingCourse/Period, Activities.
* List notifications, show, and comment in User section
* Notify button and contextual notification box on associated objects pages
* [accompanyingCourse] add a comment for each resource associated. A modal allow to save comment. Comment is displayed in on-the-fly show modal of the accompanyingCourse context (edit page + resume page).
### test release 2021-12-14
* [asideactivity] creation of aside activity category fixed (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/262)
* [vendee/person] fix typo "situation professionelle" => "situation professionnelle"
* [main] add availableForUsers condition from locationType in the location API endpoint (champs-libres/departement-de-la-vendee/accent-suivi-developpement#248)
* [main] add the current location of the user as API point + add it in the activity location list (champs-libres/departement-de-la-vendee/accent-suivi-developpement#247)
* [activity] improve show/new/edit templates, fix SEE and SEE_DETAILS acl
* [badges] create specific badge for TMS, and make person/thirdparty badges clickable with on-the-fly modal in :
* concerned groups items (activity, calendar)
* accompanyingCourseWork lists
* accompanyingCourse lists
* [acompanyingCourse] add initial comment on Resume page
* [person] create button full width (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/330)
### test release 2021-12-11
* [main] add order field to civility
* [main] change address format in case the country is France, in Address render box and address normalizer
* [person] add validator for accompanying period with a test on social issues (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/76)
* [activity] fix visibility for location
* [origin] fix origin: use correctly the translatable strings
* /!\ everyone must update the origin table. As there is only one row, execute `update chill_person_accompanying_period_origin set label = jsonb_build_object('fr', 'appel téléphonique');`
* [person] redirect bug fixed.
* [action] add an unrelated issue within action creation.
* [origin] fix origin: use correctly the translatable strings
* /!\ everyone must update the origin table. As there is only one row, execute `update chill_person_accompanying_period_origin set label = jsonb_build_object('fr', 'appel téléphonique');`
* [main] change order of civilities in civility fixtures (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/191)
* [person] set min attr in the minimum of children field (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/191)
* [person] add marital status date in person view (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/191)
* [person] show number of children + allow set number of children to null (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/191)
* [person] show acceptSMS option (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/191)
* [person] add death information in person render box in twig and vue render boxes (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/191)
* [asideactivity] creation of aside activity category fixed (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/262)
* [vendee/person] fix typo "situation professionelle" => "situation professionnelle"
* [accompanyingcourse_work] Changes in layout/behavior of edit form (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/321)
* [badge-entity] design coherency between pills badge-person and 3 kinds of badge-thirdparty
* [AddPersons] suggestions row are clickable, not only checkbox
### test release 2021-12-06
* [main] address: use search API end points for getting postal code and reference address (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316)
* [main] address: in edit mode, select the encoded values in multiselect for address reference and city (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316)
* [person search] fix bug when using birthdate after and birthdate before
* [person search] increase pertinence when lastname begins with search pattern
* [activity/actions] Améliore la cohérence du design entre
* la page résumé d'un parcours (liste d'actions récentes et liste d'activités récentes)
* la page liste des actions
* la page liste des activités (contexte personne / contexte parcours)
* [household] field to edit wheter person is titulaire of household or not removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/322)
* [activity] create work if a work with same social action is not associated to the activity
* [visgraph] improve and fix bugs on vis-network relationship graph
* [bugfix] posting of birth- and deathdate through api fixed.
* [suggestions] improve suggestions lists
### Test release 2021-11-19 - bis
* [household] do not allow to create two addresses on the same date
* [activity] handle case when there is no social action associated to social issue
* [activity] layout for issues / actions
* [activity][bugfix] in edit mode, the form will now load the social action list
### Test release 2021-11-29
* [person] suggest entities (person | thirdparty) when creating/editing the accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/119)
* [activity] add custom validation on the Activity class, based on what is required from the ActivityType (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/188)
* [main] translate multiselect messages when selecting/creating address
* [main] set the coordinates of the city when creating a new address OR choosing "pas d'adresse complète"
* Use the user.label in accompanying course banner, instead of username;
* fix: show validation message when closing accompanying course;
* [thirdparty] link from modal to thirdparty detail page fixed (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/228)
* [assets] new asset to style suggestions lists (with add/remove item link) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/258)
* [accompanyingCourseWorkEdit] improves hyphenation and line breaks for long badges
* [acompanyingCourse] improve Resume page
* complete all needed informations,
* actions and activities are clickables,
* better placement with js masonry blocks on top of content area,
* https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/101
* https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/295
* [activity/calendar] on show page, concerned groups of persons table adapt itself to isVisibles options
* [activity] remove the "plus" button in activity list
* [activity] check ACL on activity list in person context
* [list for accompanying course in person] filter list using ACL
* [validation] toasts are displayed for errors when modifying accompanying course (generalization required).
* [period] only the user can enable confidentiality
* add an endpoint for checking permissions. See https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/232
* [activity] for a new activity: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
* [calendar] for a new rdv: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
* [period] Validation added when period is confidential and confirmed -> user cannot be null.
## Test releases
### Test release 2021-11-22
* [activity] delete admin_user_show in twig template because this route is not defined and should be defined
* [activity] suggest requestor, user and ressources for adding persons|user|3rdparty
* [calendar] suggest persons, professionals and invites for adding persons|3rdparty|user
* [activity] take into account the restrictions on person|thirdparties|users visibilities defined in ActivityType
* [main] Add currentLocation to the User entity + add a page for selecting this location + add in the user menu (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/133)
* [activity] add user current location as default location for a new activity (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/133)
* [task] Select2 field in task form to allow search for a user (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/167)
* remove "search by phone configuration option": search by phone is now executed by default
* remplacer le classement par ordre alphabétique par un classement par ordre de pertinence, qui tient compte:
* de la présence d'une string avec le nom de la ville;
* de la similarité;
* du fait que la recherche commence par une partie du mot recherché
* ajouter la recherche par numéro de téléphone directement dans la barre de recherche et dans le formulaire recherche avancée;
* ajouter la recherche par date de naissance directement dans la barre de recherche;
* ajouter la recherche par ville dans la recherche avancée
* ajouter un lien vers le ménage dans les résultats de recherche
* ajouter l'id du parcours dans les résultats de recherche
* ajouter le demandeur dans les résultats de recherche
* ajout d'un bouton "recherche avancée" sur la page d'accueil
* [person] create an accompanying course: add client-side validation if no origin (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/210)
* [person] fix bounds for computing current person address: the new address appears immediatly
* [docgen] create a normalizer and serializer for normalization on doc format
* [person normalization] the key center is now "centers" and is an array. Empty array if no center
* [accompanyingCourse] Ability to close accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/296)
* [task] Select2 field in task form to allow search for a user (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/167)
* [list result] show all courses, except ones with period closed
* [accompanyingCourse] improve banner with small carousel to display slide social-issues or slide associated persons (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/69)
### Test release 2021-11-15
* [main] fix adding multiple AddresseDeRelais (combine PickAddressType with ChillCollection)
* [person]: do not suggest the current household of the person (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/51)
* [person]: display other phone numbers in view + add message in case no others phone numbers (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/184)
* unnecessary whitespace removed from person banner after person-id + double parentheses removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/290)
* [person]: delete accompanying period work, including related objects (cascade) (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/36)
* [address]: Display of incomplete address adjusted.
* [household]: improve relationship graph
* add form to create/edit/delete relationship link,
* improve graph refresh mechanism
* add feature to export canvas as image (png)
* [person suggest] In widget "add person", improve the pertinence of persons when one of the names starts with the pattern;
* [person] do not ask for center any more on person creation
* [3party] do not ask for center any more on 3party creation
## Test releases
### Test release 2021-11-08
* [person]: Display the name of a user when searching after a User (TMS)
* [person]: Add civility to the person
* [person]: Various improvements on the edit person form
* [person]: Set available_languages and available_countries as parameters for use in the edit person form
* [activity] Bugfix: documents can now be added to an activity.
* [tasks] improve tasks with filter order
* [tasks] refactor singleControllerTasks: limit the number of conditions from the context
* [validations] validation of accompanying period added: no duplicate participations or resources (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/60).
* [renderbox] If gender of person is not defined, no icon is displayed instead of neuter-icon (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/129).
* [confidential information] module added to blur confidential information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/248).
* refactor `AuthorizationHelper` and `UserACLAwareRepository` to fix constructor, and separate logic for parent role helper into `ParentRoleHelper`
* [main]: filter location and locationType in backend: exclude NULL names, only active and availableToUsers
* [activity]: perform client-side validation & show/hide fields in the "new location" modal
* [person]: normalize person with CenterResolverDispatcher and handle case where center is null or multiple in PersonRenderBox
* [docstore] voter for PersonDocument and AccompanyingCourseDocument on the 2.0 way (using VoterHelperFactory)
* [docstore] add authorization check inside controller and menu
* [activity]: fix inheritance for role `ACTIVITY FULL` and add missing acl in menu
* [person] show current address in search results
* [person] show alt names in search results
* [admin]: links to activity admin section added again.
* [household]: endDate field deleted from household edit form.
* [household]: View accompanying periods of current and old household members.
* [tasks]: different layout for task list / my tasks, and fix link to tasks in alert or in warning
* [admin]: links to activity admin section added again.
* [household]: household addresses ordered by ValidFrom date and by id to show the last created address on top.
* [socialWorkAction]: display of social issue and parent issues + banner context added.
* [DBAL dependencies] Upgrade to DBAL 3.1
### Test release 2021-10-27
* [person]: delete double actions buttons on search person page
* [person]: accompanying course work: remove creation date display the list of work + handle case when end date is null
* [main]: Add new pages with a menu for managing location and location type in the admin
* [main]: Add some fixtures for location type
* [calendar]: Pass the location when transforming a calendar item (rdv) into an activity
* [calendar]: Add a user menu for "my calendar"
### Test release 2021-10-18
* [3party]: french translation of contact and company
* [3party]: show parent in list
* [3party]: change color for badge "child"
* [3party]: fix address creation
* [household members editor] finalisation of editor
* [AccompanyingCourse banner]: replace translation referrer (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/70)
* [Location]: add location system in activity and RV (calendar). User can choose in location list or create a new location.
* [household]: add relationship page with dynamic data visualisation graph
## Test releases
### Test release 2021-10-11
* Address: zoom on postal code geometry + fix origin of manually entered postal code
* in the Address vue component, order the postal code and street address by alphabetic and numeric order
* add 3 new fields to PostalCode and adapt postal code command and fixtures
* [Aside activity] Fixes for aside activity
* categories with child
* fast creation buttons
* add ordering for types
* [AccompanyingCourse Resume page] dashboard for AccompanyingCourseWork and for Activities;
* Improve badges behaviour with small screens;
* [ThirdParty]:
* third party list
* create a kind contact/institution when create a new thirdparty, and set contact embedded as kind=child;
* filter thirdparties in list
* [FilterOrder]: add development kit for generating filter and ordering in list
* [Capitalization of names] person names are capitalized on creation, on prePersist event
* [On-The-Fly] modale works for showing, editing and creating person or thirdparty ;
* [AccompanyingCourse Resume page] associated persons list, can see household when hover, and with show on-the-fly modale when clicking person ;
### test release 2021-10-04
* [Household editor][UI] Update how household suggestion and addresses are picked;
See https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/80
* [AddAddress] Handle address suggestion;
* [CenterType][Create a person] when overriding the ACL rules, allow to show a PickCenterType
when no centers are reachable by the default ACL.
* [Household] Show comment event if no address are associated with the household;
* [Person results] Add requestor into search results:
* a badge "requestor" is shown into search results;
* periods where the person is only requestor (without participating) are also shown;
Issues:
* https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/13
* https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/199
* [Person form] "accept sms" not required:
https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/37
https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/221
* [Household editor] suggest only temporarily addresses;
See https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/82
* On-The-Fly modale works for showing, editing and creating person and thirdparty ;
* AccompanyingCourse Resume page: list associated persons by household, see household when hover, and show on-the-fly modale when clicking on person ;
* [AddAddress] Handle address suggestion;
* [AddAddress][Entity address]: add a link between address and address reference;
* [Household editor] suggest household by comparing the temporary addresses from courses;
See https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/81
* On-The-Fly modale works for showing, editing and creating person and thirdparty
## Test released
<!--
Coming soon...
DO NOT ADD unreleased items here. Add them under "Unreleased" title
### Test release yyyy-mm-dd
-->
## Stable releases
No stable releases for v2+

17
.changes/v2.1.0.md Normal file
View File

@ -0,0 +1,17 @@
## v2.1.0 - 2023-06-12
### Feature
* [docgen] allow to pick a third party when generating a document in context Activity, AccompanyingPeriod
### Fixed
* ([#111](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/111)) List of "my accompanying periods": separate the active and closed periods in two different lists, and show the inactive_long and inactive_short periods
### Security
* ([#105](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/105)) Rights are checked for display of 'accompanying period' tab in household menu. Rights are also checked for creation of 'accompanying period' from within household context
### DX
* Add methods to RegroupmentRepository and fullfill Center / Regroupment Doctrine mapping

12
.changes/v2.2.0.md Normal file
View File

@ -0,0 +1,12 @@
## v2.2.0 - 2023-06-18
### Feature
* When navigating from a workflow regarding to an evaluation's document to an accompanying course, scroll directly to the document, and blink to highlight this document
* Add notification to accompanying period work and work's evaluation's documents
* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113))[Export] Filter accompanying period by step at date: allow to pick multiple steps
* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113))[export] add a filter on accompanying period: filter by step between two dates
### Fixed
* use the correct annotation for the association between PersonCurrentCenter and Person
* ([#58](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/58))Fix birthdate timezone in PersonRenderBox
* ([#55](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/55))Fix the notification counter
### DX
* DQL function OVERLAPSI: simplify expression in postgresql

34
.changie.yaml Normal file
View File

@ -0,0 +1,34 @@
changesDir: .changes
unreleasedDir: unreleased
headerPath: header.tpl.md
changelogPath: CHANGELOG.md
versionExt: md
versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: '### {{.Kind}}'
changeFormat: >-
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{- end }}{{.Body}}
custom:
- key: Issue
label: Issue number (on chill-bundles repository) (optional)
optional: true
type: int
minInt: 1
body:
# allow multiline messages
block: true
kinds:
- label: Feature
auto: minor
- label: Deprecated
auto: minor
- label: Fixed
auto: patch
- label: Security
auto: patch
- label: DX
auto: patch
newlines:
afterChangelogHeader: 1
beforeChangelogVersion: 1
endOfVersion: 1
envPrefix: CHANGIE_

View File

@ -1,16 +1,50 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
* [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for stable releases;
* date versioning for test releases
## Unreleased ## v2.2.0 - 2023-06-18
### Feature
* When navigating from a workflow regarding to an evaluation's document to an accompanying course, scroll directly to the document, and blink to highlight this document
* Add notification to accompanying period work and work's evaluation's documents
* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113))[Export] Filter accompanying period by step at date: allow to pick multiple steps
* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113))[export] add a filter on accompanying period: filter by step between two dates
### Fixed
* use the correct annotation for the association between PersonCurrentCenter and Person
* ([#58](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/58))Fix birthdate timezone in PersonRenderBox
* ([#55](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/55))Fix the notification counter
### DX
* DQL function OVERLAPSI: simplify expression in postgresql
## v2.1.0 - 2023-06-12
### Feature
* [docgen] allow to pick a third party when generating a document in context Activity, AccompanyingPeriod
### Fixed
* ([#111](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/111)) List of "my accompanying periods": separate the active and closed periods in two different lists, and show the inactive_long and inactive_short periods
### Security
* ([#105](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/105)) Rights are checked for display of 'accompanying period' tab in household menu. Rights are also checked for creation of 'accompanying period' from within household context
### DX
* Add methods to RegroupmentRepository and fullfill Center / Regroupment Doctrine mapping
## 2.0.0
* this is a release to relaunch our proceess of release with semantic versioning
## Test releases
### 2.0.0-beta3
<!-- write down unreleased development here -->
* [person][export] Fixed: rename the alias for `accompanying_period` to `acp` in filter associated with person * [person][export] Fixed: rename the alias for `accompanying_period` to `acp` in filter associated with person
* [activity][export] Feature: improve label for aliases in "Filter by activity type" * [activity][export] Feature: improve label for aliases in "Filter by activity type"
* [activity][export] DX/Feature: use of an `ActivityTypeRepositoryInterface` instead of the old-style EntityRepository * [activity][export] DX/Feature: use of an `ActivityTypeRepositoryInterface` instead of the old-style EntityRepository
@ -18,9 +52,6 @@ and this project adheres to
* [person][export] Fixed: use left join for related entities in accompanying course aggregators * [person][export] Fixed: use left join for related entities in accompanying course aggregators
* [workflow] Feature: allow user to copy and send manually the access link for the workflow * [workflow] Feature: allow user to copy and send manually the access link for the workflow
* [workflow] Feature: show the email addresses that received an access link for the workflow * [workflow] Feature: show the email addresses that received an access link for the workflow
## Test releases
### 2.0.0-beta2 ### 2.0.0-beta2
* [workflow]: Fixed: the notification is sent when the user is added to the first step. * [workflow]: Fixed: the notification is sent when the user is added to the first step.

View File

@ -650,8 +650,8 @@ final class ActivityController extends AbstractController
throw $this->createNotFoundException('Accompanying Period not found'); throw $this->createNotFoundException('Accompanying Period not found');
} }
// TODO Add permission // TODO Add permission
// $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); // $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
} else { } else {
throw $this->createNotFoundException('Person or Accompanying Period not found'); throw $this->createNotFoundException('Person or Accompanying Period not found');
} }

View File

@ -26,12 +26,12 @@ final class PersonMenuBuilder implements LocalMenuBuilderInterface
/** /**
* @var AuthorizationCheckerInterface * @var AuthorizationCheckerInterface
*/ */
protected $authorizationChecker; private $authorizationChecker;
/** /**
* @var TranslatorInterface * @var TranslatorInterface
*/ */
protected $translator; private $translator;
public function __construct( public function __construct(
AuthorizationCheckerInterface $authorizationChecker, AuthorizationCheckerInterface $authorizationChecker,

View File

@ -24,6 +24,9 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@ -55,6 +58,10 @@ class ActivityContext implements
private TranslatorInterface $translator; private TranslatorInterface $translator;
private ThirdPartyRender $thirdPartyRender;
private ThirdPartyRepository $thirdPartyRepository;
public function __construct( public function __construct(
DocumentCategoryRepository $documentCategoryRepository, DocumentCategoryRepository $documentCategoryRepository,
NormalizerInterface $normalizer, NormalizerInterface $normalizer,
@ -63,7 +70,9 @@ class ActivityContext implements
PersonRenderInterface $personRender, PersonRenderInterface $personRender,
PersonRepository $personRepository, PersonRepository $personRepository,
TranslatorInterface $translator, TranslatorInterface $translator,
BaseContextData $baseContextData BaseContextData $baseContextData,
ThirdPartyRender $thirdPartyRender,
ThirdPartyRepository $thirdPartyRepository
) { ) {
$this->documentCategoryRepository = $documentCategoryRepository; $this->documentCategoryRepository = $documentCategoryRepository;
$this->normalizer = $normalizer; $this->normalizer = $normalizer;
@ -73,6 +82,8 @@ class ActivityContext implements
$this->personRepository = $personRepository; $this->personRepository = $personRepository;
$this->translator = $translator; $this->translator = $translator;
$this->baseContextData = $baseContextData; $this->baseContextData = $baseContextData;
$this->thirdPartyRender = $thirdPartyRender;
$this->thirdPartyRepository = $thirdPartyRepository;
} }
public function adminFormReverseTransform(array $data): array public function adminFormReverseTransform(array $data): array
@ -89,6 +100,8 @@ class ActivityContext implements
'person1Label' => $data['person1Label'] ?? $this->translator->trans('docgen.person 1'), 'person1Label' => $data['person1Label'] ?? $this->translator->trans('docgen.person 1'),
'person2' => $data['person2'] ?? false, 'person2' => $data['person2'] ?? false,
'person2Label' => $data['person2Label'] ?? $this->translator->trans('docgen.person 2'), 'person2Label' => $data['person2Label'] ?? $this->translator->trans('docgen.person 2'),
'thirdParty' => $data['thirdParty'] ?? false,
'thirdPartyLabel' => $data['thirdPartyLabel'] ?? $this->translator->trans('thirdParty'),
]; ];
} }
@ -118,6 +131,14 @@ class ActivityContext implements
->add('person2Label', TextType::class, [ ->add('person2Label', TextType::class, [
'label' => 'person 2 label', 'label' => 'person 2 label',
'required' => true, 'required' => true,
])
->add('thirdParty', CheckboxType::class, [
'required' => false,
'label' => 'docgen.Ask for thirdParty',
])
->add('thirdPartyLabel', TextType::class, [
'label' => 'docgen.thirdParty label',
'required' => true,
]); ]);
} }
@ -143,6 +164,20 @@ class ActivityContext implements
]); ]);
} }
} }
$thirdParties = $entity->getThirdParties();
if ($options['thirdParty'] ?? false) {
$builder->add('thirdParty', EntityType::class, [
'class' => ThirdParty::class,
'choices' => $thirdParties,
'choice_label' => fn (ThirdParty $p) => $this->thirdPartyRender->renderString($p, []),
'multiple' => false,
'required' => false,
'expanded' => true,
'label' => $options['thirdPartyLabel'],
'placeholder' => $this->translator->trans('Any third party selected'),
]);
}
} }
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
@ -157,6 +192,12 @@ class ActivityContext implements
} }
} }
if (null !== ($id = ($data['thirdParty'] ?? null))) {
$denormalized['thirdParty'] = $this->thirdPartyRepository->find($id);
} else {
$denormalized['thirdParty'] = null;
}
return $denormalized; return $denormalized;
} }
@ -165,9 +206,11 @@ class ActivityContext implements
$normalized = []; $normalized = [];
foreach (['mainPerson', 'person1', 'person2'] as $k) { foreach (['mainPerson', 'person1', 'person2'] as $k) {
$normalized[$k] = null === $data[$k] ? null : $data[$k]->getId(); $normalized[$k] = ($data[$k] ?? null)?->getId();
} }
$normalized['thirdParty'] = ($data['thirdParty'] ?? null)?->getId();
return $normalized; return $normalized;
} }
@ -196,6 +239,13 @@ class ActivityContext implements
} }
} }
if ($options['thirdParty']) {
$data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'], 'docgen', [
'docgen:expects' => ThirdParty::class,
'groups' => 'docgen:read'
]);
}
return $data; return $data;
} }
@ -235,7 +285,7 @@ class ActivityContext implements
{ {
$options = $template->getOptions(); $options = $template->getOptions();
return $options['mainPerson'] || $options['person1'] || $options['person2']; return $options['mainPerson'] || $options['person1'] || $options['person2'] || $options ['thirdParty'];
} }
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void

View File

@ -140,26 +140,36 @@ class ListActivitiesByAccompanyingPeriodContext implements
return $normalized; return $normalized;
} }
/**
* @return list
*/
private function filterActivitiesByUser(array $activities, User $user): array private function filterActivitiesByUser(array $activities, User $user): array
{ {
return array_filter( return array_values(
$activities, array_filter(
function ($activity) use ($user) { $activities,
$activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []); function ($activity) use ($user) {
return in_array($user->getUsername(), $activityUsernames, true); $activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []);
} return in_array($user->getUsername(), $activityUsernames, true);
}
)
); );
} }
/**
* @return list
*/
private function filterWorksByUser(array $works, User $user): array private function filterWorksByUser(array $works, User $user): array
{ {
return array_filter( return array_values(
$works, array_filter(
function ($work) use ($user) { $works,
$workUsernames = array_map(static fn ($user) => $user['username'], $work['referrers'] ?? []); function ($work) use ($user) {
$workUsernames = array_map(static fn ($user) => $user['username'], $work['referrers'] ?? []);
return in_array($user->getUsername(), $workUsernames, true); return in_array($user->getUsername(), $workUsernames, true);
} }
)
); );
} }

View File

@ -21,7 +21,7 @@ class CalculatorResult
public $label; public $label;
public $result; public float $result;
public $type; public $type;
} }

View File

@ -16,8 +16,14 @@
<td class="el-type"> <td class="el-type">
{% if f.isResource %} {% if f.isResource %}
{{ f.resource.name|localize_translatable_string }} {{ f.resource.name|localize_translatable_string }}
{% if f.resource.getKind is same as 'other' %}
: {{ f.getComment }}
{% endif %}
{% else %} {% else %}
{{ f.charge.name|localize_translatable_string }} {{ f.charge.name|localize_translatable_string }}
{% if f.charge.getKind is same as 'other' %}
: {{ f.getComment }}
{% endif %}
{% endif %} {% endif %}
</td> </td>
<td>{{ f.amount|format_currency('EUR') }}</td> <td>{{ f.amount|format_currency('EUR') }}</td>

View File

@ -13,6 +13,7 @@ namespace Chill\DocGeneratorBundle\Serializer\Normalizer;
use ArrayObject; use ArrayObject;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ReadableCollection;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@ -51,7 +52,9 @@ class CollectionDocGenNormalizer implements ContextAwareNormalizerInterface, Nor
return false; return false;
} }
return $data instanceof Collection return $data instanceof ReadableCollection
|| (null === $data && Collection::class === ($context['docgen:expects'] ?? null)); || (null === $data && Collection::class === ($context['docgen:expects'] ?? null))
|| (null === $data && ReadableCollection::class === ($context['docgen:expects'] ?? null))
;
} }
} }

View File

@ -13,6 +13,7 @@ namespace Chill\DocGeneratorBundle\Serializer\Normalizer;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper; use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\Common\Collections\ReadableCollection;
use ReflectionClass; use ReflectionClass;
use RuntimeException; use RuntimeException;
use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccess;
@ -271,6 +272,14 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
if ($isTranslatable) { if ($isTranslatable) {
$data[$key] = $this->translatableStringHelper $data[$key] = $this->translatableStringHelper
->localize($value); ->localize($value);
} elseif ($value instanceof ReadableCollection) {
// when normalizing collection, we should not preserve keys (to ensure that the result is a list)
// this is why we make call to the normalizer again to use the CollectionDocGenNormalizer
$data[$key] =
$this->normalizer->normalize($value, $format, array_merge(
$objectContext,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
} elseif (is_iterable($value)) { } elseif (is_iterable($value)) {
$arr = []; $arr = [];

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\tests\Serializer\Normalizer;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
* @coversNothing
*/
class CollectionDocGenNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalizeFilteredArray(): void
{
$coll = new ArrayCollection([
(object) ['v' => 'foo'],
(object) ['v' => 'bar'],
(object) ['v' => 'baz'],
]);
//filter to get non continuous indexes
$criteria = new Criteria();
$criteria->where(Criteria::expr()->neq('v', 'bar'));
$filtered = $coll->matching($criteria);
$normalized = $this->normalizer->normalize($filtered, 'docgen', []);
self::assertIsArray($normalized);
self::assertArrayHasKey(0, $normalized);
self::assertArrayHasKey(1, $normalized);
}
}

View File

@ -44,3 +44,9 @@ async function download_and_open(event: Event): Promise<void> {
} }
</script> </script>
<style scoped lang="sass">
i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
</style>

View File

@ -1,51 +1,96 @@
<template> <template>
<a :class="props.classes" @click="download_and_open($event)"> <a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)">
<i class="fa fa-download"></i> <i class="fa fa-download"></i>
Télécharger Télécharger
</a> </a>
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button">
<i class="fa fa-external-link"></i>
Ouvrir
</a>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {reactive} from "vue"; import {reactive, ref, nextTick, onMounted} from "vue";
import {build_download_info_link, download_and_decrypt_doc} from "./helpers"; import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
import mime from "mime"; import mime from "mime";
import {StoredObject} from "../../types"; import {StoredObject} from "../../types";
interface DownloadButtonConfig { interface DownloadButtonConfig {
storedObject: StoredObject, storedObject: StoredObject,
classes: {[k: string]: boolean}, classes: { [k: string]: boolean },
filename?: string, filename?: string,
} }
interface DownloadButtonState { interface DownloadButtonState {
content: null|string is_ready: boolean,
is_running: boolean,
href_url: string,
} }
const props = defineProps<DownloadButtonConfig>(); const props = defineProps<DownloadButtonConfig>();
const state: DownloadButtonState = reactive({content: null}); const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
const open_button = ref<HTMLAnchorElement | null>(null);
function buildDocumentName(): string {
const document_name = props.filename || 'document';
const ext = mime.getExtension(props.storedObject.type);
if (null !== ext) {
return document_name + '.' + ext;
}
return document_name;
}
async function download_and_open(event: Event): Promise<void> { async function download_and_open(event: Event): Promise<void> {
const button = event.target as HTMLAnchorElement; const button = event.target as HTMLAnchorElement;
if (null === state.content) { if (state.is_running) {
event.preventDefault(); console.log('state is running, aborting');
return;
}
state.is_running = true;
if (state.is_ready) {
console.log('state is ready. This should not happens');
return;
}
const urlInfo = build_download_info_link(props.storedObject.filename); const urlInfo = build_download_info_link(props.storedObject.filename);
let raw;
const raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv)); try {
state.content = window.URL.createObjectURL(raw); raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv));
} catch (e) {
button.href = window.URL.createObjectURL(raw); console.error("error while downloading and decrypting document");
button.type = props.storedObject.type; console.error(e);
throw e;
button.download = props.filename || 'document';
const ext = mime.getExtension(props.storedObject.type);
if (null !== ext) {
button.download = button.download + '.' + ext;
} }
}
button.click(); console.log('document downloading (and decrypting) successfully');
console.log('creating the url')
state.href_url = window.URL.createObjectURL(raw);
console.log('url created', state.href_url);
state.is_running = false;
state.is_ready = true;
console.log('new button marked as ready');
console.log('will click on button');
console.log('openbutton is now', open_button.value);
await nextTick();
console.log('next tick actions');
console.log('openbutton after next tick', open_button.value);
open_button.value?.click();
console.log('open button should have been clicked');
} }
</script> </script>
<style scoped lang="sass">
i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
</style>

View File

@ -40,5 +40,8 @@ async function beforeLeave(event: Event): Promise<true> {
</script> </script>
<style scoped lang="sass"> <style scoped lang="sass">
i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
</style> </style>

View File

@ -149,16 +149,21 @@ async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKe
} }
if (iv.length === 0) { if (iv.length === 0) {
console.log('returning document immediatly');
return rawResponse.blob(); return rawResponse.blob();
} }
console.log('start decrypting doc');
const rawBuffer = await rawResponse.arrayBuffer(); const rawBuffer = await rawResponse.arrayBuffer();
try { try {
const key = await window.crypto.subtle const key = await window.crypto.subtle
.importKey('jwk', keyData, { name: algo }, false, ['decrypt']); .importKey('jwk', keyData, { name: algo }, false, ['decrypt']);
console.log('key created');
const decrypted = await window.crypto.subtle const decrypted = await window.crypto.subtle
.decrypt({ name: algo, iv: iv }, key, rawBuffer); .decrypt({ name: algo, iv: iv }, key, rawBuffer);
console.log('doc decrypted');
return Promise.resolve(new Blob([decrypted])); return Promise.resolve(new Blob([decrypted]));
} catch (e) { } catch (e) {

View File

@ -1,5 +1,5 @@
{%- import "@ChillDocStore/Macro/macro.html.twig" as m -%} {%- import "@ChillDocStore/Macro/macro.html.twig" as m -%}
<div <div class="d-inline-flex"
data-download-buttons data-download-buttons
data-stored-object="{{ document_json|json_encode|escape('html_attr') }}" data-stored-object="{{ document_json|json_encode|escape('html_attr') }}"
data-can-edit="{{ can_edit ? '1' : '0' }}" data-can-edit="{{ can_edit ? '1' : '0' }}"

View File

@ -18,6 +18,7 @@ No document found: Aucun document trouvé
The document is successfully registered: Le document est enregistré The document is successfully registered: Le document est enregistré
The document is successfully updated: Le document est mis à jour The document is successfully updated: Le document est mis à jour
Any description: Aucune description Any description: Aucune description
See the document: Voir le document
document: document:
Any title: Aucun titre Any title: Aucun titre

View File

@ -2,11 +2,15 @@
<p>{% transchoice total with { '%pattern%' : pattern } %}%total% events match the search %pattern%{% endtranschoice %}</p> <p>{% transchoice total with { '%pattern%' : pattern } %}%total% events match the search %pattern%{% endtranschoice %}</p>
<style>
table.events td:last-child {
width: 15em;
}
</style>
{% if events|length > 0 %} {% if events|length > 0 %}
<p>{{ 'Results %start%-%end% of %total%'|trans({ '%start%' : start, '%end%': start + events|length, '%total%' : total } ) }}</p> <p>{{ 'Results %start%-%end% of %total%'|trans({ '%start%' : start, '%end%': start + events|length, '%total%' : total } ) }}</p>
<table class="table table-bordered border-dark align-middle events">
<table class="table events">
<thead> <thead>
<tr> <tr>
<th class="chill-red">{{ 'Name'|trans }}</th> <th class="chill-red">{{ 'Name'|trans }}</th>
@ -25,7 +29,7 @@
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
{# {% if is_granted('CHILL_EVENT_SEE_DETAILS', event) %} #} {# {% if is_granted('CHILL_EVENT_SEE_DETAILS', event) %} #}
<a href="{{ path('chill_event__event_show', { 'event_id' : event.id } ) }}" class="btn btn-dark"> <a href="{{ path('chill_event__event_show', { 'event_id' : event.id } ) }}" class="btn btn-view">
{{ 'See'|trans }} {{ 'See'|trans }}
</a> </a>
{# {% endif %} #} {# {% endif %} #}

View File

@ -24,7 +24,7 @@
{% block content %} {% block content %}
<h2>{{ 'Events participation' |trans }}</h2> <h2>{{ 'Events participation' |trans }}</h2>
<table class="table table-striped table-bordered mt-3 events"> <table class="table table-striped table-bordered border-dark align-middle mt-3 events">
<thead> <thead>
<tr> <tr>
<th class="chill-green">{{ 'Date'|trans }}</th> <th class="chill-green">{{ 'Date'|trans }}</th>

View File

@ -18,10 +18,10 @@
</a> </a>
</li> </li>
<li> <li>
{{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-chill-green' } }) }} {{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-submit' } }) }}
</li> </li>
</ul> </ul>
{{ form_end(form) }} {{ form_end(form) }}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,14 +1,14 @@
{% extends '@ChillEvent/layout.html.twig' %} {% extends '@ChillEvent/layout.html.twig' %}
{% block title 'Event : %label%'|trans({ '%label%' : event.name } ) %} {% block title 'Event : %label%'|trans({ '%label%' : event.name } ) %}
{% import 'ChillPersonBundle:Person:macro.html.twig' as person_macro %} {% import 'ChillPersonBundle:Person:macro.html.twig' as person_macro %}
{% block event_content -%} {% block event_content -%}
<div class="col-10"> <div class="col-10">
<h1>{{ 'Details of an event'|trans }}</h1> <h1>{{ 'Details of an event'|trans }}</h1>
<table class="table record_properties"> <table class="table table-bordered border-dark align-middle">
<tbody> <tbody>
<tr> <tr>
<th>{{ 'Name'|trans }}</th> <th>{{ 'Name'|trans }}</th>
@ -33,7 +33,7 @@
</tbody> </tbody>
</table> </table>
<ul class="record_actions"> <ul class="record_actions">
{% set returnPath = app.request.get('return_path') %} {% set returnPath = app.request.get('return_path') %}
@ -64,13 +64,13 @@
</li> </li>
</ul> </ul>
<h2>{{ 'Participations'|trans }}</h2> <h2>{{ 'Participations'|trans }}</h2>
{% set count = event.participations|length %} {% set count = event.participations|length %}
<p>{% transchoice count %}%count% participations to this event{% endtranschoice %}</p> <p>{% transchoice count %}%count% participations to this event{% endtranschoice %}</p>
{% if count > 0 %} {% if count > 0 %}
<table class="table"> <table class="table table-bordered border-dark align-middle">
<thead> <thead>
<tr> <tr>
<th>{{ 'Person'|trans }}</th> <th>{{ 'Person'|trans }}</th>
@ -108,10 +108,10 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
<ul class="record_actions"> <ul class="record_actions">
{% if count > 0 %} {% if count > 0 %}
<li><a href="{{ path('chill_event_participation_edit_multiple', { 'event_id' : event.id } ) }}" class="btn btn-edit">{{ 'Edit all the participations'|trans }}</a></li> <li><a href="{{ path('chill_event_participation_edit_multiple', { 'event_id' : event.id } ) }}" class="btn btn-edit">{{ 'Edit all the participations'|trans }}</a></li>

View File

@ -12,13 +12,14 @@ declare(strict_types=1);
namespace Chill\EventBundle\Search; namespace Chill\EventBundle\Search;
use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Repository\EventRepository;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\AbstractSearch; use Chill\MainBundle\Search\AbstractSearch;
use Chill\MainBundle\Search\SearchInterface; use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface as TemplatingEngine; use Symfony\Component\Templating\EngineInterface as TemplatingEngine;
use function count; use function count;
@ -40,15 +41,17 @@ class EventSearch extends AbstractSearch
{ {
public const NAME = 'event_regular'; public const NAME = 'event_regular';
private $security;
/** /**
* @var EntityRepository * @var EventRepository
*/ */
private $er; private $er;
/** /**
* @var AuthorizationHelper * @var AuthorizationHelper
*/ */
private $helper; private $authorizationHelper;
/** /**
* @var PaginatorFactory * @var PaginatorFactory
@ -60,21 +63,16 @@ class EventSearch extends AbstractSearch
*/ */
private $templating; private $templating;
/**
* @var \Chill\MainBundle\Entity\User
*/
private $user;
public function __construct( public function __construct(
TokenStorageInterface $tokenStorage, Security $security,
EntityRepository $eventRepository, EventRepository $eventRepository,
AuthorizationHelper $authorizationHelper, AuthorizationHelper $authorizationHelper,
TemplatingEngine $templating, TemplatingEngine $templating,
PaginatorFactory $paginatorFactory PaginatorFactory $paginatorFactory
) { ) {
$this->user = $tokenStorage->getToken()->getUser(); $this->security = $security;
$this->er = $eventRepository; $this->er = $eventRepository;
$this->helper = $authorizationHelper; $this->authorizationHelper = $authorizationHelper;
$this->templating = $templating; $this->templating = $templating;
$this->paginationFactory = $paginatorFactory; $this->paginationFactory = $paginatorFactory;
} }
@ -101,7 +99,7 @@ class EventSearch extends AbstractSearch
if ('html' === $format) { if ('html' === $format) {
return $this->templating->render( return $this->templating->render(
'ChillEventBundle:Event:list.html.twig', '@ChillEvent/Event/list.html.twig',
[ [
'events' => $this->search($terms, $start, $limit, $options), 'events' => $this->search($terms, $start, $limit, $options),
'pattern' => $this->recomposePattern($terms, $this->getAvailableTerms(), $terms['_domain']), 'pattern' => $this->recomposePattern($terms, $this->getAvailableTerms(), $terms['_domain']),
@ -140,8 +138,10 @@ class EventSearch extends AbstractSearch
protected function composeQuery(QueryBuilder &$qb, $terms) protected function composeQuery(QueryBuilder &$qb, $terms)
{ {
// add security clauses // add security clauses
$reachableCenters = $this->helper $reachableCenters = $this->authorizationHelper->getReachableCenters(
->getReachableCenters($this->user, 'CHILL_EVENT_SEE'); $this->security->getUser(),
'CHILL_EVENT_SEE'
);
if (count($reachableCenters) === 0) { if (count($reachableCenters) === 0) {
// add a clause to block all events // add a clause to block all events
@ -152,8 +152,9 @@ class EventSearch extends AbstractSearch
$orWhere = $qb->expr()->orX(); $orWhere = $qb->expr()->orX();
foreach ($reachableCenters as $center) { foreach ($reachableCenters as $center) {
$circles = $this->helper->getReachableScopes( $n = $n+1;
$this->user, $circles = $this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
'CHILL_EVENT_SEE', 'CHILL_EVENT_SEE',
$center $center
); );

View File

@ -17,7 +17,7 @@ services:
factory: ['@doctrine.orm.entity_manager', getRepository] factory: ['@doctrine.orm.entity_manager', getRepository]
arguments: arguments:
- 'Chill\EventBundle\Entity\Role' - 'Chill\EventBundle\Entity\Role'
chill_event.repository.status: chill_event.repository.status:
class: Doctrine\ORM\EntityRepository class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository] factory: ['@doctrine.orm.entity_manager', getRepository]

View File

@ -1,12 +1,11 @@
services: services:
chill_event.search_events: Chill\EventBundle\Search\EventSearch:
class: Chill\EventBundle\Search\EventSearch
arguments: arguments:
- "@security.token_storage" $security: '@Symfony\Component\Security\Core\Security'
- "@chill_event.repository.event" $eventRepository: "@chill_event.repository.event"
- "@chill.main.security.authorization.helper" $authorizationHelper: "@chill.main.security.authorization.helper"
- "@templating" $templating: "@templating"
- "@chill_main.paginator_factory" $paginatorFactory: "@chill_main.paginator_factory"
tags: tags:
- { name: chill.search, alias: 'event_regular' } - { name: chill.search, alias: 'event_regular' }

View File

@ -30,7 +30,7 @@ The event was created: L'événement a été créé
#crud participation #crud participation
Edit all the participations: Modifier toutes les participations Edit all the participations: Modifier toutes les participations
Edit the participation: Modifier la participation Edit the participation: Modifier la participation à l'événement
Participation Edit: Modifier une participation Participation Edit: Modifier une participation
Add a participation: Ajouter un participant Add a participation: Ajouter un participant
Participation creation: Ajouter une participation Participation creation: Ajouter une participation

View File

@ -36,5 +36,4 @@ class SynchronizeEntityInfoViewsCommand extends Command
return 0; return 0;
} }
} }

View File

@ -16,14 +16,18 @@ use Chill\MainBundle\Entity\RoleScope;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Form\PermissionsGroupType; use Chill\MainBundle\Form\PermissionsGroupType;
use Chill\MainBundle\Form\Type\ComposedRoleScopeType; use Chill\MainBundle\Form\Type\ComposedRoleScopeType;
use Chill\MainBundle\Repository\PermissionsGroupRepository;
use Chill\MainBundle\Repository\RoleScopeRepository;
use Chill\MainBundle\Security\RoleProvider; use Chill\MainBundle\Security\RoleProvider;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityManagerInterface;
use RuntimeException; use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -32,62 +36,28 @@ use function array_key_exists;
/** /**
* Class PermissionsGroupController. * Class PermissionsGroupController.
*/ */
class PermissionsGroupController extends AbstractController final class PermissionsGroupController extends AbstractController
{ {
/**
* @var RoleHierarchy
*/
private $roleHierarchy;
/**
* @var RoleProvider
*/
private $roleProvider;
/**
* @var TranslatableStringHelper
*/
private $translatableStringHelper;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var ValidatorInterface
*/
private $validator;
/** /**
* PermissionsGroupController constructor. * PermissionsGroupController constructor.
*/ */
public function __construct( public function __construct(
TranslatableStringHelper $translatableStringHelper, private readonly TranslatableStringHelper $translatableStringHelper,
RoleProvider $roleProvider, private readonly RoleProvider $roleProvider,
RoleHierarchy $roleHierarchy, private readonly RoleHierarchyInterface $roleHierarchy,
TranslatorInterface $translator, private readonly TranslatorInterface $translator,
ValidatorInterface $validator private readonly ValidatorInterface $validator,
private readonly EntityManagerInterface $em,
private readonly PermissionsGroupRepository $permissionsGroupRepository,
private readonly RoleScopeRepository $roleScopeRepository,
) { ) {
$this->translatableStringHelper = $translatableStringHelper;
$this->roleProvider = $roleProvider;
$this->roleHierarchy = $roleHierarchy;
$this->translator = $translator;
$this->validator = $validator;
} }
/** /**
* @param int $id
*
* @throws type
*
* @return Respon
*/ */
public function addLinkRoleScopeAction(Request $request, $id) public function addLinkRoleScopeAction(Request $request, int $id): Response
{ {
$em = $this->getDoctrine()->getManager(); $permissionsGroup = $this->permissionsGroupRepository->find($id);
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
if (!$permissionsGroup) { if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.'); throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@ -106,7 +76,7 @@ class PermissionsGroupController extends AbstractController
$violations = $this->validator->validate($permissionsGroup); $violations = $this->validator->validate($permissionsGroup);
if ($violations->count() === 0) { if ($violations->count() === 0) {
$em->flush(); $this->em->flush();
$this->addFlash( $this->addFlash(
'notice', 'notice',
@ -166,16 +136,15 @@ class PermissionsGroupController extends AbstractController
/** /**
* Creates a new PermissionsGroup entity. * Creates a new PermissionsGroup entity.
*/ */
public function createAction(Request $request) public function createAction(Request $request): Response
{ {
$permissionsGroup = new PermissionsGroup(); $permissionsGroup = new PermissionsGroup();
$form = $this->createCreateForm($permissionsGroup); $form = $this->createCreateForm($permissionsGroup);
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isValid()) { if ($form->isValid()) {
$em = $this->getDoctrine()->getManager(); $this->em->persist($permissionsGroup);
$em->persist($permissionsGroup); $this->em->flush();
$em->flush();
return $this->redirect($this->generateUrl( return $this->redirect($this->generateUrl(
'admin_permissionsgroup_edit', 'admin_permissionsgroup_edit',
@ -191,18 +160,11 @@ class PermissionsGroupController extends AbstractController
/** /**
* remove an association between permissionsGroup and roleScope. * remove an association between permissionsGroup and roleScope.
*
* @param int $pgid permissionsGroup id
* @param int $rsid roleScope id
*
* @return redirection to edit form
*/ */
public function deleteLinkRoleScopeAction($pgid, $rsid) public function deleteLinkRoleScopeAction(int $pgid, int $rsid): Response
{ {
$em = $this->getDoctrine()->getManager(); $permissionsGroup = $this->permissionsGroupRepository->find($pgid);
$roleScope = $this->roleScopeRepository->find($rsid);
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($pgid);
$roleScope = $em->getRepository(\Chill\MainBundle\Entity\RoleScope::class)->find($rsid);
if (!$permissionsGroup) { if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.'); throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@ -214,7 +176,7 @@ class PermissionsGroupController extends AbstractController
try { try {
$permissionsGroup->removeRoleScope($roleScope); $permissionsGroup->removeRoleScope($roleScope);
} catch (RuntimeException $ex) { } catch (RuntimeException) {
$this->addFlash( $this->addFlash(
'notice', 'notice',
$this->translator->trans("The role '%role%' and circle " $this->translator->trans("The role '%role%' and circle "
@ -231,7 +193,7 @@ class PermissionsGroupController extends AbstractController
)); ));
} }
$em->flush(); $this->em->flush();
if ($roleScope->getScope() !== null) { if ($roleScope->getScope() !== null) {
$this->addFlash( $this->addFlash(
@ -260,14 +222,10 @@ class PermissionsGroupController extends AbstractController
/** /**
* Displays a form to edit an existing PermissionsGroup entity. * Displays a form to edit an existing PermissionsGroup entity.
*
* @param mixed $id
*/ */
public function editAction($id) public function editAction(int $id): Response
{ {
$em = $this->getDoctrine()->getManager(); $permissionsGroup = $this->permissionsGroupRepository->find($id);
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
if (!$permissionsGroup) { if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.'); throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@ -311,11 +269,9 @@ class PermissionsGroupController extends AbstractController
/** /**
* Lists all PermissionsGroup entities. * Lists all PermissionsGroup entities.
*/ */
public function indexAction() public function indexAction(): Response
{ {
$em = $this->getDoctrine()->getManager(); $entities = $this->permissionsGroupRepository->findAllOrderedAlphabetically();
$entities = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->findAll();
return $this->render('@ChillMain/PermissionsGroup/index.html.twig', [ return $this->render('@ChillMain/PermissionsGroup/index.html.twig', [
'entities' => $entities, 'entities' => $entities,
@ -325,7 +281,7 @@ class PermissionsGroupController extends AbstractController
/** /**
* Displays a form to create a new PermissionsGroup entity. * Displays a form to create a new PermissionsGroup entity.
*/ */
public function newAction() public function newAction(): Response
{ {
$permissionsGroup = new PermissionsGroup(); $permissionsGroup = new PermissionsGroup();
$form = $this->createCreateForm($permissionsGroup); $form = $this->createCreateForm($permissionsGroup);
@ -338,14 +294,10 @@ class PermissionsGroupController extends AbstractController
/** /**
* Finds and displays a PermissionsGroup entity. * Finds and displays a PermissionsGroup entity.
*
* @param mixed $id
*/ */
public function showAction($id) public function showAction(int $id): Response
{ {
$em = $this->getDoctrine()->getManager(); $permissionsGroup = $this->permissionsGroupRepository->find($id);
$permissionsGroup = $em->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)->find($id);
if (!$permissionsGroup) { if (!$permissionsGroup) {
throw $this->createNotFoundException('Unable to find PermissionsGroup entity.'); throw $this->createNotFoundException('Unable to find PermissionsGroup entity.');
@ -393,15 +345,10 @@ class PermissionsGroupController extends AbstractController
/** /**
* Edits an existing PermissionsGroup entity. * Edits an existing PermissionsGroup entity.
*
* @param mixed $id
*/ */
public function updateAction(Request $request, $id) public function updateAction(Request $request, int $id): Response
{ {
$em = $this->getDoctrine()->getManager(); $permissionsGroup = $this->permissionsGroupRepository
$permissionsGroup = $em
->getRepository(\Chill\MainBundle\Entity\PermissionsGroup::class)
->find($id); ->find($id);
if (!$permissionsGroup) { if (!$permissionsGroup) {
@ -413,7 +360,7 @@ class PermissionsGroupController extends AbstractController
$editForm->handleRequest($request); $editForm->handleRequest($request);
if ($editForm->isValid()) { if ($editForm->isValid()) {
$em->flush(); $this->em->flush();
return $this->redirect($this->generateUrl('admin_permissionsgroup_edit', ['id' => $id])); return $this->redirect($this->generateUrl('admin_permissionsgroup_edit', ['id' => $id]));
} }
@ -452,18 +399,11 @@ class PermissionsGroupController extends AbstractController
/** /**
* get a role scope by his parameters. The role scope is persisted if it * get a role scope by his parameters. The role scope is persisted if it
* doesn't exists in database. * doesn't exist in database.
*
* @param Scope $scope
* @param string $role
*
* @return RoleScope
*/ */
protected function getPersistentRoleScopeBy($role, ?Scope $scope = null) protected function getPersistentRoleScopeBy(string $role, ?Scope $scope = null): RoleScope
{ {
$em = $this->getDoctrine()->getManager(); $roleScope = $this->roleScopeRepository
$roleScope = $em->getRepository(\Chill\MainBundle\Entity\RoleScope::class)
->findOneBy(['role' => $role, 'scope' => $scope]); ->findOneBy(['role' => $role, 'scope' => $scope]);
if (null === $roleScope) { if (null === $roleScope) {
@ -471,7 +411,7 @@ class PermissionsGroupController extends AbstractController
->setRole($role) ->setRole($role)
->setScope($scope); ->setScope($scope);
$em->persist($roleScope); $this->em->persist($roleScope);
} }
return $roleScope; return $roleScope;
@ -479,10 +419,8 @@ class PermissionsGroupController extends AbstractController
/** /**
* creates a form to add a role scope to permissionsgroup. * creates a form to add a role scope to permissionsgroup.
*
* @return \Symfony\Component\Form\Form The form
*/ */
private function createAddRoleScopeForm(PermissionsGroup $permissionsGroup) private function createAddRoleScopeForm(PermissionsGroup $permissionsGroup): FormInterface
{ {
return $this->createFormBuilder() return $this->createFormBuilder()
->setAction($this->generateUrl( ->setAction($this->generateUrl(
@ -499,10 +437,8 @@ class PermissionsGroupController extends AbstractController
* Creates a form to create a PermissionsGroup entity. * Creates a form to create a PermissionsGroup entity.
* *
* @param PermissionsGroup $permissionsGroup The entity * @param PermissionsGroup $permissionsGroup The entity
*
* @return \Symfony\Component\Form\Form The form
*/ */
private function createCreateForm(PermissionsGroup $permissionsGroup) private function createCreateForm(PermissionsGroup $permissionsGroup): FormInterface
{ {
$form = $this->createForm(PermissionsGroupType::class, $permissionsGroup, [ $form = $this->createForm(PermissionsGroupType::class, $permissionsGroup, [
'action' => $this->generateUrl('admin_permissionsgroup_create'), 'action' => $this->generateUrl('admin_permissionsgroup_create'),
@ -518,13 +454,11 @@ class PermissionsGroupController extends AbstractController
* Creates a form to delete a link to roleScope. * Creates a form to delete a link to roleScope.
* *
* @param mixed $permissionsGroup The entity id * @param mixed $permissionsGroup The entity id
*
* @return \Symfony\Component\Form\Form The form
*/ */
private function createDeleteRoleScopeForm( private function createDeleteRoleScopeForm(
PermissionsGroup $permissionsGroup, PermissionsGroup $permissionsGroup,
RoleScope $roleScope RoleScope $roleScope
) { ): FormInterface {
return $this->createFormBuilder() return $this->createFormBuilder()
->setAction($this->generateUrl( ->setAction($this->generateUrl(
'admin_permissionsgroup_delete_role_scope', 'admin_permissionsgroup_delete_role_scope',
@ -537,12 +471,8 @@ class PermissionsGroupController extends AbstractController
/** /**
* Creates a form to edit a PermissionsGroup entity. * Creates a form to edit a PermissionsGroup entity.
*
* @param PermissionsGroup $permissionsGroup The entity
*
* @return \Symfony\Component\Form\Form The form
*/ */
private function createEditForm(PermissionsGroup $permissionsGroup) private function createEditForm(PermissionsGroup $permissionsGroup): FormInterface
{ {
$form = $this->createForm(PermissionsGroupType::class, $permissionsGroup, [ $form = $this->createForm(PermissionsGroupType::class, $permissionsGroup, [
'action' => $this->generateUrl('admin_permissionsgroup_update', ['id' => $permissionsGroup->getId()]), 'action' => $this->generateUrl('admin_permissionsgroup_update', ['id' => $permissionsGroup->getId()]),
@ -556,10 +486,8 @@ class PermissionsGroupController extends AbstractController
/** /**
* expand roleScopes to be easily shown in template. * expand roleScopes to be easily shown in template.
*
* @return array
*/ */
private function getExpandedRoles(array $roleScopes) private function getExpandedRoles(array $roleScopes): array
{ {
$expandedRoles = []; $expandedRoles = [];
@ -567,10 +495,10 @@ class PermissionsGroupController extends AbstractController
if (!array_key_exists($roleScope->getRole(), $expandedRoles)) { if (!array_key_exists($roleScope->getRole(), $expandedRoles)) {
$expandedRoles[$roleScope->getRole()] = $expandedRoles[$roleScope->getRole()] =
array_map( array_map(
static fn (Role $role) => $role->getRole(), static fn ($role) => $role,
$this->roleHierarchy $this->roleHierarchy
->getReachableRoles( ->getReachableRoleNames(
[new Role($roleScope->getRole())] [$roleScope->getRole()]
) )
); );
} }

View File

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class UserExportController
{
public function __construct(
private UserRepositoryInterface $userRepository,
private Security $security,
private TranslatorInterface $translator,
) {
}
/**
* @throws \League\Csv\CannotInsertRecord
* @throws \League\Csv\Exception
* @throws \League\Csv\UnavailableStream
*
* @Route("/{_locale}/admin/main/users/export/list.{_format}", requirements={"_format": "csv"}, name="chill_main_users_export_list")
*/
public function userList(Request $request, string $_format = 'csv'): StreamedResponse
{
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list');
}
$users = $this->userRepository->findAllAsArray($request->getLocale());
$csv = Writer::createFromPath('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.' . $e),
[
'id',
'username',
'email',
'enabled',
'civility_id',
'civility_abbreviation',
'civility_name',
'label',
'mainCenter_id' ,
'mainCenter_name',
'mainScope_id',
'mainScope_name',
'userJob_id',
'userJob_name',
'currentLocation_id',
'currentLocation_name',
'mainLocation_id',
'mainLocation_name',
'absenceStart'
]
)
);
$csv->addFormatter(fn (array $row) => null !== ($row['absenceStart'] ?? null) ? array_merge($row, ['absenceStart' => $row['absenceStart']->format('Y-m-d')]) : $row);
$csv->insertAll($users);
return new StreamedResponse(
function () use ($csv) {
foreach ($csv->chunk(1024) as $chunk) {
echo $chunk;
flush();
}
},
Response::HTTP_OK,
[
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; users.csv',
]
);
}
/**
* @return StreamedResponse
* @throws \League\Csv\CannotInsertRecord
* @throws \League\Csv\Exception
* @throws \League\Csv\UnavailableStream
*
* @Route("/{_locale}/admin/main/users/export/permissions.{_format}", requirements={"_format": "csv"}, name="chill_main_users_export_permissions")
*/
public function userPermissionsList(string $_format = 'csv'): StreamedResponse
{
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list');
}
$userPermissions = $this->userRepository->findAllUserACLAsArray();
$csv = Writer::createFromPath('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.' . $e),
[
'id',
'username',
'email',
'label',
'enabled',
'center_id',
'center_name',
'permissionsGroup_id',
'permissionsGroup_name',
]
)
);
$csv->insertAll($userPermissions);
return new StreamedResponse(
function () use ($csv) {
foreach ($csv->chunk(1024) as $chunk) {
echo $chunk;
flush();
}
},
Response::HTTP_OK,
[
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; users.csv',
]
);
}
}

View File

@ -359,9 +359,9 @@ class WorkflowController extends AbstractController
} }
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context) // TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData(); $entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData(); $entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData(); $entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
$workflow->apply($entityWorkflow, $transition); $workflow->apply($entityWorkflow, $transition);

View File

@ -90,16 +90,14 @@ class OverlapsI extends FunctionNode
if ($part instanceof PathExpression) { if ($part instanceof PathExpression) {
return sprintf( return sprintf(
"CASE WHEN %s IS NOT NULL THEN %s ELSE '%s'::date END", "COALESCE(%s, '%s'::date)",
$part->dispatch($sqlWalker),
$part->dispatch($sqlWalker), $part->dispatch($sqlWalker),
$p $p
); );
} }
return sprintf( return sprintf(
"CASE WHEN %s::date IS NOT NULL THEN %s::date ELSE '%s'::date END", "COALESCE(%s::date, '%s'::date)",
$part->dispatch($sqlWalker),
$part->dispatch($sqlWalker), $part->dispatch($sqlWalker),
$p $p
); );

View File

@ -48,12 +48,19 @@ class Center implements HasCenterInterface
*/ */
private string $name = ''; private string $name = '';
/**
* @var Collection<Regroupment>
* @ORM\ManyToMany(targetEntity=Regroupment::class, mappedBy="centers")
*/
private Collection $regroupments;
/** /**
* Center constructor. * Center constructor.
*/ */
public function __construct() public function __construct()
{ {
$this->groupCenters = new \Doctrine\Common\Collections\ArrayCollection(); $this->groupCenters = new ArrayCollection();
$this->regroupments = new ArrayCollection();
} }
/** /**
@ -106,6 +113,14 @@ class Center implements HasCenterInterface
return $this->name; return $this->name;
} }
/**
* @return Collection<Regroupment>
*/
public function getRegroupments(): Collection
{
return $this->regroupments;
}
/** /**
* @param $name * @param $name
* *

View File

@ -22,11 +22,12 @@ use Doctrine\ORM\Mapping as ORM;
class Regroupment class Regroupment
{ {
/** /**
* @var Center
* @ORM\ManyToMany( * @ORM\ManyToMany(
* targetEntity=Center::class * targetEntity=Center::class,
* inversedBy="regroupments"
* ) * )
* @ORM\Id * @ORM\Id
* @var Collection<Center>
*/ */
private Collection $centers; private Collection $centers;
@ -52,6 +53,26 @@ class Regroupment
$this->centers = new ArrayCollection(); $this->centers = new ArrayCollection();
} }
public function addCenter(Center $center): self
{
if (!$this->centers->contains($center)) {
$this->centers->add($center);
$center->getRegroupments()->add($this);
}
return $this;
}
public function removeCenter(Center $center): self
{
if ($this->centers->contains($center)) {
$this->centers->removeElement($center);
$center->getRegroupments()->removeElement($this);
}
return $this;
}
public function getCenters(): Collection public function getCenters(): Collection
{ {
return $this->centers; return $this->centers;

View File

@ -506,11 +506,11 @@ class User implements UserInterface
* *
* @return User * @return User
*/ */
public function setUsername($name) public function setUsername(?string $name)
{ {
$this->username = $name; $this->username = (string) $name;
if (empty($this->getLabel())) { if ("" === trim($this->getLabel())) {
$this->setLabel($name); $this->setLabel($name);
} }

View File

@ -31,7 +31,7 @@ class RegroupmentType extends AbstractType
->add('centers', EntityType::class, [ ->add('centers', EntityType::class, [
'class' => Center::class, 'class' => Center::class,
'multiple' => true, 'multiple' => true,
'attr' => ['class' => 'select2'], 'expanded' => true,
]) ])
->add('isActive', CheckboxType::class, [ ->add('isActive', CheckboxType::class, [
'label' => 'Actif ?', 'label' => 'Actif ?',

View File

@ -43,6 +43,10 @@ class EntityToJsonTransformer implements DataTransformerInterface
public function reverseTransform($value) public function reverseTransform($value)
{ {
if ("" === $value) {
return null;
}
$denormalized = json_decode($value, true, 512, JSON_THROW_ON_ERROR); $denormalized = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
if ($this->multiple) { if ($this->multiple) {
@ -56,10 +60,6 @@ class EntityToJsonTransformer implements DataTransformerInterface
); );
} }
if ('' === $value) {
return null;
}
return $this->denormalizeOne($denormalized); return $this->denormalizeOne($denormalized);
} }

View File

@ -34,9 +34,13 @@ class NotificationPresence
$this->notificationRepository = $notificationRepository; $this->notificationRepository = $notificationRepository;
} }
public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
* @return array{unread: int, sent: int, total: int}
*/
public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId, array $more = [], array $options = []): array
{ {
if (array_key_exists($relatedEntityClass, $this->cache) && array_key_exists($relatedEntityId, $this->cache[$relatedEntityClass])) { if ([] === $more && array_key_exists($relatedEntityClass, $this->cache) && array_key_exists($relatedEntityId, $this->cache[$relatedEntityClass])) {
return $this->cache[$relatedEntityClass][$relatedEntityId]; return $this->cache[$relatedEntityClass][$relatedEntityId];
} }
@ -46,21 +50,25 @@ class NotificationPresence
$counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated( $counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated(
$relatedEntityClass, $relatedEntityClass,
$relatedEntityId, $relatedEntityId,
$user $user,
$more
); );
$this->cache[$relatedEntityClass][$relatedEntityId] = $counter; if ([] === $more) {
$this->cache[$relatedEntityClass][$relatedEntityId] = $counter;
}
return $counter; return $counter;
} }
return ['unread' => 0, 'read' => 0]; return ['unread' => 0, 'sent' => 0, 'total' => 0];
} }
/** /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
* @return array|Notification[] * @return array|Notification[]
*/ */
public function getNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array public function getNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId, array $more = []): array
{ {
$user = $this->security->getUser(); $user = $this->security->getUser();
@ -68,7 +76,8 @@ class NotificationPresence
return $this->notificationRepository->findNotificationByRelatedEntityAndUserAssociated( return $this->notificationRepository->findNotificationByRelatedEntityAndUserAssociated(
$relatedEntityClass, $relatedEntityClass,
$relatedEntityId, $relatedEntityId,
$user $user,
$more
); );
} }

View File

@ -34,24 +34,30 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
} }
public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $more = [], array $options = []): string
{ {
return $environment->render( return $environment->render(
'@ChillMain/Notification/extension_counter_notifications_for.html.twig', '@ChillMain/Notification/extension_counter_notifications_for.html.twig',
[ [
'counter' => $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId), 'counter' => $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId, $more),
] ]
); );
} }
public function countNotificationsFor(string $relatedEntityClass, int $relatedEntityId, array $options = []): array /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
*/
public function countNotificationsFor(string $relatedEntityClass, int $relatedEntityId, array $more = [], array $options = []): array
{ {
return $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId); return $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId, $more);
} }
public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
*/
public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $more = [], array $options = []): string
{ {
$notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId); $notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId, $more);
if ([] === $notifications) { if ([] === $notifications) {
return ''; return '';

View File

@ -29,6 +29,15 @@ final class NotificationRepository implements ObjectRepository
private EntityRepository $repository; private EntityRepository $repository;
private const BASE_COUNTER_SQL = <<<'SQL'
SELECT
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = :userid and cmnau.notification_id = cmn.id))::int) AS unread,
SUM((cmn.sender_id = :userid)::int) AS sent,
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_user cmnau_all WHERE user_id = :userid and cmnau_all.notification_id = cmn.id))::int) + SUM((cmn.sender_id = :userid)::int) AS total
FROM chill_main_notification cmn
SQL;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->em = $entityManager; $this->em = $entityManager;
@ -51,29 +60,45 @@ final class NotificationRepository implements ObjectRepository
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
* @return array{unread: int, sent: int, total: int}
*/
public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user, array $more = []): array
{ {
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) { $sqlParams = ['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()];
$sql =
'SELECT
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = :userid and cmnau.notification_id = cmn.id))::int) AS unread,
SUM((cmn.sender_id = :userid)::int) AS sent,
COUNT(cmn.*) AS total
FROM chill_main_notification cmn
WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
$this->notificationByRelatedEntityAndUserAssociatedStatement = if ([] === $more) {
$this->em->getConnection()->prepare($sql); if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
$sql = self::BASE_COUNTER_SQL . ' WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
$this->notificationByRelatedEntityAndUserAssociatedStatement =
$this->em->getConnection()->prepare($sql);
}
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement
->executeQuery($sqlParams);
$result = $results->fetchAssociative();
$results->free();
} else {
$wheres = [];
foreach ([
['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId],
...$more
] as $k => ['relatedEntityClass' => $relClass, 'relatedEntityId' => $relId]) {
$wheres[] = "(relatedEntityClass = :relatedEntityClass_{$k} AND relatedEntityId = :relatedEntityId_{$k})";
$sqlParams["relatedEntityClass_{$k}"] = $relClass;
$sqlParams["relatedEntityId_{$k}"] = $relId;
}
$sql = self::BASE_COUNTER_SQL . ' WHERE sender_id IS NOT NULL AND (' . implode(' OR ', $wheres) . ')';
$result = $this->em->getConnection()->fetchAssociative($sql, $sqlParams);
} }
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement return array_map(fn (?int $number) => $number ?? 0, $result);
->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()]);
$result = $results->fetchAssociative();
$results->free();
return $result;
} }
public function countUnreadByUser(User $user): int public function countUnreadByUser(User $user): int
@ -167,8 +192,8 @@ final class NotificationRepository implements ObjectRepository
} }
/** /**
* @param mixed|null $limit * @param int|null $limit
* @param mixed|null $offset * @param int|null $offset
* *
* @return Notification[] * @return Notification[]
*/ */
@ -178,13 +203,15 @@ final class NotificationRepository implements ObjectRepository
} }
/** /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
* @return array|Notification[] * @return array|Notification[]
*/ */
public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user, array $more): array
{ {
return return
$this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user) $this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user, $more)
->select('n') ->select('n')
->addOrderBy('n.date', 'DESC')
->getQuery() ->getQuery()
->getResult(); ->getResult();
} }
@ -222,13 +249,36 @@ final class NotificationRepository implements ObjectRepository
return Notification::class; return Notification::class;
} }
private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): QueryBuilder /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
*/
private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user, array $more = []): QueryBuilder
{ {
$qb = $this->repository->createQueryBuilder('n'); $qb = $this->repository->createQueryBuilder('n');
// add condition for related entity (in main arguments, and in more)
$or = $qb->expr()->orX($qb->expr()->andX(
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'),
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')
));
$qb $qb
->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass')) ->setParameter('relatedEntityClass', $relatedEntityClass)
->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')) ->setParameter('relatedEntityId', $relatedEntityId);
foreach ($more as $k => ['relatedEntityClass' => $relatedClass, 'relatedEntityId' => $relatedId]) {
$or->add(
$qb->expr()->andX(
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass_'.$k),
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId_'.$k)
)
);
$qb
->setParameter('relatedEntityClass_'.$k, $relatedClass)
->setParameter('relatedEntityId_'.$k, $relatedId);
}
$qb
->andWhere($or)
->andWhere($qb->expr()->isNotNull('n.sender')) ->andWhere($qb->expr()->isNotNull('n.sender'))
->andWhere( ->andWhere(
$qb->expr()->orX( $qb->expr()->orX(
@ -236,8 +286,6 @@ final class NotificationRepository implements ObjectRepository
$qb->expr()->eq('n.sender', ':user') $qb->expr()->eq('n.sender', ':user')
) )
) )
->setParameter('relatedEntityClass', $relatedEntityClass)
->setParameter('relatedEntityId', $relatedEntityId)
->setParameter('user', $user); ->setParameter('user', $user);
return $qb; return $qb;

View File

@ -38,6 +38,19 @@ final class PermissionsGroupRepository implements ObjectRepository
return $this->repository->findAll(); return $this->repository->findAll();
} }
/**
* @return list<PermissionsGroup>
*/
public function findAllOrderedAlphabetically(): array
{
$qb = $this->repository->createQueryBuilder('pg');
return $qb->select(['pg', 'pg.name AS HIDDEN sort_name'])
->orderBy('sort_name')
->getQuery()
->getResult();
}
/** /**
* @param mixed|null $limit * @param mixed|null $limit
* @param mixed|null $offset * @param mixed|null $offset

View File

@ -14,6 +14,8 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\Regroupment;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
final class RegroupmentRepository implements ObjectRepository final class RegroupmentRepository implements ObjectRepository
@ -59,6 +61,30 @@ final class RegroupmentRepository implements ObjectRepository
return $this->repository->findOneBy($criteria, $orderBy); return $this->repository->findOneBy($criteria, $orderBy);
} }
/**
* @throws NonUniqueResultException
* @throws NoResultException
*/
public function findOneByName(string $name): ?Regroupment
{
return $this->repository->createQueryBuilder('r')
->where('LOWER(r.name) = LOWER(:searched)')
->setParameter('searched', $name)
->getQuery()
->getSingleResult();
}
/**
* @return array<Regroupment>
*/
public function findRegroupmentAssociatedToNoCenter(): array
{
return $this->repository->createQueryBuilder('r')
->where('SIZE(r.centers) = 0')
->getQuery()
->getResult();
}
public function getClassName() public function getClassName()
{ {
return Regroupment::class; return Regroupment::class;

View File

@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\GroupCenter; use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException; use Doctrine\ORM\NoResultException;
@ -76,6 +77,81 @@ final class UserRepository implements UserRepositoryInterface
return $this->repository->findAll(); return $this->repository->findAll();
} }
/**
* @param string $lang
*/
public function findAllAsArray(string $lang): iterable
{
$dql = sprintf(<<<'DQL'
SELECT
u.id AS id,
u.username AS username,
u.email,
u.enabled,
IDENTITY(u.civility) AS civility_id,
JSON_EXTRACT(civility.abbreviation, :lang) AS civility_abbreviation,
JSON_EXTRACT(civility.name, :lang) AS civility_name,
u.label,
mainCenter.id AS mainCenter_id,
mainCenter.name AS mainCenter_name,
IDENTITY(u.mainScope) AS mainScope_id,
JSON_EXTRACT(mainScope.name, :lang) AS mainScope_name,
IDENTITY(u.userJob) AS userJob_id,
JSON_EXTRACT(userJob.label, :lang) AS userJob_name,
currentLocation.id AS currentLocation_id,
currentLocation.name AS currentLocation_name,
mainLocation.id AS mainLocation_id,
mainLocation.name AS mainLocation_name,
u.absenceStart
FROM Chill\MainBundle\Entity\User u
LEFT JOIN u.civility civility
LEFT JOIN u.currentLocation currentLocation
LEFT JOIN u.mainLocation mainLocation
LEFT JOIN u.mainCenter mainCenter
LEFT JOIN u.mainScope mainScope
LEFT JOIN u.userJob userJob
ORDER BY u.label
DQL);
$query = $this->entityManager->createQuery($dql)
->setHydrationMode(AbstractQuery::HYDRATE_ARRAY)
->setParameter('lang', $lang)
;
foreach ($query->toIterable() as $u) {
yield $u;
}
}
public function findAllUserACLAsArray(): iterable
{
$sql = <<<'SQL'
SELECT
u.id,
u.username,
u.email,
u.label,
u.enabled,
c.id AS center_id,
c.name AS center_name,
pg.id AS permissionsGroup_id,
pg.name AS permissionsGroup_name
FROM users u
LEFT JOIN user_groupcenter ON u.id = user_groupcenter.user_id
LEFT JOIN group_centers ON user_groupcenter.groupcenter_id = group_centers.id
LEFT JOIN centers c on group_centers.center_id = c.id
LEFT JOIN permission_groups pg on group_centers.permissionsgroup_id = pg.id
ORDER BY u.username, c.name, pg.name
SQL;
$query = $this->entityManager->getConnection()->executeQuery($sql);
foreach ($query->iterateAssociative() as $u) {
yield $u;
}
}
/** /**
* @param mixed|null $limit * @param mixed|null $limit
* @param mixed|null $offset * @param mixed|null $offset

View File

@ -14,6 +14,9 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
/**
* @template ObjectRepository<User>
*/
interface UserRepositoryInterface extends ObjectRepository interface UserRepositoryInterface extends ObjectRepository
{ {
public function countBy(array $criteria): int; public function countBy(array $criteria): int;
@ -24,20 +27,25 @@ interface UserRepositoryInterface extends ObjectRepository
public function countByUsernameOrEmail(string $pattern): int; public function countByUsernameOrEmail(string $pattern): int;
public function find($id, $lockMode = null, $lockVersion = null): ?User;
/** /**
* @return User[] * Find a list of all users.
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
* *
* @return User[] * The main purpose for this method is to provide a lightweight list of all users in the database.
*
* @param string $lang The lang to display all the translatable string (no fallback if not present)
* @return iterable<array{id: int, username: string, email: string, enabled: bool, civility_id: int, civility_abbreviation: string, civility_name: string, label: string, mainCenter_id: int, mainCenter_name: string, mainScope_id: int, mainScope_name: string, userJob_id: int, userJob_name: string, currentLocation_id: int, currentLocation_name: string, mainLocation_id: int, mainLocation_name: string, absenceStart: \DateTimeImmutable}>
*/ */
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array; public function findAllAsArray(string $lang): iterable;
/**
* Find a list of permissions associated to each users.
*
* The main purpose for this method is to provide a lightweight list of all permissions group and center
* associated to each user.
*
* @return iterable<array{id: int, username: string, email: string, enabled: bool, center_id: int, center_name: string, permissionsGroup_id: int, permissionsGroup_name: string}>
*/
public function findAllUserACLAsArray(): iterable;
/** /**
* @return array|User[] * @return array|User[]
@ -53,8 +61,6 @@ interface UserRepositoryInterface extends ObjectRepository
public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array; public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?User;
public function findOneByUsernameOrEmail(string $pattern): ?User; public function findOneByUsernameOrEmail(string $pattern): ?User;
/** /**
@ -68,6 +74,4 @@ interface UserRepositoryInterface extends ObjectRepository
* @param mixed $flag * @param mixed $flag
*/ */
public function findUsersHavingFlags($flag, array $amongstUsers = []): array; public function findUsersHavingFlags($flag, array $amongstUsers = []): array;
public function getClassName(): string;
} }

View File

@ -39,8 +39,13 @@
{{ 'This will eventually restrict your possibilities in filtering the data.'|trans }}</p> {{ 'This will eventually restrict your possibilities in filtering the data.'|trans }}</p>
<h3 class="m-3">{{ 'Center'|trans }}</h3> <h3 class="m-3">{{ 'Center'|trans }}</h3>
{{ form_widget(form.centers.center) }} {{ form_widget(form.centers.center) }}
<div class="mb-3 mt-3">
<input id="toggle-check-all" class="btn btn-misc" type= "button" onclick='uncheckAll(this)' value="{{ 'uncheck all centers'|trans|e('html_attr') }}"/>
</div>
{% if form.centers.regroupment is defined %} {% if form.centers.regroupment is defined %}
<h3 class="m-3">{{ 'Pick aggregated centers'|trans }}</h3> <h3 class="m-3">{{ 'Pick aggregated centers'|trans }}</h3>
{{ form_widget(form.centers.regroupment) }} {{ form_widget(form.centers.regroupment) }}
@ -53,3 +58,15 @@
</div> </div>
{% endblock content %} {% endblock content %}
{% block js %}
<script>
const uncheckAll = () => {
const allCenters = document.getElementsByName('centers[center][]');
allCenters.forEach(checkbox => checkbox.checked = false)
}
</script>
{% endblock js %}

View File

@ -9,4 +9,4 @@
{{ 'notification.counter unread notifications'|trans({'unread': counter.unread }) }} {{ 'notification.counter unread notifications'|trans({'unread': counter.unread }) }}
</span> </span>
{% endif %} {% endif %}
</div> </div>

View File

@ -98,6 +98,18 @@
<li class='cancel'> <li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a> <a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li> </li>
<li>
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-download"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ path('chill_main_users_export_list') }}">{{ 'admin.users.export_list_csv'|trans }}</a></li>
<li><a class="dropdown-item" href="{{ path('chill_main_users_export_permissions') }}">{{ 'admin.users.export_permissions_csv'|trans }}</a></li>
</ul>
</div>
</li>
<li> <li>
<a href="{{ path('chill_crud_admin_user_new') }}" class="btn btn-create">{{ 'Create'|trans }}</a> <a href="{{ path('chill_crud_admin_user_new') }}" class="btn btn-create">{{ 'Create'|trans }}</a>
</li> </li>

View File

@ -37,3 +37,6 @@ services:
Chill\MainBundle\Controller\RegroupmentController: Chill\MainBundle\Controller\RegroupmentController:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
Chill\MainBundle\Controller\UserExportController:
tags: ['controller.service_arguments']

View File

@ -285,6 +285,8 @@ The export will contains only data from the picked centers.: L'export ne contien
This will eventually restrict your possibilities in filtering the data.: Les possibilités de filtrages seront adaptées aux droits de consultation pour les centres choisis. This will eventually restrict your possibilities in filtering the data.: Les possibilités de filtrages seront adaptées aux droits de consultation pour les centres choisis.
Go to export options: Vers la préparation de l'export Go to export options: Vers la préparation de l'export
Pick aggregated centers: Regroupement de centres Pick aggregated centers: Regroupement de centres
uncheck all centers: Désélectionner tous les centres
check all centers: Sélectionner tous les centres
# export creation step 'export' : choose aggregators, filtering and formatter # export creation step 'export' : choose aggregators, filtering and formatter
Formatter: Mise en forme Formatter: Mise en forme
Choose the formatter: Choisissez le format d'export voulu. Choose the formatter: Choisissez le format d'export voulu.
@ -610,3 +612,32 @@ absence:
You are listed as absent, as of: Votre absence est indiquée à partir du You are listed as absent, as of: Votre absence est indiquée à partir du
No absence listed: Aucune absence indiquée. No absence listed: Aucune absence indiquée.
Is absent: Absent? Is absent: Absent?
admin:
users:
export_list_csv: Liste des utilisateurs (format CSV)
export_permissions_csv: Association utilisateurs - groupes de permissions - centre (format CSV)
export:
id: Identifiant
username: Nom d'utilisateur
email: Courriel
enabled: Activé
civility_id: Identifiant civilité
civility_abbreviation: Abbréviation civilité
civility_name: Civilité
label: Label
mainCenter_id: Identifiant centre principal
mainCenter_name: Centre principal
mainScope_id: Identifiant service principal
mainScope_name: Service principal
userJob_id: Identifiant métier
userJob_name: Métier
currentLocation_id: Identifiant localisation actuelle
currentLocation_name: Localisation actuelle
mainLocation_id: Identifiant localisation principale
mainLocation_name: Localisation principale
absenceStart: Absent à partir du
center_id: Identifiant du centre
center_name: Centre
permissionsGroup_id: Identifiant du groupe de permissions
permissionsGroup_name: Groupe de permissions

View File

@ -51,7 +51,10 @@ class UserRefEventSubscriber implements EventSubscriberInterface
public function onStateEntered(EnteredEvent $enteredEvent): void public function onStateEntered(EnteredEvent $enteredEvent): void
{ {
if ($enteredEvent->getMarking()->has(AccompanyingPeriod::STEP_CONFIRMED)) { if (
$enteredEvent->getMarking()->has(AccompanyingPeriod::STEP_CONFIRMED)
and $enteredEvent->getTransition()->getName() === 'confirm'
) {
$this->onPeriodConfirmed($enteredEvent->getSubject()); $this->onPeriodConfirmed($enteredEvent->getSubject());
} }
} }

View File

@ -27,7 +27,7 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface
{ {
$now = $this->clock->now(); $now = $this->clock->now();
if ($now->sub(new \DateInterval('P1D')) < $cronJobExecution->getLastStart()) { if (null !== $cronJobExecution && $now->sub(new \DateInterval('P1D')) < $cronJobExecution->getLastStart()) {
return false; return false;
} }

View File

@ -34,5 +34,4 @@ class AccompanyingPeriodStepChangeMessageHandler implements MessageHandlerInterf
($this->changer)($period, $message->getTransition()); ($this->changer)($period, $message->getTransition());
} }
} }

View File

@ -84,5 +84,4 @@ class AccompanyingPeriodStepChangeRequestor
$this->messageBus->dispatch(new AccompanyingPeriodStepChangeRequestMessage($accompanyingPeriodId, 'mark_active')); $this->messageBus->dispatch(new AccompanyingPeriodStepChangeRequestMessage($accompanyingPeriodId, 'mark_active'));
} }
} }
} }

View File

@ -13,7 +13,11 @@ namespace Chill\PersonBundle\Actions\Remove;
use Chill\PersonBundle\Actions\ActionEvent; use Chill\PersonBundle\Actions\ActionEvent;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Relationships\Relationship;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -42,7 +46,7 @@ class PersonMove
protected $eventDispatcher; protected $eventDispatcher;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
EventDispatcherInterface $eventDispatcher EventDispatcherInterface $eventDispatcher
) { ) {
$this->em = $em; $this->em = $em;
@ -84,8 +88,11 @@ class PersonMove
} }
foreach ($metadata->getAssociationMappings() as $field => $mapping) { foreach ($metadata->getAssociationMappings() as $field => $mapping) {
if (Person::class === $mapping['targetEntity']) { if (in_array($mapping['sourceEntity'], $this->getIgnoredEntities(), true)) {
if (in_array($metadata->getName(), $toDelete, true)) { continue;
}
if (Person::class === $mapping['targetEntity'] and true === $mapping['isOwningSide']) {
if (in_array($mapping['sourceEntity'], $toDelete, true)) {
$sql = $this->createDeleteSQL($metadata, $from, $field); $sql = $this->createDeleteSQL($metadata, $from, $field);
$event = new ActionEvent( $event = new ActionEvent(
$from->getId(), $from->getId(),
@ -120,7 +127,7 @@ class PersonMove
return $sqls; return $sqls;
} }
protected function createDeleteSQL(ClassMetadata $metadata, Person $from, $field): string private function createDeleteSQL(ClassMetadata $metadata, Person $from, $field): string
{ {
$mapping = $metadata->getAssociationMapping($field); $mapping = $metadata->getAssociationMapping($field);
@ -137,26 +144,41 @@ class PersonMove
); );
} }
protected function createMoveSQL(ClassMetadata $metadata, Person $from, Person $to, $field): string private function createMoveSQL(ClassMetadata $metadata, Person $from, Person $to, $field): string
{ {
$mapping = $metadata->getAssociationMapping($field); $mapping = $metadata->getAssociationMapping($field);
// Set part of the query, aka <here> in "UPDATE table SET <here> " // Set part of the query, aka <here> in "UPDATE table SET <here> "
$sets = []; $sets = [];
foreach ($mapping['joinColumns'] as $columns) {
$sets[] = sprintf('%s = %d', $columns['name'], $to->getId());
}
$conditions = []; $conditions = [];
$tableName = '';
foreach ($mapping['joinColumns'] as $columns) { if (array_key_exists('joinTable', $mapping)) {
$conditions[] = sprintf('%s = %d', $columns['name'], $from->getId()); $tableName = (null !== ($mapping['joinTable']['schema'] ?? null) ? $mapping['joinTable']['schema'] . '.' : '')
. $mapping['joinTable']['name'];
foreach ($mapping['joinTable']['inverseJoinColumns'] as $columns) {
$sets[] = sprintf('%s = %d', $columns['name'], $to->getId());
}
foreach ($mapping['joinTable']['inverseJoinColumns'] as $columns) {
$conditions[] = sprintf('%s = %d', $columns['name'], $from->getId());
}
} elseif (array_key_exists('joinColumns', $mapping)) {
$tableName = $this->getTableName($metadata);
foreach ($mapping['joinColumns'] as $columns) {
$sets[] = sprintf('%s = %d', $columns['name'], $to->getId());
}
foreach ($mapping['joinColumns'] as $columns) {
$conditions[] = sprintf('%s = %d', $columns['name'], $from->getId());
}
} }
return sprintf( return sprintf(
'UPDATE %s SET %s WHERE %s', 'UPDATE %s SET %s WHERE %s',
$this->getTableName($metadata), $tableName,
implode(' ', $sets), implode(' ', $sets),
implode(' AND ', $conditions) implode(' AND ', $conditions)
); );
@ -166,10 +188,23 @@ class PersonMove
* return an array of classes where entities should be deleted * return an array of classes where entities should be deleted
* instead of moved. * instead of moved.
*/ */
protected function getDeleteEntities(): array private function getDeleteEntities(): array
{ {
return [ return [
AccompanyingPeriod::class, Person\PersonCenterHistory::class,
HouseholdMember::class,
AccompanyingPeriodParticipation::class,
AccompanyingPeriod\AccompanyingPeriodWork::class,
Relationship::class
];
}
private function getIgnoredEntities(): array
{
return [
Person\PersonCurrentAddress::class,
PersonHouseholdAddress::class,
Person\PersonCenterCurrent::class,
]; ];
} }

View File

@ -42,10 +42,18 @@ final class ImportSocialWorkMetadata extends Command
protected function configure() protected function configure()
{ {
$description = 'Imports a structured table containing social issues, social actions, objectives, results and evaluations.';
$help = 'File to csv format, no headers, semi-colon as delimiter, datas sorted by alphabetical order, column after column.'. PHP_EOL
. 'Columns are: social issues parent, social issues child, social actions parent, social actions child, goals, results, evaluations.'. PHP_EOL
. PHP_EOL
. 'See social_work_metadata.csv as example.'. PHP_EOL;
$this $this
->setName('chill:person:import-socialwork') ->setName('chill:person:import-socialwork')
->addOption('filepath', 'f', InputOption::VALUE_REQUIRED, 'The file to import.') ->addOption('filepath', 'f', InputOption::VALUE_REQUIRED, 'The file to import.')
->addOption('language', 'l', InputOption::VALUE_OPTIONAL, 'The default language'); ->addOption('language', 'l', InputOption::VALUE_OPTIONAL, 'The default language')
->setDescription($description)
->setHelp($help);
} }
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)

View File

@ -247,7 +247,7 @@ class PersonDuplicateController extends Controller
); );
$duplicatePersons = $this->similarPersonMatcher-> $duplicatePersons = $this->similarPersonMatcher->
matchPerson($person, $personNotDuplicateRepository, 0.5, SimilarPersonMatcher::SIMILAR_SEARCH_ORDER_BY_ALPHABETICAL); matchPerson($person, 0.5, SimilarPersonMatcher::SIMILAR_SEARCH_ORDER_BY_ALPHABETICAL, false);
$notDuplicatePersons = $personNotDuplicateRepository->findNotDuplicatePerson($person); $notDuplicatePersons = $personNotDuplicateRepository->findNotDuplicatePerson($person);
@ -264,14 +264,14 @@ class PersonDuplicateController extends Controller
$nb_activity = $em->getRepository(Activity::class)->findBy(['person' => $id]); $nb_activity = $em->getRepository(Activity::class)->findBy(['person' => $id]);
$nb_document = $em->getRepository(PersonDocument::class)->findBy(['person' => $id]); $nb_document = $em->getRepository(PersonDocument::class)->findBy(['person' => $id]);
$nb_event = $em->getRepository(Participation::class)->findBy(['person' => $id]); // $nb_event = $em->getRepository(Participation::class)->findBy(['person' => $id]);
$nb_task = $em->getRepository(SingleTask::class)->countByParameters(['person' => $id]); $nb_task = $em->getRepository(SingleTask::class)->countByParameters(['person' => $id]);
$person = $em->getRepository(Person::class)->findOneBy(['id' => $id]); $person = $em->getRepository(Person::class)->findOneBy(['id' => $id]);
return [ return [
'nb_activity' => count($nb_activity), 'nb_activity' => count($nb_activity),
'nb_document' => count($nb_document), 'nb_document' => count($nb_document),
'nb_event' => count($nb_event), // 'nb_event' => count($nb_event),
'nb_task' => $nb_task, 'nb_task' => $nb_task,
'nb_addresses' => count($person->getAddresses()), 'nb_addresses' => count($person->getAddresses()),
]; ];

View File

@ -12,9 +12,11 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller; namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
class UserAccompanyingPeriodController extends AbstractController class UserAccompanyingPeriodController extends AbstractController
@ -32,12 +34,24 @@ class UserAccompanyingPeriodController extends AbstractController
/** /**
* @Route("/{_locale}/person/accompanying-periods/my", name="chill_person_accompanying_period_user") * @Route("/{_locale}/person/accompanying-periods/my", name="chill_person_accompanying_period_user")
*/ */
public function listAction(Request $request) public function listAction(Request $request): Response
{ {
$total = $this->accompanyingPeriodRepository->countBy(['user' => $this->getUser(), 'step' => ['CONFIRMED', 'CLOSED']]); $active = $request->query->getBoolean('active', true);
$steps = match ($active) {
true => [
AccompanyingPeriod::STEP_CONFIRMED,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
],
false => [
AccompanyingPeriod::STEP_CLOSED,
]
};
$total = $this->accompanyingPeriodRepository->countBy(['user' => $this->getUser(), 'step' => $steps]);
$pagination = $this->paginatorFactory->create($total); $pagination = $this->paginatorFactory->create($total);
$accompanyingPeriods = $this->accompanyingPeriodRepository->findBy( $accompanyingPeriods = $this->accompanyingPeriodRepository->findBy(
['user' => $this->getUser(), 'step' => ['CONFIRMED', 'CLOSED']], ['user' => $this->getUser(), 'step' => $steps],
['openingDate' => 'DESC'], ['openingDate' => 'DESC'],
$pagination->getItemsPerPage(), $pagination->getItemsPerPage(),
$pagination->getCurrentPageFirstItemNumber() $pagination->getCurrentPageFirstItemNumber()
@ -46,13 +60,14 @@ class UserAccompanyingPeriodController extends AbstractController
return $this->render('@ChillPerson/AccompanyingPeriod/user_periods_list.html.twig', [ return $this->render('@ChillPerson/AccompanyingPeriod/user_periods_list.html.twig', [
'accompanyingPeriods' => $accompanyingPeriods, 'accompanyingPeriods' => $accompanyingPeriods,
'pagination' => $pagination, 'pagination' => $pagination,
'active' => $active,
]); ]);
} }
/** /**
* @Route("/{_locale}/person/accompanying-periods/my/drafts", name="chill_person_accompanying_period_draft_user") * @Route("/{_locale}/person/accompanying-periods/my/drafts", name="chill_person_accompanying_period_draft_user")
*/ */
public function listDraftsAction(Request $request) public function listDraftsAction(): Response
{ {
$total = $this->accompanyingPeriodRepository->countBy(['user' => $this->getUser(), 'step' => 'DRAFT']); $total = $this->accompanyingPeriodRepository->countBy(['user' => $this->getUser(), 'step' => 'DRAFT']);
$pagination = $this->paginatorFactory->create($total); $pagination = $this->paginatorFactory->create($total);

View File

@ -59,6 +59,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* ) * )
* @Serializer\Groups({"read", "docgen:read"}) * @Serializer\Groups({"read", "docgen:read"})
* @ORM\OrderBy({"startDate": "DESC", "id": "DESC"}) * @ORM\OrderBy({"startDate": "DESC", "id": "DESC"})
* @var Collection<AccompanyingPeriodWorkEvaluation>
* *
* @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer` * @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer`
*/ */
@ -278,6 +279,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this->accompanyingPeriod; return $this->accompanyingPeriod;
} }
/**
* @return Collection<AccompanyingPeriodWorkEvaluation>
*/
public function getAccompanyingPeriodWorkEvaluations(): Collection public function getAccompanyingPeriodWorkEvaluations(): Collection
{ {
return $this->accompanyingPeriodWorkEvaluations; return $this->accompanyingPeriodWorkEvaluations;

View File

@ -79,6 +79,7 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
* ) * )
* @ORM\OrderBy({"createdAt": "DESC", "id": "DESC"}) * @ORM\OrderBy({"createdAt": "DESC", "id": "DESC"})
* @Serializer\Groups({"read"}) * @Serializer\Groups({"read"})
* @var Collection<AccompanyingPeriodWorkEvaluationDocument>
*/ */
private Collection $documents; private Collection $documents;
@ -204,7 +205,7 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
} }
/** /**
* @return Collection * @return Collection<AccompanyingPeriodWorkEvaluationDocument>
*/ */
public function getDocuments() public function getDocuments()
{ {

View File

@ -213,6 +213,10 @@ class Household
return null; return null;
} }
/**
* @Serializer\Groups({"docgen:read"})
* @Serializer\SerializedName("current_composition")
*/
public function getCurrentComposition(?DateTimeImmutable $at = null): ?HouseholdComposition public function getCurrentComposition(?DateTimeImmutable $at = null): ?HouseholdComposition
{ {
$at ??= new DateTimeImmutable('today'); $at ??= new DateTimeImmutable('today');

View File

@ -44,6 +44,7 @@ class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterfa
/** /**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null}) * @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Assert\GreaterThanOrEqual(propertyPath="startDate", groups={"Default", "household_composition"}) * @Assert\GreaterThanOrEqual(propertyPath="startDate", groups={"Default", "household_composition"})
* @Serializer\Groups({"docgen:read"})
*/ */
private ?DateTimeImmutable $endDate = null; private ?DateTimeImmutable $endDate = null;
@ -56,6 +57,7 @@ class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterfa
/** /**
* @ORM\ManyToOne(targetEntity=HouseholdCompositionType::class) * @ORM\ManyToOne(targetEntity=HouseholdCompositionType::class)
* @ORM\JoinColumn(nullable=false) * @ORM\JoinColumn(nullable=false)
* @Serializer\Groups({"docgen:read"})
*/ */
private ?HouseholdCompositionType $householdCompositionType = null; private ?HouseholdCompositionType $householdCompositionType = null;
@ -71,12 +73,14 @@ class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterfa
* @ORM\Column(type="integer", nullable=true, options={"default": null}) * @ORM\Column(type="integer", nullable=true, options={"default": null})
* @Assert\NotNull * @Assert\NotNull
* @Assert\GreaterThanOrEqual(0, groups={"Default", "household_composition"}) * @Assert\GreaterThanOrEqual(0, groups={"Default", "household_composition"})
* @Serializer\Groups({"docgen:read"})
*/ */
private ?int $numberOfChildren = null; private ?int $numberOfChildren = null;
/** /**
* @ORM\Column(type="date_immutable", nullable=false) * @ORM\Column(type="date_immutable", nullable=false)
* @Assert\NotNull(groups={"Default", "household_composition"}) * @Assert\NotNull(groups={"Default", "household_composition"})
* @Serializer\Groups({"docgen:read"})
*/ */
private ?DateTimeImmutable $startDate = null; private ?DateTimeImmutable $startDate = null;

View File

@ -45,7 +45,7 @@ class PersonCenterCurrent
private ?int $id = null; private ?int $id = null;
/** /**
* @ORM\ManyToOne(targetEntity=Person::class, inversedBy="centerCurrent") * @ORM\OneToOne(targetEntity=Person::class, inversedBy="centerCurrent")
*/ */
private Person $person; private Person $person;

View File

@ -98,7 +98,7 @@ final class GeographicalUnitStatAggregator implements AggregatorInterface
'acp_geog_units' 'acp_geog_units'
); );
$qb->andWhere($qb->expr()->eq('acp_geog_units.layer', ':acp_geog_unit_layer')); $qb->andWhere($qb->expr()->in('acp_geog_units.layer', ':acp_geog_unit_layer'));
$qb->setParameter('acp_geog_unit_layer', $data['level']); $qb->setParameter('acp_geog_unit_layer', $data['level']);
@ -129,6 +129,8 @@ final class GeographicalUnitStatAggregator implements AggregatorInterface
'class' => GeographicalUnitLayer::class, 'class' => GeographicalUnitLayer::class,
'choices' => $this->geographicalUnitLayerRepository->findAllHavingUnits(), 'choices' => $this->geographicalUnitLayerRepository->findAllHavingUnits(),
'choice_label' => fn (GeographicalUnitLayer $item) => $this->translatableStringHelper->localize($item->getName()), 'choice_label' => fn (GeographicalUnitLayer $item) => $this->translatableStringHelper->localize($item->getName()),
'multiple' => true,
'expanded' => true,
]); ]);
} }

View File

@ -101,6 +101,8 @@ class GeographicalUnitAggregator implements AggregatorInterface
'class' => GeographicalUnitLayer::class, 'class' => GeographicalUnitLayer::class,
'choices' => $this->geographicalUnitLayerRepository->findAllHavingUnits(), 'choices' => $this->geographicalUnitLayerRepository->findAllHavingUnits(),
'choice_label' => fn (GeographicalUnitLayer $item) => $this->translatableStringHelper->localize($item->getName()), 'choice_label' => fn (GeographicalUnitLayer $item) => $this->translatableStringHelper->localize($item->getName()),
'multiple' => true,
'expanded' => true,
]); ]);
} }

View File

@ -107,7 +107,6 @@ class ListHouseholdInPeriod implements ListInterface, GroupedExportInterface
return $this->aggregateStringHelper->getLabelMulti($key, $values, 'export.list.household.' . $key); return $this->aggregateStringHelper->getLabelMulti($key, $values, 'export.list.household.' . $key);
case 'compositionType': case 'compositionType':
//dump($values);
return $this->translatableStringHelper->getLabel($key, $values, 'export.list.household.' . $key); return $this->translatableStringHelper->getLabel($key, $values, 'export.list.household.' . $key);
default: default:

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array;
class StepFilterBetweenDates implements FilterInterface
{
private const DEFAULT_CHOICE = [
AccompanyingPeriod::STEP_CONFIRMED,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
];
private const STEPS = [
'course.draft' => AccompanyingPeriod::STEP_DRAFT,
'course.confirmed' => AccompanyingPeriod::STEP_CONFIRMED,
'course.closed' => AccompanyingPeriod::STEP_CLOSED,
'course.inactive_short' => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
'course.inactive_long' => AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
];
private RollingDateConverterInterface $rollingDateConverter;
private TranslatorInterface $translator;
public function __construct(
RollingDateConverterInterface $rollingDateConverter,
TranslatorInterface $translator
) {
$this->rollingDateConverter = $rollingDateConverter;
$this->translator = $translator;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$alias = 'acp_filter_by_step_between_dat_alias';
$steps = 'acp_filter_by_step_between_dat_steps';
$from = 'acp_filter_by_step_between_dat_from';
$to = 'acp_filter_by_step_between_dat_to';
$qb
->andWhere(
$qb->expr()->exists(
"SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodStepHistory::class . " {$alias} " .
"WHERE {$alias}.step IN (:{$steps}) AND OVERLAPSI ({$alias}.startDate, {$alias}.endDate),(:{$from}, :{$to}) = TRUE " .
"AND {$alias}.period = acp"
)
)
->setParameter($from, $this->rollingDateConverter->convert($data['date_from']))
->setParameter($to, $this->rollingDateConverter->convert($data['date_to']))
->setParameter($steps, $data['accepted_steps_multi']);
}
public function applyOn()
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('accepted_steps_multi', ChoiceType::class, [
'label' => 'export.filter.course.by_step.steps',
'choices' => self::STEPS,
'multiple' => true,
'expanded' => true,
])
->add('date_from', PickRollingDateType::class, [
'label' => 'export.filter.course.by_step.date_from',
])
->add('date_to', PickRollingDateType::class, [
'label' => 'export.filter.course.by_step.date_to',
]);
}
public function getFormDefaultData(): array
{
return [
'accepted_steps_multi' => self::DEFAULT_CHOICE,
'date_from' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'date_to' => new RollingDate(RollingDate::T_TODAY),
];
}
public function describeAction($data, $format = 'string')
{
$steps = array_map(fn (string $step) => $this->translator->trans(array_flip(self::STEPS)[$step]), $data['accepted_steps_multi']);
return ['export.filter.course.by_step.Filtered by steps: only %step% and between %date_from% and %date_to%', [
'%step%' => implode(', ', $steps),
'%date_from%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
'%date_to%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
]];
}
public function getTitle()
{
return 'export.filter.course.by_step.Filter by step between dates';
}
}

View File

@ -23,11 +23,15 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array; use function in_array;
class StepFilter implements FilterInterface class StepFilterOnDate implements FilterInterface
{ {
private const A = 'acp_filter_bystep_stephistories'; private const A = 'acp_filter_bystep_stephistories';
private const DEFAULT_CHOICE = AccompanyingPeriod::STEP_CONFIRMED; private const DEFAULT_CHOICE = [
AccompanyingPeriod::STEP_CONFIRMED,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT,
AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG,
];
private const P = 'acp_step_filter_date'; private const P = 'acp_step_filter_date';
@ -79,7 +83,7 @@ class StepFilter implements FilterInterface
$qb->expr()->in(self::A . '.step', ':acp_filter_by_step_steps') $qb->expr()->in(self::A . '.step', ':acp_filter_by_step_steps')
) )
->setParameter(self::P, $this->rollingDateConverter->convert($data['calc_date'])) ->setParameter(self::P, $this->rollingDateConverter->convert($data['calc_date']))
->setParameter('acp_filter_by_step_steps', $data['accepted_steps']); ->setParameter('acp_filter_by_step_steps', $data['accepted_steps_multi']);
} }
public function applyOn() public function applyOn()
@ -90,30 +94,36 @@ class StepFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder) public function buildForm(FormBuilderInterface $builder)
{ {
$builder $builder
->add('accepted_steps', ChoiceType::class, [ ->add('accepted_steps_multi', ChoiceType::class, [
'label' => 'export.filter.course.by_step.steps',
'choices' => self::STEPS, 'choices' => self::STEPS,
'multiple' => false, 'multiple' => true,
'expanded' => true, 'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]) ])
->add('calc_date', PickRollingDateType::class, [ ->add('calc_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_step.date_calc', 'label' => 'export.filter.course.by_step.date_calc',
'data' => new RollingDate(RollingDate::T_TODAY),
]); ]);
} }
public function getFormDefaultData(): array
{
return [
'accepted_steps_multi' => self::DEFAULT_CHOICE,
'calc_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function describeAction($data, $format = 'string') public function describeAction($data, $format = 'string')
{ {
$step = array_flip(self::STEPS)[$data['accepted_steps']]; $steps = array_map(fn (string $step) => $this->translator->trans(array_flip(self::STEPS)[$step]), $data['accepted_steps_multi']);
return ['Filtered by steps: only %step%', [ return ['Filtered by steps: only %step%', [
'%step%' => $this->translator->trans($step), '%step%' => implode(', ', $steps),
]]; ]];
} }
public function getTitle() public function getTitle()
{ {
return 'Filter by step'; return 'export.filter.course.by_step.Filter by step';
} }
} }

View File

@ -12,7 +12,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Menu; namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class HouseholdMenuBuilder implements LocalMenuBuilderInterface class HouseholdMenuBuilder implements LocalMenuBuilderInterface
@ -22,9 +24,12 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface
*/ */
protected $translator; protected $translator;
public function __construct(TranslatorInterface $translator) private Security $security;
public function __construct(TranslatorInterface $translator, Security $security)
{ {
$this->translator = $translator; $this->translator = $translator;
$this->security = $security;
} }
public function buildMenu($menuId, MenuItem $menu, array $parameters): void public function buildMenu($menuId, MenuItem $menu, array $parameters): void
@ -53,12 +58,14 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface
], ]) ], ])
->setExtras(['order' => 17]); ->setExtras(['order' => 17]);
$menu->addChild($this->translator->trans('household.Accompanying period'), [ if ($this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['household'])) {
'route' => 'chill_person_household_accompanying_period', $menu->addChild($this->translator->trans('household.Accompanying period'), [
'routeParameters' => [ 'route' => 'chill_person_household_accompanying_period',
'household_id' => $household->getId(), 'routeParameters' => [
], ]) 'household_id' => $household->getId(),
->setExtras(['order' => 20]); ],])
->setExtras(['order' => 20]);
}
$menu->addChild($this->translator->trans('household.Addresses'), [ $menu->addChild($this->translator->trans('household.Addresses'), [
'route' => 'chill_person_household_addresses', 'route' => 'chill_person_household_addresses',

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\ResidentialAddressRepository; use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@ -106,17 +107,19 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
->setExtras([ ->setExtras([
'order' => 99999, 'order' => 99999,
]); ]);
/*
$menu->addChild($this->translator->trans('Person duplicate'), [ if ($this->security->isGranted(PersonVoter::DUPLICATE, $parameters['person'])) {
'route' => 'chill_person_duplicate_view', $menu->addChild($this->translator->trans('Person duplicate'), [
'routeParameters' => [ 'route' => 'chill_person_duplicate_view',
'person_id' => $parameters['person']->getId(), 'routeParameters' => [
], 'person_id' => $parameters['person']->getId(),
]) ],
->setExtras([ ])
'order' => 99999, ->setExtras([
]); 'order' => 99999,
*/ ]);
}
if ( if (
'visible' === $this->showAccompanyingPeriod 'visible' === $this->showAccompanyingPeriod
&& $this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['person']) && $this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['person'])

View File

@ -27,7 +27,7 @@ final class AccompanyingPeriodNotificationHandler implements NotificationHandler
public function getTemplate(Notification $notification, array $options = []): string public function getTemplate(Notification $notification, array $options = []): string
{ {
return 'ChillPersonBundle:AccompanyingPeriod:showInNotification.html.twig'; return '@ChillPerson/AccompanyingPeriod/showInNotification.html.twig';
} }
public function getTemplateData(Notification $notification, array $options = []): array public function getTemplateData(Notification $notification, array $options = []): array

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Notification;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
final class AccompanyingPeriodWorkEvaluationDocumentNotificationHandler implements NotificationHandlerInterface
{
private AccompanyingPeriodWorkEvaluationDocumentRepository $accompanyingPeriodWorkEvaluationDocumentRepository;
public function __construct(AccompanyingPeriodWorkEvaluationDocumentRepository $accompanyingPeriodWorkEvaluationDocumentRepository)
{
$this->accompanyingPeriodWorkEvaluationDocumentRepository = $accompanyingPeriodWorkEvaluationDocumentRepository;
}
public function getTemplate(Notification $notification, array $options = []): string
{
return '@ChillPerson/AccompanyingCourseWork/showEvaluationDocumentInNotification.html.twig';
}
public function getTemplateData(Notification $notification, array $options = []): array
{
return [
'notification' => $notification,
'document' => $doc = $this->accompanyingPeriodWorkEvaluationDocumentRepository->find($notification->getRelatedEntityId()),
'evaluation' => $doc?->getAccompanyingPeriodWorkEvaluation(),
];
}
public function supports(Notification $notification, array $options = []): bool
{
return $notification->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluationDocument::class;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Notification;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
final class AccompanyingPeriodWorkNotificationHandler implements NotificationHandlerInterface
{
private AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository;
public function __construct(AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository)
{
$this->accompanyingPeriodWorkRepository = $accompanyingPeriodWorkRepository;
}
public function getTemplate(Notification $notification, array $options = []): string
{
return '@ChillPerson/AccompanyingCourseWork/showInNotification.html.twig';
}
public function getTemplateData(Notification $notification, array $options = []): array
{
return [
'notification' => $notification,
'work' => $this->accompanyingPeriodWorkRepository->find($notification->getRelatedEntityId()),
];
}
public function supports(Notification $notification, array $options = []): bool
{
return $notification->getRelatedEntityClass() === AccompanyingPeriodWork::class;
}
}

View File

@ -89,5 +89,4 @@ readonly class AccompanyingPeriodInfoRepository implements AccompanyingPeriodInf
{ {
return AccompanyingPeriodInfo::class; return AccompanyingPeriodInfo::class;
} }
} }

View File

@ -122,7 +122,8 @@
<add-evaluation <add-evaluation
v-for="e in pickedEvaluations" v-for="e in pickedEvaluations"
v-bind:key="e.key" v-bind:key="e.key"
v-bind:evaluation="e"> v-bind:evaluation="e"
v-bind:docAnchorId="this.docAnchorId">
</add-evaluation> </add-evaluation>
<!-- box to add new evaluation --> <!-- box to add new evaluation -->
@ -296,7 +297,21 @@
@go-to-generate-workflow="goToGenerateWorkflow" @go-to-generate-workflow="goToGenerateWorkflow"
></list-workflow-modal> ></list-workflow-modal>
</li> </li>
<li>
<button v-if="AmIRefferer"
class="btn btn-notify"
@click="goToGenerateNotification(false)"
></button>
<template v-else>
<button id="btnGroupNotifyButtons" type="button" class="btn btn-notify dropdown-toggle" :title="$t('notification_send')" data-bs-toggle="dropdown" aria-expanded="false">&nbsp;</button>
<ul class="dropdown-menu" aria-labelledby="btnGroupNotifyButtons">
<li><a class="dropdown-item" @click="goToGenerateNotification(true)">{{ $t('notification_notify_referrer') }}</a></li>
<li><a class="dropdown-item" @click="goToGenerateNotification(false)">{{ $t('notification_notify_any') }}</a></li>
</ul>
</template>
</li>
<li v-if="!isPosting"> <li v-if="!isPosting">
<button class="btn btn-save" @click="submit"> <button class="btn btn-save" @click="submit">
{{ $t('action.save') }} {{ $t('action.save') }}
@ -366,7 +381,10 @@ const i18n = {
no_referrers: "Aucun agent traitant", no_referrers: "Aucun agent traitant",
choose_referrers: "Choisir des agents traitants", choose_referrers: "Choisir des agents traitants",
remove_referrer: "Enlever l'agent", remove_referrer: "Enlever l'agent",
private_comment: "Commentaire privé" private_comment: "Commentaire privé",
notification_notify_referrer: "Notifier le référent",
notification_notify_any: "Notifier d'autres utilisateurs",
notification_send: "Envoyer une notification",
} }
} }
}; };
@ -389,6 +407,7 @@ export default {
i18n, i18n,
data() { data() {
return { return {
docAnchorId: null,
isExpanded: false, isExpanded: false,
editor: ClassicEditor, editor: ClassicEditor,
showAddObjective: false, showAddObjective: false,
@ -428,7 +447,14 @@ export default {
}, },
}; };
}, },
computed: { beforeMount() {
const urlParams = new URLSearchParams(window.location.search);
this.docAnchorId = urlParams.get('doc_id');
},
mounted() {
this.scrollToElement(this.docAnchorId);
},
computed: {
...mapState([ ...mapState([
'work', 'work',
'resultsForAction', 'resultsForAction',
@ -441,6 +467,7 @@ export default {
'isPosting', 'isPosting',
'errors', 'errors',
'templatesAvailablesForAction', 'templatesAvailablesForAction',
'me',
]), ]),
...mapGetters([ ...mapGetters([
'hasResultsForAction', 'hasResultsForAction',
@ -498,6 +525,10 @@ export default {
this.$store.commit('setPersonsPickedIds', v); this.$store.commit('setPersonsPickedIds', v);
} }
}, },
AmIRefferer() {
return (!(this.work.accompanyingPeriod.user && this.me
&& (this.work.accompanyingPeriod.user.id !== this.me.id)));
}
}, },
methods: { methods: {
toggleSelect() { toggleSelect() {
@ -548,6 +579,19 @@ export default {
return this.$store.dispatch('submit', callback) return this.$store.dispatch('submit', callback)
.catch(e => { console.log(e); throw e; }); .catch(e => { console.log(e); throw e; });
}, },
goToGenerateNotification(tos) {
console.log('save before leave to notification');
const callback = (data) => {
if (tos === true) {
window.location.assign(`/fr/notification/create?entityClass=Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork&entityId=${this.work.id}&tos[0]=${this.work.accompanyingPeriod.user.id}&returnPath=/fr/person/accompanying-period/${this.work.accompanyingPeriod.id}/work`);
} else {
window.location.assign(`/fr/notification/create?entityClass=Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork&entityId=${this.work.id}&returnPath=/fr/person/accompanying-period/${this.work.accompanyingPeriod.id}/work`);
}
}
return this.$store.dispatch('submit', callback)
.catch(e => {console.log(e); throw e});
},
submit() { submit() {
this.$store.dispatch('submit').catch((error) => { this.$store.dispatch('submit').catch((error) => {
if (error.name === 'ValidationException' || error.name === 'AccessException') { if (error.name === 'ValidationException' || error.name === 'AccessException') {
@ -559,7 +603,7 @@ export default {
}); });
}, },
saveFormOnTheFly(payload) { saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data); // console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
let body = { type: payload.type }; let body = { type: payload.type };
body.name = payload.data.text; body.name = payload.data.text;
@ -581,6 +625,12 @@ export default {
this.$toast.open({message: 'An error occurred'}); this.$toast.open({message: 'An error occurred'});
} }
}) })
},
scrollToElement(docAnchorId) {
const documentEl = document.getElementById(`document_${docAnchorId}`);
if (documentEl) {
documentEl.scrollIntoView({behavior: 'smooth'});
}
} }
} }
}; };

View File

@ -11,7 +11,7 @@
</div> </div>
<div> <div>
<form-evaluation ref="FormEvaluation" :key="evaluation.key" :evaluation="evaluation"></form-evaluation> <form-evaluation ref="FormEvaluation" :key="evaluation.key" :evaluation="evaluation" :docAnchorId="docAnchorId"></form-evaluation>
<ul class="record_actions"> <ul class="record_actions">
<li v-if="evaluation.workflows_availables.length > 0"> <li v-if="evaluation.workflows_availables.length > 0">
@ -85,7 +85,7 @@ export default {
Modal, Modal,
ListWorkflowModal, ListWorkflowModal,
}, },
props: ['evaluation'], props: ['evaluation', 'docAnchorId'],
i18n, i18n,
data() { data() {
return { return {

View File

@ -79,8 +79,8 @@
<h5>{{ $t('Documents') }} :</h5> <h5>{{ $t('Documents') }} :</h5>
<div class="flex-table"> <div class="flex-table">
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.id"> <div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.id" :class="[parseInt(this.docAnchorId) === d.id ? 'bg-blink' : 'nothing']">
<div class="item-row"> <div :id="`document_${d.id}`" class="item-row">
<div class="input-group input-group-lg mb-3 row"> <div class="input-group input-group-lg mb-3 row">
<label class="col-sm-3 col-form-label">Titre du document:</label> <label class="col-sm-3 col-form-label">Titre du document:</label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -92,7 +92,7 @@
:data-key="i" :data-key="i"
@input="onInputDocumentTitle"/> @input="onInputDocumentTitle"/>
</div> </div>
</div> </div>
</div> </div>
<div class="item-row"> <div class="item-row">
<div class="item-col item-meta"> <div class="item-col item-meta">
@ -102,27 +102,32 @@
</div> </div>
<div class="item-row"> <div class="item-row">
<div class="item-col"> <div class="item-col">
<ul class="record_actions" > <ul class="record_actions">
<li v-if="d.workflows_availables.length > 0"> <li v-if="d.workflows_availables.length > 0">
<list-workflow-modal <list-workflow-modal
:workflows="d.workflows" :workflows="d.workflows"
:allowCreate="true" :allowCreate="true"
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument" relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument"
:relatedEntityId="d.id" :relatedEntityId="d.id"
:workflowsAvailables="d.workflows_availables" :workflowsAvailables="d.workflows_availables"
:preventDefaultMoveToGenerate="true" :preventDefaultMoveToGenerate="true"
:goToGenerateWorkflowPayload="{doc: d}" :goToGenerateWorkflowPayload="{doc: d}"
@go-to-generate-workflow="goToGenerateWorkflowEvaluationDocument" @go-to-generate-workflow="goToGenerateWorkflowEvaluationDocument"
></list-workflow-modal> ></list-workflow-modal>
</li> </li>
<li> <li>
<add-async-upload <button
:buttonTitle="$t('replace')" v-if="AmIRefferer"
:options="asyncUploadOptions" class="btn btn-notify"
:btnClasses="{'btn': true, 'btn-edit': true}" @click="goToGenerateDocumentNotification(d, false)">
@addDocument="(arg) => replaceDocument(d, arg)" </button>
> <template v-else>
</add-async-upload> <button id="btnGroupNotifyButtons" type="button" class="btn btn-notify dropdown-toggle" :title="$t('notification_send')" data-bs-toggle="dropdown" aria-expanded="false">&nbsp;</button>
<ul class="dropdown-menu" aria-labelledby="btnGroupNotifyButtons">
<li><a class="dropdown-item" @click="goToGenerateDocumentNotification(d, true)">{{ $t('notification_notify_referrer') }}</a></li>
<li><a class="dropdown-item" @click="goToGenerateDocumentNotification(d, false)">{{ $t('notification_notify_any') }}</a></li>
</ul>
</template>
</li> </li>
<li> <li>
<document-action-buttons-group <document-action-buttons-group
@ -133,6 +138,15 @@
@on-stored-object-status-change="onStatusDocumentChanged" @on-stored-object-status-change="onStatusDocumentChanged"
></document-action-buttons-group> ></document-action-buttons-group>
</li> </li>
<li>
<add-async-upload
:buttonTitle="$t('replace')"
:options="asyncUploadOptions"
:btnClasses="{'btn': true, 'btn-edit': true}"
@addDocument="(arg) => replaceDocument(d, arg)"
>
</add-async-upload>
</li>
<li v-if="d.workflows.length === 0"> <li v-if="d.workflows.length === 0">
<a class="btn btn-delete" @click="removeDocument(d)"> <a class="btn btn-delete" @click="removeDocument(d)">
</a> </a>
@ -214,14 +228,17 @@ const i18n = {
template_title: "Nom du template", template_title: "Nom du template",
browse: "Ajouter un document", browse: "Ajouter un document",
replace: "Remplacer", replace: "Remplacer",
download: "Télécharger le fichier existant" download: "Télécharger le fichier existant",
notification_notify_referrer: "Notifier le référent",
notification_notify_any: "Notifier d'autres utilisateurs",
notification_send: "Envoyer une notification",
} }
} }
}; };
export default { export default {
name: "FormEvaluation", name: "FormEvaluation",
props: ['evaluation'], props: ['evaluation', 'docAnchorId'],
components: { components: {
ckeditor: CKEditor.component, ckeditor: CKEditor.component,
PickTemplate, PickTemplate,
@ -260,8 +277,14 @@ export default {
}, },
computed: { computed: {
...mapState([ ...mapState([
'isPosting' 'isPosting',
'work',
'me',
]), ]),
AmIRefferer() {
return (!(this.$store.state.work.accompanyingPeriod.user && this.$store.state.me
&& (this.$store.state.work.accompanyingPeriod.user.id !== this.$store.state.me.id)));
},
getTemplatesAvailables() { getTemplatesAvailables() {
return this.$store.getters.getTemplatesAvailablesForEvaluation(this.evaluation.evaluation); return this.$store.getters.getTemplatesAvailablesForEvaluation(this.evaluation.evaluation);
}, },
@ -390,6 +413,18 @@ export default {
return this.$store.dispatch('submit', callback) return this.$store.dispatch('submit', callback)
.catch(e => { console.log(e); throw e; }); .catch(e => { console.log(e); throw e; });
}, },
goToGenerateDocumentNotification(document, tos){
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key);
if (tos === true) {
window.location.assign(`/fr/notification/create?entityClass=Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument&entityId=${document.id}&tos[0]=${this.$store.state.work.accompanyingPeriod.user.id}&returnPath=/fr/person/accompanying-period/work/${evaluation.id}/edit`)
} else {
window.location.assign(`/fr/notification/create?entityClass=Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument&entityId=${document.id}&returnPath=/fr/person/accompanying-period/work/${evaluation.id}/edit`)
}
};
return this.$store.dispatch('submit', callback)
.catch(e => {console.log(e); throw e});
}
}, },
} }
</script> </script>
@ -402,4 +437,19 @@ export default {
ul.document-upload { ul.document-upload {
justify-content: flex-start; justify-content: flex-start;
} }
.bg-blink{
color: #050000;
padding: 10px;
display: inline-block;
border-radius: 5px;
animation: blinkingBackground 2.2s infinite;
animation-iteration-count: 2;
}
@keyframes blinkingBackground{
0% { background-color: #ed776d;}
50% { background-color: #ffffff;}
100% { background-color: #ed776d;}
}
</style> </style>

View File

@ -35,6 +35,7 @@ const store = createStore({
referrers: window.accompanyingCourseWork.referrers, referrers: window.accompanyingCourseWork.referrers,
isPosting: false, isPosting: false,
errors: [], errors: [],
me: null
}, },
getters: { getters: {
socialAction(state) { socialAction(state) {
@ -130,6 +131,9 @@ const store = createStore({
} }
}, },
mutations: { mutations: {
setWhoAmiI(state, me) {
state.me = me;
},
setEvaluationsPicked(state, evaluations) { setEvaluationsPicked(state, evaluations) {
state.evaluationsPicked = evaluations.map((e, index) => { state.evaluationsPicked = evaluations.map((e, index) => {
var k = Object.assign(e, { var k = Object.assign(e, {
@ -385,6 +389,19 @@ const store = createStore({
}, },
}, },
actions: { actions: {
getWhoAmI({ commit }) {
let url = `/api/1.0/main/whoami.json`;
window.fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw { m: 'Error while retriving results for goal', s: response.status, b: response.body };
})
.then(data => {
commit('setWhoAmiI', data);
});
},
updateThirdParty({ commit }, payload) { updateThirdParty({ commit }, payload) {
commit('updateThirdParty', payload); commit('updateThirdParty', payload);
}, },
@ -514,6 +531,7 @@ store.commit('setEvaluationsPicked', window.accompanyingCourseWork.accompanyingP
store.dispatch('getReachablesResultsForAction'); store.dispatch('getReachablesResultsForAction');
store.dispatch('getReachablesGoalsForAction'); store.dispatch('getReachablesGoalsForAction');
store.dispatch('getReachablesEvaluationsForAction'); store.dispatch('getReachablesEvaluationsForAction');
store.dispatch('getWhoAmI');
store.state.evaluationsPicked.forEach(evaluation => { store.state.evaluationsPicked.forEach(evaluation => {
store.dispatch('fetchTemplatesAvailablesForEvaluation', evaluation.evaluation) store.dispatch('fetchTemplatesAvailablesForEvaluation', evaluation.evaluation)

View File

@ -207,7 +207,7 @@
</template> </template>
<script> <script>
import {dateToISO} from 'ChillMainAssets/chill/js/date'; import {dateToISO, ISOToDate} from 'ChillMainAssets/chill/js/date';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue'; import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue'; import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue';
import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue'; import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue';
@ -262,7 +262,7 @@ export default {
}, },
birthdate: function () { birthdate: function () {
if (this.person.birthdate !== null || this.person.birthdate === "undefined") { if (this.person.birthdate !== null || this.person.birthdate === "undefined") {
return new Date(this.person.birthdate.datetime); return ISOToDate(this.person.birthdate.datetime);
} else { } else {
return ""; return "";
} }

View File

@ -5,7 +5,8 @@
# - displayAction: [true|false] default: false # - displayAction: [true|false] default: false
# - displayFontSmall: [true|false] default: false # - displayFontSmall: [true|false] default: false
#} #}
<div class="item-bloc{% if displayContent is defined %} {{ displayContent }}{% endif %}{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}"> <div
class="item-bloc{% if displayContent is defined %} {{ displayContent }}{% endif %}{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<div class="item-row"> <div class="item-row">
<h2 class="badge-title"> <h2 class="badge-title">
@ -111,9 +112,11 @@
</div> </div>
{% if displayContent is not defined or displayContent == 'short' %} {% if displayContent is not defined or displayContent == 'short' %}
<div class="item-row column{% if displayFontSmall is defined and displayFontSmall == true %} smallfont{% endif %}"> <div
{% include 'ChillPersonBundle:AccompanyingCourseWork:_objectifs_results_evaluations.html.twig' with { class="item-row column{% if displayFontSmall is defined and displayFontSmall == true %} smallfont{% endif %}">
'displayContent': displayContent {% include '@ChillPerson/AccompanyingCourseWork/_objectifs_results_evaluations.html.twig' with {
'displayContent': displayContent,
'onlyone': false
} %} } %}
</div> </div>
{% endif %} {% endif %}
@ -121,35 +124,56 @@
{% if displayContent is not defined or displayContent == 'short' %} {% if displayContent is not defined or displayContent == 'short' %}
<div class="item-row separator"> <div class="item-row separator">
{% import '@ChillPerson/AccompanyingCourseWork/_macros.html.twig' as macro %} {% import '@ChillPerson/AccompanyingCourseWork/_macros.html.twig' as macro %}
<div class="item-col item-meta"> <div class="item-col item-meta">
{{ macro.metadata(w) }} {{ macro.metadata(w) }}
</div> </div>
{% if displayAction is defined and displayAction == true %} {% if displayAction is defined and displayAction == true %}
<ul class="item-col record_actions"> <ul class="item-col record_actions">
<li>{{ macro.workflowButton(w) }}</li> <li>{{ macro.workflowButton(w) }}</li>
<li> {% if displayNotification is defined and displayNotification == true %}
<a class="btn btn-show" title="{{ 'Show'|trans }}" <li>
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_show', {'id': w.id }) }}" <div class="d-grid gap-2 {% if accompanyingCourse.hasUser and accompanyingCourse.user is not same as(app.user) %}btn-group{% endif %}" {% if accompanyingCourse.hasUser and accompanyingCourse.user is not same as(app.user) %}role="group"{% endif %}>
></a> {% if accompanyingCourse.hasUser and accompanyingCourse.user is not same as(app.user) %}
</li> <button id="btnGroupNotifyButtons" type="button" class="btn btn-notify dropdown-toggle" title="{{ 'notification.Notify'|trans }}" data-bs-toggle="dropdown" aria-expanded="false">
{% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w) %} </button>
<ul class="dropdown-menu" aria-labelledby="btnGroupNotifyButtons">
<li>
<a class="dropdown-item" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', 'entityId': w.id, 'tos': [accompanyingCourse.user.id]}) }}">{{ 'notification.Notify referrer'|trans }}</a>
</li>
<li>
<a class="dropdown-item" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', 'entityId': w.id}) }}">{{ 'notification.Notify any'|trans }}</a>
</li>
</ul>
{% else %}
<a class="btn btn-notify" title="{{ 'notification.Notify'|trans }}" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', 'entityId': w.id}) }}">
</a>
{% endif %}
</div>
</li>
{% endif %}
<li> <li>
<a class="btn btn-edit" title="{{ 'Edit'|trans }}" <a class="btn btn-show" title="{{ 'Show'|trans }}"
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}" href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_show', {'id': w.id }) }}"
></a> ></a>
</li> </li>
{% endif %} {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w) %}
{% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE', w) %} <li>
<li> <a class="btn btn-edit" title="{{ 'Edit'|trans }}"
<a class="btn btn-delete" title="{{ 'Delete'|trans }}" href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}"
href="{{ path('chill_person_accompanying_period_work_delete', { 'id': w.id } ) }}" ></a>
></a> </li>
</li> {% endif %}
{% endif %} {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE', w) %}
</ul> <li>
<a class="btn btn-delete" title="{{ 'Delete'|trans }}"
href="{{ path('chill_person_accompanying_period_work_delete', { 'id': w.id } ) }}"
></a>
</li>
{% endif %}
</ul>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -157,47 +181,51 @@
</div> </div>
{# {#
# This is for 'long' version of content # This is for 'long' version of content
# Note: this include is wrapped in a flex-table container. # Note: this include is wrapped in a flex-table container.
# We start by closing the flex-table so we can add more. # We start by closing the flex-table so we can add more.
# At the end we leave the last flex-table open, as it will be closed in the container. # At the end we leave the last flex-table open, as it will be closed in the container.
#} #}
{% if displayContent is defined and displayContent == 'long' %} {% if displayContent is defined and displayContent == 'long' %}
</div>
{% if w.results|length > 0 or w.goals|length > 0 or w.accompanyingPeriodWorkEvaluations|length > 0 %}
<h2 class="chill-blue">{{ 'Dispositifs' }}</h2>
<div class="flex-table">{# new flex-table wrapper #}
<div
class="item-bloc colored{% if displayContent is defined %} {{ displayContent }}{% endif %}{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
{% include 'ChillPersonBundle:AccompanyingCourseWork:_objectifs_results_evaluations.html.twig' with {
'displayContent': displayContent,
'onlyone' : false
} %}
</div>
</div> </div>
{% endif %}
{% if w.results|length > 0 or w.goals|length > 0 or w.accompanyingPeriodWorkEvaluations|length > 0 %} <h2 class="chill-blue">{{ 'Comments'|trans }}</h2>
<h2 class="chill-blue">{{ 'Dispositifs' }}</h2>
<div class="flex-table">{# new flex-table wrapper #} <div class="flex-table">
<div class="item-bloc colored{% if displayContent is defined %} {{ displayContent }}{% endif %}{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}"> <div
{% include 'ChillPersonBundle:AccompanyingCourseWork:_objectifs_results_evaluations.html.twig' with { class="item-bloc no-altern{% if displayContent is defined %} {{ displayContent }}{% endif %}{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
'displayContent': displayContent <h3 class="chill-beige">Public</h3>
} %} {% if w.note is not empty %}
</div> <blockquote class="chill-user-quote">
{{ w.note|chill_entity_render_box({'metadata': true }) }}
</blockquote>
{% else %}
<span class="chill-no-data-statement">{{ 'No comment associated'|trans }}</span>
{% endif %}
</div>
{% if w.privateComment.hasCommentForUser(app.user) %}
<div
class="item-bloc no-altern{% if displayContent is defined %} {{ displayContent }}{% endif %}{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<h3 class="chill-beige">Privé</h3>
<blockquote class="chill-user-quote private-quote">
{{ w.privateComment.commentForUser(app.user)|chill_markdown_to_html }}
</blockquote>
</div> </div>
{% endif %} {% endif %}
<h2 class="chill-blue">{{ 'Comments'|trans }}</h2>
<div class="flex-table">
<div class="item-bloc no-altern{% if displayContent is defined %} {{ displayContent }}{% endif %}{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<h3 class="chill-beige">Public</h3>
{% if w.note is not empty %}
<blockquote class="chill-user-quote">
{{ w.note|chill_entity_render_box({'metadata': true }) }}
</blockquote>
{% else %}
<span class="chill-no-data-statement">{{ 'No comment associated'|trans }}</span>
{% endif %}
</div>
{% if w.privateComment.hasCommentForUser(app.user) %}
<div class="item-bloc no-altern{% if displayContent is defined %} {{ displayContent }}{% endif %}{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<h3 class="chill-beige">Privé</h3>
<blockquote class="chill-user-quote private-quote">
{{ w.privateComment.commentForUser(app.user)|chill_markdown_to_html }}
</blockquote>
</div>
{% endif %}
{# Here flex-table stay open ! read above #} {# Here flex-table stay open ! read above #}
{% endif %} {% endif %}

View File

@ -1,7 +1,17 @@
{% macro metadata(w) %} {% macro metadata(w, include_notif_counter = true) %}
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id) %} {% if include_notif_counter == true %}
{% if notif_counter.total > 0 %} {% set more = [] %}
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id) }} {% for e in w.accompanyingPeriodWorkEvaluations %}
{% for d in e.documents %}
{% set more = more|merge([{'relatedEntityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', 'relatedEntityId': d.id}]) %}
{% endfor %}
{% endfor %}
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id, more) %}
{% if notif_counter.total > 0 %}
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id, more) }}
{% endif %}
{% endif %} {% endif %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as macro %} {% import '@ChillPerson/Macro/updatedBy.html.twig' as macro %}
{{ macro.updatedBy(w) }} {{ macro.updatedBy(w) }}
@ -20,4 +30,4 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{{ chill_entity_workflow_list('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id, [], suppEvaluations) }} {{ chill_entity_workflow_list('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id, [], suppEvaluations) }}
{% endmacro %} {% endmacro %}

View File

@ -3,6 +3,7 @@
# - displayContent: [short|long] default: short # - displayContent: [short|long] default: short
#} #}
{% if w.results|length > 0 %} {% if w.results|length > 0 %}
<table class="obj-res-eval"> <table class="obj-res-eval">
<thead> <thead>
<th class="obj"><h4 class="title_label">{{ 'accompanying_course_work.goal'|trans }}</h4></th> <th class="obj"><h4 class="title_label">{{ 'accompanying_course_work.goal'|trans }}</h4></th>
@ -64,9 +65,11 @@
</th> </th>
</thead> </thead>
<tbody> <tbody>
{% for e in w.accompanyingPeriodWorkEvaluations %} {% if onlyone|default(false) %}
<tr> {% for e in w.accompanyingPeriodWorkEvaluations %}
<td class="eval"> {% if evalId is defined and evalId == e.id %}
<tr>
<td class="eval">
<ul class="eval_title"> <ul class="eval_title">
<li> <li>
{{ e.evaluation.title|localize_translatable_string }} {{ e.evaluation.title|localize_translatable_string }}
@ -78,13 +81,15 @@
{% if e.endDate %} {% if e.endDate %}
<li> <li>
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span> <span
class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ e.endDate|format_date('short') }}</b> <b>{{ e.endDate|format_date('short') }}</b>
</li> </li>
{% else %} {% else %}
{% if displayContent is defined and displayContent == 'long' %} {% if displayContent is defined and displayContent == 'long' %}
<li> <li>
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span> <span
class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span> <span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
</li> </li>
{% endif %} {% endif %}
@ -92,13 +97,15 @@
{% if e.maxDate %} {% if e.maxDate %}
<li> <li>
<span class="item-key">{{ 'accompanying_course_work.max_date'|trans ~ ' : ' }}</span> <span
class="item-key">{{ 'accompanying_course_work.max_date'|trans ~ ' : ' }}</span>
<b>{{ e.maxDate|format_date('short') }}</b> <b>{{ e.maxDate|format_date('short') }}</b>
</li> </li>
{% else %} {% else %}
{% if displayContent is defined and displayContent == 'long' %} {% if displayContent is defined and displayContent == 'long' %}
<li> <li>
<span class="item-key">{{ 'accompanying_course_work.max_date'|trans ~ ' : ' }}</span> <span
class="item-key">{{ 'accompanying_course_work.max_date'|trans ~ ' : ' }}</span>
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span> <span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
</li> </li>
{% endif %} {% endif %}
@ -107,13 +114,15 @@
{% if e.warningInterval and e.warningInterval.d > 0 %} {% if e.warningInterval and e.warningInterval.d > 0 %}
<li> <li>
{% set days = (e.warningInterval.d + e.warningInterval.m * 30) %} {% set days = (e.warningInterval.d + e.warningInterval.m * 30) %}
<span class="item-key">{{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }}</span> <span
class="item-key">{{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }}</span>
{{ 'accompanying_course_work.%days% days before max_date'|trans({'%days%': days }) }} {{ 'accompanying_course_work.%days% days before max_date'|trans({'%days%': days }) }}
</li> </li>
{% else %} {% else %}
{% if displayContent is defined and displayContent == 'long' %} {% if displayContent is defined and displayContent == 'long' %}
<li> <li>
<span class="item-key">{{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }}</span> <span
class="item-key">{{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }}</span>
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span> <span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
</li> </li>
{% endif %} {% endif %}
@ -122,45 +131,166 @@
{% if e.timeSpent is not null and e.timeSpent > 0 %} {% if e.timeSpent is not null and e.timeSpent > 0 %}
<li> <li>
{% set minutes = (e.timeSpent / 60) %} {% set minutes = (e.timeSpent / 60) %}
<span class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span> {{ 'duration.minute'|trans({ '{m}' : minutes }) }} <span
class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span> {{ 'duration.minute'|trans({ '{m}' : minutes }) }}
</li> </li>
{% elseif displayContent is defined and displayContent == 'long' %} {% elseif displayContent is defined and displayContent == 'long' %}
<li> <li>
<span class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span> <span
class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span>
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span> <span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
</ul> </ul>
{% if displayContent is defined and displayContent == 'long' %} {% endif %}
{% if e.comment is not empty %}
<blockquote class="chill-user-quote">{{ e.comment|chill_entity_render_box }}</blockquote> {% endfor %}
{% if recordAction is defined %}
{{ recordAction }}
{% endif %}
{% else %}
{% for e in w.accompanyingPeriodWorkEvaluations %}
<tr>
<td class="eval">
<ul class="eval_title">
<li>
{{ e.evaluation.title|localize_translatable_string }}
<ul class="columns">
<li>
<span
class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ e.startDate|format_date('short') }}</b>
</li>
{% if e.endDate %}
<li>
<span
class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ e.endDate|format_date('short') }}</b>
</li>
{% else %}
{% if displayContent is defined and displayContent == 'long' %}
<li>
<span
class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
</li>
{% endif %}
{% endif %}
{% if e.maxDate %}
<li>
<span
class="item-key">{{ 'accompanying_course_work.max_date'|trans ~ ' : ' }}</span>
<b>{{ e.maxDate|format_date('short') }}</b>
</li>
{% else %}
{% if displayContent is defined and displayContent == 'long' %}
<li>
<span
class="item-key">{{ 'accompanying_course_work.max_date'|trans ~ ' : ' }}</span>
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
</li>
{% endif %}
{% endif %}
{% if e.warningInterval and e.warningInterval.d > 0 %}
<li>
{% set days = (e.warningInterval.d + e.warningInterval.m * 30) %}
<span
class="item-key">{{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }}</span>
{{ 'accompanying_course_work.%days% days before max_date'|trans({'%days%': days }) }}
</li>
{% else %}
{% if displayContent is defined and displayContent == 'long' %}
<li>
<span
class="item-key">{{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }}</span>
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
</li>
{% endif %}
{% endif %}
{% if e.timeSpent is not null and e.timeSpent > 0 %}
<li>
{% set minutes = (e.timeSpent / 60) %}
<span
class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span> {{ 'duration.minute'|trans({ '{m}' : minutes }) }}
</li>
{% elseif displayContent is defined and displayContent == 'long' %}
<li>
<span
class="item-key">{{ 'accompanying_course_work.timeSpent'|trans ~ ' : ' }}</span>
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
</li>
{% endif %}
</ul>
</li>
</ul>
{% if recordAction is defined %}
{{ recordAction }}
{% endif %} {% endif %}
{% import "@ChillDocStore/Macro/macro.html.twig" as m %} {% if displayContent is defined and displayContent == 'long' %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% if e.comment is not empty %}
<blockquote
class="chill-user-quote">{{ e.comment|chill_entity_render_box }}</blockquote>
{% endif %}
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% if e.documents|length > 0 %}
<table class="table table-hover align-middle mt-4 mx-auto">
{% for d in e.documents %}
<tr>
<td class="border-0">{{ d.title }}</td>
<td class="border-0">{{ mm.mimeIcon(d.storedObject.type) }}</td>
<td class="border-0 text-end">
{% if accompanyingCourse.hasUser and accompanyingCourse.user is not same as(app.user) %}
<button id="btnGroupNotifyButtons" type="button" class="btn btn-sm btn-notify dropdown-toggle" title="{{ 'notification.Notify'|trans }}"
data-bs-toggle="dropdown" aria-expanded="false">
</button>
<ul class="dropdown-menu" aria-labelledby="btnGroupNotifyButtons">
<li>
<a class="dropdown-item"
href="{{ chill_path_add_return_path('chill_main_notification_create', {
'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument',
'entityId': d.id, 'tos': [accompanyingCourse.user.id]}) }}">{{ 'notification.Notify referrer'|trans }}</a>
</li>
<li>
<a class="dropdown-item"
href="{{ chill_path_add_return_path('chill_main_notification_create', {
'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument',
'entityId': d.id}) }}">{{ 'notification.Notify any'|trans }}</a>
</li>
</ul>
{% else %}
<a class="btn btn-notify btn-sm"
title="{{ 'notification.Notify'|trans }}"
href="{{ chill_path_add_return_path('chill_main_notification_create', {
'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument',
'entityId': d.id }) }}"></a>
{% endif %}
{{ d.storedObject|chill_document_button_group(d.title, is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w), {'small': true}) }}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<span class="chill-no-data-statement">{{ 'No document found'|trans }}</span>
{% endif %}
{% if e.documents|length > 0 %}
<table class="table mt-4 mx-auto">
{% for d in e.documents %}
<tr class="border-0">
<td class="border-0">{{ d.title }}</td>
<td class="border-0">{{ mm.mimeIcon(d.storedObject.type) }}</td>
<td class="border-0 text-end">{{ d.storedObject|chill_document_button_group(d.title, is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w), {'small': true}) }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<span class="chill-no-data-statement">{{ 'No document found'|trans }}</span>
{% endif %} {% endif %}
</td>
</tr>
{% endif %} {% endfor %}
</td> {% endif %}
</tr>
{% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}

View File

@ -26,7 +26,8 @@
'displayAction': true, 'displayAction': true,
'displayContent': 'short', 'displayContent': 'short',
'displayFontSmall': true, 'displayFontSmall': true,
'itemBlocClass': '' 'itemBlocClass': '',
'displayNotification': true
} %} } %}
{% endfor %} {% endfor %}
</div> </div>

View File

@ -27,7 +27,20 @@
'displayContent': 'long', 'displayContent': 'long',
'itemBlocClass': 'uniq extended', 'itemBlocClass': 'uniq extended',
} %} } %}
<div class="p-3 mt-3">{{ macro.metadata(work) }}</div> <div class="p-3 mt-3">{{ macro.metadata(work, false) }}</div>
</div>
<div class="notification notification-list">
{% set more = [] %}
{% for e in work.accompanyingPeriodWorkEvaluations %}
{% for d in e.documents %}
{% set more = more|merge([{'relatedEntityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', 'relatedEntityId': d.id}]) %}
{% endfor %}
{% endfor %}
{% set notifications = chill_list_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', work.id, more) %}
{% if notifications is not empty %}
{{ notifications|raw }}
{% endif %}
</div> </div>
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
@ -36,6 +49,25 @@
class="btn btn-cancel">{{ 'Back to the list'|trans }}</a> class="btn btn-cancel">{{ 'Back to the list'|trans }}</a>
</li> </li>
<li>{{ macro.workflowButton(work) }}</li> <li>{{ macro.workflowButton(work) }}</li>
<li>
<div class="d-grid gap-2 {% if accompanyingCourse.hasUser and accompanyingCourse.user is not same as(app.user) %}btn-group{% endif %}" {% if accompanyingCourse.hasUser and accompanyingCourse.user is not same as(app.user) %}role="group"{% endif %}>
{% if accompanyingCourse.hasUser and accompanyingCourse.user is not same as(app.user) %}
<button id="btnGroupNotifyButtons" type="button" class="btn btn-notify dropdown-toggle" title="{{ 'notification.Notify'|trans }}" data-bs-toggle="dropdown" aria-expanded="false">
</button>
<ul class="dropdown-menu" aria-labelledby="btnGroupNotifyButtons">
<li>
<a class="dropdown-item" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', 'entityId': work.id, 'tos': [accompanyingCourse.user.id]}) }}">{{ 'notification.Notify referrer'|trans }}</a>
</li>
<li>
<a class="dropdown-item" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', 'entityId': work.id}) }}">{{ 'notification.Notify any'|trans }}</a>
</li>
</ul>
{% else %}
<a class="btn btn-notify" title="{{ 'notification.Notify'|trans }}" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', 'entityId': work.id}) }}">
</a>
{% endif %}
</div>
</li>
{% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', work) %} {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', work) %}
<li> <li>
<a class="btn btn-edit" <a class="btn btn-edit"
@ -47,7 +79,8 @@
<li> <li>
<a class="btn btn-delete" <a class="btn btn-delete"
href="{{ path('chill_person_accompanying_period_work_delete', { 'id': work.id } ) }}" href="{{ path('chill_person_accompanying_period_work_delete', { 'id': work.id } ) }}"
>{{ 'Delete'|trans }}</a> title = "{{ 'Delete'|trans }}"
></a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>

View File

@ -0,0 +1,134 @@
{% if document is not null %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
<div class="flex-table accompanying-course-work">
{% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW', document) %}
{% set doc = document %}
<div class="item-bloc evaluation-item bg-chill-llight-gray">
<div class="item-row mb-2">
<h1>{{ "Document"|trans }}: {{ doc.title }}</h1>
</div>
<div class="item-row mb-2">
<h2 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
{{ evaluation.accompanyingPeriodWork.socialAction|chill_entity_render_string }}
<ul class="small_in_title columns mt-1">
<li>
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.accompanyingPeriodWork.startDate|format_date('short') }}</b>
</li>
{% if evaluation.accompanyingPeriodWork.endDate %}
<li>
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.accompanyingPeriodWork.endDate|format_date('short') }}</b>
</li>
{% endif %}
</ul>
</span>
</h2>
</div>
<div class="item-row mb-2">
<div class="item-col" style="width: 17%;">
<h4 class="title_label">
{{ 'Participants'|trans }}
</h4>
</div>
<div class="item-col list">
{% for p in evaluation.accompanyingPeriodWork.persons %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: p.id },
action: 'show',
displayBadge: true,
buttonText: p|chill_entity_render_string,
isDead: p.deathdate is not null
} %}
{% endfor %}
</div>
</div>
<div class="item-row column">
<table class="obj-res-eval my-3">
<thead>
<tr>
<th class="eval">
<h4 class="title_label">
{{ 'Évaluation'|trans }}
</h4>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="eval">
<ul class="eval_title">
<li class="my-2">
{{ evaluation.evaluation.title|localize_translatable_string }}
<ul class="columns pt-2">
<li>
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.startDate|format_date('short') }}</b>
</li>
{% if evaluation.endDate %}
<li>
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.endDate|format_date('short') }}</b>
</li>
{% endif %}
{% if evaluation.maxDate %}
<li>
<span class="item-key">{{ 'accompanying_course_work.max_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.maxDate|format_date('short') }}</b>
</li>
{% endif %}
{% if evaluation.warningInterval and evaluation.warningInterval.d > 0 %}
<li>
{% set days = (evaluation.warningInterval.d + evaluation.warningInterval.m * 30) %}
<span class="item-key">{{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }}</span>
{{ 'accompanying_course_work.%days% days before max_date'|trans({'%days%': days }) }}
</li>
{% endif %}
<li>
{% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b>
{% endif %}
{% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span>
<b>{{ evaluation.createdAt|format_date('short') }}</b>
{% endif %}
</li>
</ul>
{% if evaluation.comment %}
<blockquote class="chill-user-quote" style="margin-left: 0;">
{{ evaluation.comment }}
</blockquote>
{% endif %}
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
<div class="item-row">
<ul class="record_actions">
<li>
<a class="btn btn-show"
href="{{ path('chill_person_accompanying_period_work_show', { 'id': document.accompanyingPeriodWorkEvaluation.accompanyingPeriodWork.id }) }}"
title="{{ 'See the document'|trans }}"></a>
</li>
</ul>
</div>
</div>
{% else %}
<div class="alert alert-warning border-warning border-1">
{{ 'This is the minimal period details'|trans ~ ': ' ~ document.id }}<br>
{{ 'You are getting a notification for a period you are not allowed to see'|trans }}
</div>
{% endif %}
</div>
{% else %}
<div class="alert alert-warning border-warning border-1">
{{ 'You are getting a notification for a period which does not exists any more'|trans }}
</div>
{% endif %}

View File

@ -0,0 +1,30 @@
{% macro recordAction(work) %}
<li>
<a class="btn btn-show" title="{{ 'Show'|trans }}"
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_show', {'id': work.id }) }}"
></a>
</li>
{% endmacro %}
{% if work is not null %}
<div class="flex-table accompanying-course-work">
{% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE', work) %}
{% include "@ChillPerson/AccompanyingCourseWork/_item.html.twig" with {
'itemBlocClass': 'bg-chill-llight-gray',
'displayAction': true,
'displayContent': 'short',
'displayFontSmall': true,
'displayNotification:':true,
'w': work
} %}
{% else %}
<div class="alert alert-warning border-warning border-1">
{{ 'This is the minimal period details'|trans ~ ': ' ~ work.id }}<br>
{{ 'You are getting a notification for a period you are not allowed to see'|trans }}
</div>
{% endif %}
</div>
{% else %}
<div class="alert alert-warning border-warning border-1">
{{ 'You are getting a notification for a period which does not exists any more'|trans }}
</div>
{% endif %}

View File

@ -16,11 +16,13 @@
</div> </div>
<div class="wh-col"> <div class="wh-col">
{% if period.step == 'DRAFT' %} {% if period.step == 'DRAFT' %}
<span class="badge bg-secondary">{{- 'Draft'|trans|upper -}}</span> <span class="badge bg-secondary" style="font-size: 85%;" title="{{ 'course.draft'|trans|e('html_attr') }}">{{ 'course.draft'|trans }}</span>
{% elseif period.step == 'CONFIRMED' %} {% elseif period.step == 'CLOSED' %}
<span class="badge bg-primary">{{- 'Confirmed'|trans|upper -}}</span> <span class="badge bg-danger" style="font-size: 85%;" title="{{ 'course.closed'|trans|e('html_attr') }}">{{ 'course.closed'|trans }}</span>
{% else %} {% elseif period.step == 'CONFIRMED_INACTIVE_SHORT' %}
<span class="badge bg-danger">{{- 'Closed'|trans|upper -}}</span> <span class="badge bg-chill-yellow text-primary" style="font-size: 85%;" title="{{ 'course.inactive_short'|trans|e('html_attr') }}">{{ 'course.inactive_short'|trans }}</span>
{% elseif period.step == 'CONFIRMED_INACTIVE_LONG' %}
<span class="badge bg-danger" style="font-size: 85%;" title="{{ 'course.inactive_long'|trans|e('html_attr') }}">{{ 'course.inactive_long'|trans }}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -8,7 +8,7 @@
{% if period is not null %} {% if period is not null %}
<div class="flex-table"> <div class="flex-table">
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', period) %} {% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', period) %}
{% include 'ChillPersonBundle:AccompanyingPeriod:_list_item.html.twig' with { {% include "@ChillPerson/AccompanyingPeriod/_list_item.html.twig" with {
'recordAction': _self.recordAction(notification.relatedEntityId), 'recordAction': _self.recordAction(notification.relatedEntityId),
'itemBlocClass': 'bg-chill-llight-gray' 'itemBlocClass': 'bg-chill-llight-gray'
} %} } %}

View File

@ -17,6 +17,15 @@
<div class="col-md-10"> <div class="col-md-10">
<h1>{{ 'My accompanying periods'|trans }}</h1> <h1>{{ 'My accompanying periods'|trans }}</h1>
<ul class="nav nav-pills justify-content-center">
<li class="nav-item">
<a class="nav-link {% if active == true %}active{% endif %}" aria-current="page" href="{{ chill_path_forward_return_path('chill_person_accompanying_period_user', {'active': true}) }}">{{ ['Confirmed'|trans, 'course.inactive_short'|trans, 'course.inactive_long'|trans]|join(', ') }}</a>
</li>
<li class="nav-item ">
<a class="nav-link {% if active == false %}active{% endif %}" href="{{ chill_path_forward_return_path('chill_person_accompanying_period_user', {'active': false}) }}">{{ 'course.closed'|trans }}</a>
</li>
</ul>
<p>{{ 'Number of periods'|trans }}: <span class="badge rounded-pill bg-primary">{{ pagination.totalItems }}</span></p> <p>{{ 'Number of periods'|trans }}: <span class="badge rounded-pill bg-primary">{{ pagination.totalItems }}</span></p>
<div class="flex-table accompanyingcourse-list"> <div class="flex-table accompanyingcourse-list">

View File

@ -40,13 +40,14 @@
{{ 'Household summary'|trans }} {{ 'Household summary'|trans }}
</a> </a>
</li> </li>
{# TODO: add ACL to check if user is allowed to edit household? #} {% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<li> <li>
<a class="btn btn-create" <a class="btn btn-create"
href="{{ path ('chill_household_accompanying_course_new', {'household_id' : household.id } ) }}" role="button"> href="{{ path ('chill_household_accompanying_course_new', {'household_id' : household.id } ) }}" role="button">
{{ 'Create an accompanying period'|trans }} {{ 'Create an accompanying period'|trans }}
</a> </a>
</li> </li>
{% endif %}
</ul> </ul>
</div> </div>

View File

@ -39,7 +39,7 @@
<li><b>{{ person.counters.nb_activity }}</b> {{ (person.counters.nb_activity > 1)? 'échanges' : 'échange' }}</li> <li><b>{{ person.counters.nb_activity }}</b> {{ (person.counters.nb_activity > 1)? 'échanges' : 'échange' }}</li>
<li><b>{{ person.counters.nb_task }}</b> {{ (person.counters.nb_task > 1)? 'tâches' : 'tâche' }}</li> <li><b>{{ person.counters.nb_task }}</b> {{ (person.counters.nb_task > 1)? 'tâches' : 'tâche' }}</li>
<li><b>{{ person.counters.nb_document }}</b> {{ (person.counters.nb_document > 1)? 'documents' : 'document' }}</li> <li><b>{{ person.counters.nb_document }}</b> {{ (person.counters.nb_document > 1)? 'documents' : 'document' }}</li>
<li><b>{{ person.counters.nb_event }}</b> {{ (person.counters.nb_event > 1)? 'événements' : 'événement' }}</li> {# <li><b>{{ person.counters.nb_event }}</b> {{ (person.counters.nb_event > 1)? 'événements' : 'événement' }}</li>#}
<li><b>{{ person.counters.nb_addresses }}</b> {{ (person.counters.nb_addresses > 1)? 'adresses' : 'adresse' }}</li> <li><b>{{ person.counters.nb_addresses }}</b> {{ (person.counters.nb_addresses > 1)? 'adresses' : 'adresse' }}</li>
</ul> </ul>

View File

@ -25,7 +25,7 @@
<h1>{{ 'Merge duplicate persons folders'|trans }}</h1> <h1>{{ 'Merge duplicate persons folders'|trans }}</h1>
<div class="col-md-6"> <div class="col-md-11">
<p><b>{{ 'Old person'|trans }}</b>: <p><b>{{ 'Old person'|trans }}</b>:
{{ 'Old person explain'|trans }} {{ 'Old person explain'|trans }}
</p> </p>
@ -43,7 +43,7 @@
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-11">
<p><b>{{ 'New person'|trans }}</b>: <p><b>{{ 'New person'|trans }}</b>:
{{ 'New person explain'|trans }} {{ 'New person explain'|trans }}
</p> </p>
@ -63,10 +63,10 @@
{{ form_start(form) }} {{ form_start(form) }}
<div class="col-md-4 centered"> <div class="col-md-12 centered">
<div class="container-fluid" style="padding-top: 1em;"> <div class="container-fluid" style="padding-top: 1em;">
<div class="col-1 clear" style="padding-top: 10px;"> <div class="clear" style="padding-top: 10px;">
{{ form_widget(form.confirm) }} {{ form_widget(form.confirm) }}
</div> </div>
<div class="col-11"> <div class="col-11">

View File

@ -3,6 +3,14 @@
{{ 'workflow.SocialAction deleted'|trans }} {{ 'workflow.SocialAction deleted'|trans }}
</div> </div>
{% else %} {% else %}
<div class="flex-table accompanying-course-work">
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with {
'w': work,
'displayAction': false,
'displayContent': 'short',
'itemBlocClass': 'bg-chill-light-gray'
} %}
</div>
{% if display_action is defined and display_action == true %} {% if display_action is defined and display_action == true %}
<ul class="record_actions"> <ul class="record_actions">
<li> <li>

View File

@ -123,7 +123,7 @@
<ul class="record_actions"> <ul class="record_actions">
<li>{{ doc.storedObject|chill_document_button_group(doc.title, is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', evaluation.accompanyingPeriodWork)) }}</li> <li>{{ doc.storedObject|chill_document_button_group(doc.title, is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', evaluation.accompanyingPeriodWork)) }}</li>
<li> <li>
<a class="btn btn-show" href="{{ path('chill_person_accompanying_period_work_edit', {'id': evaluation.accompanyingPeriodWork.id}) }}"> <a class="btn btn-show" href="{{ path('chill_person_accompanying_period_work_edit', {'id': evaluation.accompanyingPeriodWork.id, 'doc_id': doc.id}) }}">
{{ 'Show'|trans }} {{ 'Show'|trans }}
</a> </a>
</li> </li>

View File

@ -18,6 +18,7 @@ use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@ -119,6 +120,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
->generate(self::class) ->generate(self::class)
->addCheckFor(null, [self::CREATE, self::REASSIGN_BULK]) ->addCheckFor(null, [self::CREATE, self::REASSIGN_BULK])
->addCheckFor(AccompanyingPeriod::class, [self::TOGGLE_CONFIDENTIAL, ...self::ALL]) ->addCheckFor(AccompanyingPeriod::class, [self::TOGGLE_CONFIDENTIAL, ...self::ALL])
->addCheckFor(Household::class, [self::SEE])
->addCheckFor(Person::class, [self::SEE, self::CREATE]) ->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Center::class, [self::STATS]) ->addCheckFor(Center::class, [self::STATS])
->build(); ->build();

View File

@ -22,10 +22,14 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository; use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use DateTime; use DateTime;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -40,6 +44,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
use function array_key_exists; use function array_key_exists;
/** /**
* @see AccompanyingPeriodContextTest
* @template-implements DocGeneratorContextWithPublicFormInterface<AccompanyingPeriod> * @template-implements DocGeneratorContextWithPublicFormInterface<AccompanyingPeriod>
*/ */
class AccompanyingPeriodContext implements class AccompanyingPeriodContext implements
@ -62,6 +67,10 @@ class AccompanyingPeriodContext implements
private TranslatorInterface $translator; private TranslatorInterface $translator;
private ThirdPartyRender $thirdPartyRender;
private ThirdPartyRepository $thirdPartyRepository;
public function __construct( public function __construct(
DocumentCategoryRepository $documentCategoryRepository, DocumentCategoryRepository $documentCategoryRepository,
NormalizerInterface $normalizer, NormalizerInterface $normalizer,
@ -70,7 +79,9 @@ class AccompanyingPeriodContext implements
PersonRenderInterface $personRender, PersonRenderInterface $personRender,
PersonRepository $personRepository, PersonRepository $personRepository,
TranslatorInterface $translator, TranslatorInterface $translator,
BaseContextData $baseContextData BaseContextData $baseContextData,
ThirdPartyRender $thirdPartyRender,
ThirdPartyRepository $thirdPartyRepository
) { ) {
$this->documentCategoryRepository = $documentCategoryRepository; $this->documentCategoryRepository = $documentCategoryRepository;
$this->normalizer = $normalizer; $this->normalizer = $normalizer;
@ -80,6 +91,8 @@ class AccompanyingPeriodContext implements
$this->personRepository = $personRepository; $this->personRepository = $personRepository;
$this->translator = $translator; $this->translator = $translator;
$this->baseContextData = $baseContextData; $this->baseContextData = $baseContextData;
$this->thirdPartyRender = $thirdPartyRender;
$this->thirdPartyRepository = $thirdPartyRepository;
} }
public function adminFormReverseTransform(array $data): array public function adminFormReverseTransform(array $data): array
@ -103,11 +116,12 @@ class AccompanyingPeriodContext implements
'person1Label' => $data['person1Label'] ?? $this->translator->trans('docgen.person 1'), 'person1Label' => $data['person1Label'] ?? $this->translator->trans('docgen.person 1'),
'person2' => $data['person2'] ?? false, 'person2' => $data['person2'] ?? false,
'person2Label' => $data['person2Label'] ?? $this->translator->trans('docgen.person 2'), 'person2Label' => $data['person2Label'] ?? $this->translator->trans('docgen.person 2'),
'thirdParty' => $data['thirdParty'] ?? false,
'thirdPartyLabel' => $data['thirdPartyLabel'] ?? $this->translator->trans('Third party'),
]; ];
if (array_key_exists('category', $data)) { if (array_key_exists('category', $data)) {
$r['category'] = array_key_exists('category', $data) ? $r['category'] = $this->documentCategoryRepository->find($data['category']);
$this->documentCategoryRepository->find($data['category']) : null;
} }
return $r; return $r;
@ -140,6 +154,14 @@ class AccompanyingPeriodContext implements
'label' => 'person 2 label', 'label' => 'person 2 label',
'required' => true, 'required' => true,
]) ])
->add('thirdParty', CheckboxType::class, [
'required' => false,
'label' => 'docgen.Ask for thirdParty',
])
->add('thirdPartyLabel', TextType::class, [
'label' => 'docgen.thirdParty label',
'required' => true,
])
->add('category', EntityType::class, [ ->add('category', EntityType::class, [
'placeholder' => 'Choose a document category', 'placeholder' => 'Choose a document category',
'class' => DocumentCategory::class, 'class' => DocumentCategory::class,
@ -190,6 +212,28 @@ class AccompanyingPeriodContext implements
]); ]);
} }
} }
$thirdParties = [...array_values(array_filter([$entity->getRequestorThirdParty()])), ...array_values(array_filter(
array_map(
fn (Resource $r): ?ThirdParty => $r->getThirdParty(),
$entity->getResources()->filter(
static fn (Resource $r): bool => null !== $r->getThirdParty()
)->toArray()
)
))];
if ($options['thirdParty'] ?? false) {
$builder->add('thirdParty', EntityType::class, [
'class' => ThirdParty::class,
'choices' => $thirdParties,
'choice_label' => fn (ThirdParty $p) => $this->thirdPartyRender->renderString($p, []),
'multiple' => false,
'required' => false,
'expanded' => true,
'label' => $options['thirdPartyLabel'],
'placeholder' => $this->translator->trans('Any third party selected'),
]);
}
} }
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
@ -215,6 +259,13 @@ class AccompanyingPeriodContext implements
} }
} }
if ($options['thirdParty']) {
$data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'], 'docgen', [
'docgen:expects' => ThirdParty::class,
'groups' => 'docgen:read'
]);
}
return $data; return $data;
} }
@ -254,7 +305,7 @@ class AccompanyingPeriodContext implements
{ {
$options = $template->getOptions(); $options = $template->getOptions();
return $options['mainPerson'] || $options['person1'] || $options['person2']; return $options['mainPerson'] || $options['person1'] || $options['person2'] || $options ['thirdParty'];
} }
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
@ -264,6 +315,8 @@ class AccompanyingPeriodContext implements
$normalized[$k] = null !== ($data[$k] ?? null) ? $data[$k]->getId() : null; $normalized[$k] = null !== ($data[$k] ?? null) ? $data[$k]->getId() : null;
} }
$normalized['thirdParty'] = ($data['thirdParty'] ?? null)?->getId();
return $normalized; return $normalized;
} }
@ -279,6 +332,12 @@ class AccompanyingPeriodContext implements
} }
} }
if (null !== ($id = ($data['thirdParty'] ?? null))) {
$denormalized['thirdParty'] = $this->thirdPartyRepository->find($id);
} else {
$denormalized['thirdParty'] = null;
}
return $denormalized; return $denormalized;
} }

View File

@ -18,13 +18,18 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Repository\SocialWork\EvaluationRepository; use Chill\PersonBundle\Repository\SocialWork\EvaluationRepository;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @implements DocGeneratorContextWithPublicFormInterface<AccompanyingPeriodWorkEvaluation> * @implements DocGeneratorContextWithPublicFormInterface<AccompanyingPeriodWorkEvaluation>
@ -43,18 +48,26 @@ class AccompanyingPeriodWorkEvaluationContext implements
private TranslatableStringHelperInterface $translatableStringHelper; private TranslatableStringHelperInterface $translatableStringHelper;
private ThirdPartyRender $thirdPartyRender;
private TranslatorInterface $translator;
public function __construct( public function __construct(
AccompanyingPeriodWorkContext $accompanyingPeriodWorkContext, AccompanyingPeriodWorkContext $accompanyingPeriodWorkContext,
EntityManagerInterface $em, EntityManagerInterface $em,
EvaluationRepository $evaluationRepository, EvaluationRepository $evaluationRepository,
NormalizerInterface $normalizer, NormalizerInterface $normalizer,
TranslatableStringHelperInterface $translatableStringHelper TranslatableStringHelperInterface $translatableStringHelper,
ThirdPartyRender $thirdPartyRender,
TranslatorInterface $translator
) { ) {
$this->accompanyingPeriodWorkContext = $accompanyingPeriodWorkContext; $this->accompanyingPeriodWorkContext = $accompanyingPeriodWorkContext;
$this->em = $em; $this->em = $em;
$this->evaluationRepository = $evaluationRepository; $this->evaluationRepository = $evaluationRepository;
$this->normalizer = $normalizer; $this->normalizer = $normalizer;
$this->translatableStringHelper = $translatableStringHelper; $this->translatableStringHelper = $translatableStringHelper;
$this->thirdPartyRender = $thirdPartyRender;
$this->translator = $translator;
} }
public function adminFormReverseTransform(array $data): array public function adminFormReverseTransform(array $data): array
@ -102,6 +115,31 @@ class AccompanyingPeriodWorkEvaluationContext implements
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
{ {
$this->accompanyingPeriodWorkContext->buildPublicForm($builder, $template, $entity->getAccompanyingPeriodWork()); $this->accompanyingPeriodWorkContext->buildPublicForm($builder, $template, $entity->getAccompanyingPeriodWork());
$thirdParties = [...array_values(array_filter($entity->getAccompanyingPeriodWork()->getThirdParties()->toArray())), ...array_values(array_filter([$entity->getAccompanyingPeriodWork()->getHandlingThierParty()])), ...array_values(
array_filter(
array_map(
fn (Resource $r): ?ThirdParty => $r->getThirdParty(),
$entity->getAccompanyingPeriodWork()->getAccompanyingPeriod()->getResources()->filter(
static fn (Resource $r): bool => null !== $r->getThirdParty()
)->toArray()
)
)
)];
$options = $template->getOptions();
if ($options['thirdParty'] ?? false) {
$builder->add('thirdParty', EntityType::class, [
'class' => ThirdParty::class,
'choices' => $thirdParties,
'choice_label' => fn (ThirdParty $p) => $this->thirdPartyRender->renderString($p, []),
'multiple' => false,
'required' => false,
'expanded' => true,
'label' => $options['thirdPartyLabel'],
'placeholder' => $this->translator->trans('Any third party selected'),
]);
}
} }
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
@ -116,7 +154,6 @@ class AccompanyingPeriodWorkEvaluationContext implements
AbstractNormalizer::GROUPS => ['docgen:read'], AbstractNormalizer::GROUPS => ['docgen:read'],
] ]
); );
return $data; return $data;
} }

View File

@ -26,14 +26,22 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Person\PersonResource;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Person\ResidentialAddress;
use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use DateTime; use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use LogicException; use LogicException;
use Service\DocGenerator\PersonContextTest;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@ -43,6 +51,9 @@ use Symfony\Contracts\Translation\TranslatorInterface;
use function array_key_exists; use function array_key_exists;
use function count; use function count;
/**
* @see PersonContextTest
*/
final class PersonContext implements PersonContextInterface final class PersonContext implements PersonContextInterface
{ {
private AuthorizationHelperInterface $authorizationHelper; private AuthorizationHelperInterface $authorizationHelper;
@ -67,6 +78,12 @@ final class PersonContext implements PersonContextInterface
private TranslatorInterface $translator; private TranslatorInterface $translator;
private ThirdPartyRender $thirdPartyRender;
private ThirdPartyRepository $thirdPartyRepository;
private ResidentialAddressRepository $residentialAddressRepository;
public function __construct( public function __construct(
AuthorizationHelperInterface $authorizationHelper, AuthorizationHelperInterface $authorizationHelper,
BaseContextData $baseContextData, BaseContextData $baseContextData,
@ -78,7 +95,10 @@ final class PersonContext implements PersonContextInterface
ScopeRepositoryInterface $scopeRepository, ScopeRepositoryInterface $scopeRepository,
Security $security, Security $security,
TranslatorInterface $translator, TranslatorInterface $translator,
TranslatableStringHelperInterface $translatableStringHelper TranslatableStringHelperInterface $translatableStringHelper,
ThirdPartyRender $thirdPartyRender,
ThirdPartyRepository $thirdPartyRepository,
ResidentialAddressRepository $residentialAddressRepository
) { ) {
$this->authorizationHelper = $authorizationHelper; $this->authorizationHelper = $authorizationHelper;
$this->centerResolverManager = $centerResolverManager; $this->centerResolverManager = $centerResolverManager;
@ -91,6 +111,9 @@ final class PersonContext implements PersonContextInterface
$this->showScopes = $parameterBag->get('chill_main')['acl']['form_show_scopes']; $this->showScopes = $parameterBag->get('chill_main')['acl']['form_show_scopes'];
$this->translator = $translator; $this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper; $this->translatableStringHelper = $translatableStringHelper;
$this->thirdPartyRender = $thirdPartyRender;
$this->thirdPartyRepository = $thirdPartyRepository;
$this->residentialAddressRepository = $residentialAddressRepository;
} }
public function adminFormReverseTransform(array $data): array public function adminFormReverseTransform(array $data): array
@ -110,11 +133,12 @@ final class PersonContext implements PersonContextInterface
$r = [ $r = [
'mainPerson' => $data['mainPerson'] ?? false, 'mainPerson' => $data['mainPerson'] ?? false,
'mainPersonLabel' => $data['mainPersonLabel'] ?? $this->translator->trans('docgen.Main person'), 'mainPersonLabel' => $data['mainPersonLabel'] ?? $this->translator->trans('docgen.Main person'),
'thirdParty' => $data['thirdParty'] ?? false,
'thirdPartyLabel' => $data['thirdPartyLabel'] ?? $this->translator->trans('Third party'),
]; ];
if (array_key_exists('category', $data)) { if (array_key_exists('category', $data)) {
$r['category'] = array_key_exists('category', $data) ? $r['category'] = $this->documentCategoryRepository->find($data['category']);
$this->documentCategoryRepository->find($data['category']) : null;
} }
return $r; return $r;
@ -131,6 +155,14 @@ final class PersonContext implements PersonContextInterface
->setParameter('docClass', PersonDocument::class), ->setParameter('docClass', PersonDocument::class),
'choice_label' => fn ($entity = null) => $entity ? $this->translatableStringHelper->localize($entity->getName()) : '', 'choice_label' => fn ($entity = null) => $entity ? $this->translatableStringHelper->localize($entity->getName()) : '',
'required' => true, 'required' => true,
])
->add('thirdParty', CheckboxType::class, [
'required' => false,
'label' => 'docgen.Ask for thirdParty',
])
->add('thirdPartyLabel', TextType::class, [
'label' => 'docgen.thirdParty label',
'required' => true,
]); ]);
} }
@ -139,12 +171,47 @@ final class PersonContext implements PersonContextInterface
*/ */
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
{ {
$options = $template->getOptions();
$builder->add('title', TextType::class, [ $builder->add('title', TextType::class, [
'required' => true, 'required' => true,
'label' => 'docgen.Document title', 'label' => 'docgen.Document title',
'data' => $this->translatableStringHelper->localize($template->getName()), 'data' => $this->translatableStringHelper->localize($template->getName()),
]); ]);
$thirdParties = [...array_values(
array_filter(
array_map(
fn (ResidentialAddress $r): ?ThirdParty => $r->getHostThirdParty(),
$this
->residentialAddressRepository
->findCurrentResidentialAddressByPerson($entity)
)
)
), ...array_values(
array_filter(
array_map(
fn (PersonResource $r): ?ThirdParty => $r->getThirdParty(),
$entity->getResources()->filter(
static fn (PersonResource $r): bool => null !== $r->getThirdParty()
)->toArray()
)
)
)];
if ($options['thirdParty'] ?? false) {
$builder->add('thirdParty', EntityType::class, [
'class' => ThirdParty::class,
'choices' => $thirdParties,
'choice_label' => fn (ThirdParty $p) => $this->thirdPartyRender->renderString($p, []),
'multiple' => false,
'required' => false,
'expanded' => true,
'label' => $options['thirdPartyLabel'],
'placeholder' => $this->translator->trans('Any third party selected'),
]);
}
if ($this->isScopeNecessary($entity)) { if ($this->isScopeNecessary($entity)) {
$builder->add('scope', ScopePickerType::class, [ $builder->add('scope', ScopePickerType::class, [
'center' => $this->centerResolverManager->resolveCenters($entity), 'center' => $this->centerResolverManager->resolveCenters($entity),
@ -156,10 +223,6 @@ final class PersonContext implements PersonContextInterface
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
{ {
if (!$entity instanceof Person) {
throw new UnexpectedTypeException($entity, Person::class);
}
$data = []; $data = [];
$data = array_merge($data, $this->baseContextData->getData($contextGenerationData['creator'] ?? null)); $data = array_merge($data, $this->baseContextData->getData($contextGenerationData['creator'] ?? null));
$data['person'] = $this->normalizer->normalize($entity, 'docgen', [ $data['person'] = $this->normalizer->normalize($entity, 'docgen', [
@ -170,6 +233,13 @@ final class PersonContext implements PersonContextInterface
'docgen:person:with-budget' => true, 'docgen:person:with-budget' => true,
]); ]);
if ($template->getOptions()['thirdParty']) {
$data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'], 'docgen', [
'docgen:expects' => ThirdParty::class,
'groups' => 'docgen:read'
]);
}
return $data; return $data;
} }
@ -223,6 +293,7 @@ final class PersonContext implements PersonContextInterface
return [ return [
'title' => $data['title'] ?? '', 'title' => $data['title'] ?? '',
'scope_id' => $scope instanceof Scope ? $scope->getId() : null, 'scope_id' => $scope instanceof Scope ? $scope->getId() : null,
'thirdParty' => ($data['thirdParty'] ?? null)?->getId(),
]; ];
} }
@ -242,6 +313,7 @@ final class PersonContext implements PersonContextInterface
return [ return [
'title' => $data['title'] ?? '', 'title' => $data['title'] ?? '',
'scope' => $scope, 'scope' => $scope,
'thirdParty' => null !== ($id = ($data['thirdParty'] ?? null)) ? $this->thirdPartyRepository->find($id) : null,
]; ];
} }

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