diff --git a/.changes/header.tpl.md b/.changes/header.tpl.md new file mode 100644 index 000000000..df8faa7b2 --- /dev/null +++ b/.changes/header.tpl.md @@ -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). diff --git a/.changes/unreleased/.gitkeep b/.changes/unreleased/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.changes/v2.0.0.md b/.changes/v2.0.0.md new file mode 100644 index 000000000..8d70ecba6 --- /dev/null +++ b/.changes/v2.0.0.md @@ -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 + + + +## Stable releases + +No stable releases for v2+ + diff --git a/.changes/v2.1.0.md b/.changes/v2.1.0.md new file mode 100644 index 000000000..ade83aee0 --- /dev/null +++ b/.changes/v2.1.0.md @@ -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 diff --git a/.changes/v2.2.0.md b/.changes/v2.2.0.md new file mode 100644 index 000000000..5371cad35 --- /dev/null +++ b/.changes/v2.2.0.md @@ -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 diff --git a/.changes/v2.2.1.md b/.changes/v2.2.1.md new file mode 100644 index 000000000..c96b18585 --- /dev/null +++ b/.changes/v2.2.1.md @@ -0,0 +1,3 @@ +## v2.2.1 - 2023-06-19 +### Fixed +* ([#114](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/114)) [notification on document evaluation] fix entityId and return path when adding a notification on a document in an evaluation diff --git a/.changes/v2.2.2.md b/.changes/v2.2.2.md new file mode 100644 index 000000000..61d194b6d --- /dev/null +++ b/.changes/v2.2.2.md @@ -0,0 +1,5 @@ +## v2.2.2 - 2023-06-26 +### Fixed +* [Accompanying period comments]: order comments from the most recent to the oldest, in the list +* Api: filter social action to keep only the currently activated +* ([#82](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/82)) Fix deletion and re-creation of filiation relationship diff --git a/.changes/v2.3.0.md b/.changes/v2.3.0.md new file mode 100644 index 000000000..827a338de --- /dev/null +++ b/.changes/v2.3.0.md @@ -0,0 +1,42 @@ +## v2.3.0 - 2023-06-27 +### Feature +* ([#110](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/110)) Edit saved exports options: the saved exports options (forms, filters, aggregators) are now editable. +* ([#103](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/103)) Get an unified list of document in person and accompanying period context +* [export] Set the default date of calculation of the accompanying period's list as "today" +* Force accompanying period user history to be unique for the same period and stardate/enddate [:warning: may encounter migration issue] + + If some issue is encountered during migration, use this SQL to find the line which are in conflict, examine the problem and delete some of the concerning line +* + ```sql + -- to see the line which are in conflict with another one + SELECT o.* + FROM chill_person_accompanying_period_user_history o + JOIN chill_person_accompanying_period_user_history c ON o.id < c.id AND o.accompanyingperiod_id = c.accompanyingperiod_id + WHERE tsrange(o.startdate, o.enddate, '[)') && tsrange(c.startdate, c.enddate, '[)') + ORDER BY accompanyingperiod_id; + -- to examine line in conflict for a given accompanyingperiod_id (given by the previous query) + SELECT * FROM chill_person_accompanying_period_user_history WHERE accompanyingperiod_id = IIIIDDDD order by startdate, enddate; + ``` +* Rename label of filter in French: "parcours actif" => "parcours ouvert", and "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d'ouverture" + +### Traduction francophone des principaux changements + +* Les exports enregistrés sont éditables par l'utilisateur; +* L'onglet "Document" dans les parcours et les dossiers d'usager affiche désormais les documents ajoutés à différents endroits. + + Pour les parcours, il s'agit de: + + - documents ajoutés directement dans le parcours; + - documents des échanges; + - documents des rendez-vous; + - documents des évaluations; + - documents directement ajoutés dans le dossier des usagers concernés par le parcours; + + Pour les usagers, il s'agit de: + + - documents des échanges; + - documents des parcours; + - documents des rendez-vous; + - documents des actions, des échanges, des rendez-vous, des évaluations ajoutés dans les parcours. +* Dans la liste des parcours, la date de calcul des éléments associés est "aujourd'hui" par défaut. +* Dans les exports, renommage des libellés des filtres: "parcours actif" => "parcours ouvert", et "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d'ouverture" diff --git a/.changes/v2.4.0.md b/.changes/v2.4.0.md new file mode 100644 index 000000000..522957300 --- /dev/null +++ b/.changes/v2.4.0.md @@ -0,0 +1,36 @@ +## v2.4.0 - 2023-07-07 + +### Feature +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] on "filter by user working" on accompanying period, add two dates to filters intervention within a period +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add an aggregator by user's job working on a course +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add an aggregator by user's scope working on a course +* [export] on aggregator "user working on a course" +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a center aggregator for Person +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a filter on "job working on a course" +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add a filter on "scope working on a course" +* ([#121](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/121)) Create a role "See Confidential Periods", separated from the "Reassign courses" role +* ([#124](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/124)) Sync user absence / presence through microsoft outlook / graph api. + +### Fixed +* ([#116](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/116)) On the accompanying course page, open the action on view mode if the user does not have right to update them (i.e. if the accompanying period is closed) +* [export] Rename label for CurrentActionFilter (on accompanying period work) to make precision between "ouvert" and "sans date de fin" +* Force the db to have either a person_location or a address_location, and avoid to have both also internally in the entity +* [export] set rolling date on person age aggregator +* [export] fix list when a person locating a course is without address +* [export] remove unused condition on course about duration participation +* Command to subscribe on MS Graph users calendars: improve the loop to be more efficient + +### DX +* Rolling Date: can receive a null parameter + +### Traduction francophone des principaux changements + +- sur le "filtre par intervenant", ajoute deux dates pour limiter la période d'intervention; +- ajout d'un regroupement par métier des intervenants sur un parcours; +- ajout d'un regroupement par service des intervenants sur un parcours; +- ajout d'un regroupement par utilisateur intervenant sur un parcours +- ajout d'un regroupement "par centre de l'usager"; +- ajout d'un filtre "par métier intervenant sur un parcours"; +- ajout d'un filtre "par service intervenant sur un parcours"; +- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot); +- synchronisation de l'absence des utilisateurs par microsoft graph api diff --git a/.changie.yaml b/.changie.yaml new file mode 100644 index 000000000..8a25ed695 --- /dev/null +++ b/.changie.yaml @@ -0,0 +1,37 @@ +changesDir: .changes +unreleasedDir: unreleased +headerPath: header.tpl.md +changelogPath: CHANGELOG.md +versionExt: md +versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}' +kindFormat: '### {{.Kind}}' +# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description. +changeFormat: >- + * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}} {{ if not (eq .Custom.Long "") }} + + {{ .Custom.Long }}{{ end }} +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_ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3f1d75ed5..aa1eff8b1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,6 +34,7 @@ variables: stages: - Composer install - Tests + - Deploy build: stage: Composer install @@ -121,3 +122,14 @@ unit_tests: paths: - bin - tests/app/vendor/ + +release: + stage: Deploy + image: registry.gitlab.com/gitlab-org/release-cli:latest + rules: + - if: $CI_COMMIT_TAG + script: + - echo "running release_job" + release: + tag_name: '$CI_COMMIT_TAG' + description: "./.changes/v$CI_COMMIT_TAG.md" diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 4b5bf98ee..31d64e600 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -13,6 +13,7 @@ $finder = PhpCsFixer\Finder::create(); $finder ->in(__DIR__.'/src') + ->in(__DIR__.'/utils') ->append([__FILE__]) ->exclude(['docs/', 'tests/app']) ->notPath('tests/app') diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f951479..b74eea58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,140 @@ # Changelog - All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to +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.4.0 - 2023-07-07 + +### Feature +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] on "filter by user working" on accompanying period, add two dates to filters intervention within a period +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add an aggregator by user's job working on a course +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add an aggregator by user's scope working on a course +* [export] on aggregator "user working on a course" +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a center aggregator for Person +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a filter on "job working on a course" +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add a filter on "scope working on a course" +* ([#121](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/121)) Create a role "See Confidential Periods", separated from the "Reassign courses" role +* ([#124](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/124)) Sync user absence / presence through microsoft outlook / graph api. + +### Fixed +* ([#116](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/116)) On the accompanying course page, open the action on view mode if the user does not have right to update them (i.e. if the accompanying period is closed) +* [export] Rename label for CurrentActionFilter (on accompanying period work) to make precision between "ouvert" and "sans date de fin" +* Force the db to have either a person_location or a address_location, and avoid to have both also internally in the entity +* [export] set rolling date on person age aggregator +* [export] fix list when a person locating a course is without address +* [export] remove unused condition on course about duration participation +* Command to subscribe on MS Graph users calendars: improve the loop to be more efficient + +### DX +* Rolling Date: can receive a null parameter + +### Traduction francophone des principaux changements + +- sur le "filtre par intervenant", ajoute deux dates pour limiter la période d'intervention; +- ajout d'un regroupement par métier des intervenants sur un parcours; +- ajout d'un regroupement par service des intervenants sur un parcours; +- ajout d'un regroupement par utilisateur intervenant sur un parcours +- ajout d'un regroupement "par centre de l'usager"; +- ajout d'un filtre "par métier intervenant sur un parcours"; +- ajout d'un filtre "par service intervenant sur un parcours"; +- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot); +- synchronisation de l'absence des utilisateurs par microsoft graph api + +## v2.3.0 - 2023-06-27 +### Feature +* ([#110](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/110)) Edit saved exports options: the saved exports options (forms, filters, aggregators) are now editable. +* ([#103](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/103)) Get an unified list of document in person and accompanying period context +* [export] Set the default date of calculation of the accompanying period's list as "today" +* Force accompanying period user history to be unique for the same period and stardate/enddate [:warning: may encounter migration issue] + + If some issue is encountered during migration, use this SQL to find the line which are in conflict, examine the problem and delete some of the concerning line +* + ```sql + -- to see the line which are in conflict with another one + SELECT o.* + FROM chill_person_accompanying_period_user_history o + JOIN chill_person_accompanying_period_user_history c ON o.id < c.id AND o.accompanyingperiod_id = c.accompanyingperiod_id + WHERE tsrange(o.startdate, o.enddate, '[)') && tsrange(c.startdate, c.enddate, '[)') + ORDER BY accompanyingperiod_id; + -- to examine line in conflict for a given accompanyingperiod_id (given by the previous query) + SELECT * FROM chill_person_accompanying_period_user_history WHERE accompanyingperiod_id = IIIIDDDD order by startdate, enddate; + ``` +* Rename label of filter in French: "parcours actif" => "parcours ouvert", and "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d'ouverture" + +### Traduction francophone des principaux changements + +* Les exports enregistrés sont éditables par l'utilisateur; +* L'onglet "Document" dans les parcours et les dossiers d'usager affiche désormais les documents ajoutés à différents endroits. + + Pour les parcours, il s'agit de: + + - documents ajoutés directement dans le parcours; + - documents des échanges; + - documents des rendez-vous; + - documents des évaluations; + - documents directement ajoutés dans le dossier des usagers concernés par le parcours; + + Pour les usagers, il s'agit de: + + - documents des échanges; + - documents des parcours; + - documents des rendez-vous; + - documents des actions, des échanges, des rendez-vous, des évaluations ajoutés dans les parcours. +* Dans la liste des parcours, la date de calcul des éléments associés est "aujourd'hui" par défaut. +* Dans les exports, renommage des libellés des filtres: "parcours actif" => "parcours ouvert", et "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d'ouverture" + +## v2.2.2 - 2023-06-26 +### Fixed +* [Accompanying period comments]: order comments from the most recent to the oldest, in the list +* Api: filter social action to keep only the currently activated +* ([#82](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/82)) Fix deletion and re-creation of filiation relationship + +## v2.2.1 - 2023-06-19 +### Fixed +* ([#114](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/114)) [notification on document evaluation] fix entityId and return path when adding a notification on a document in an evaluation + +## 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 - * [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 @@ -18,9 +142,6 @@ and this project adheres to * [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 - -## Test releases - ### 2.0.0-beta2 * [workflow]: Fixed: the notification is sent when the user is added to the first step. diff --git a/composer.json b/composer.json index f6d3eb27a..a128b1b77 100644 --- a/composer.json +++ b/composer.json @@ -67,6 +67,7 @@ "fakerphp/faker": "^1.13", "jangregor/phpstan-prophecy": "^1.0", "nelmio/alice": "^3.8", + "nikic/php-parser": "^4.15", "phpspec/prophecy-phpunit": "^2.0", "phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^1.9", @@ -103,14 +104,16 @@ "Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle", "Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle", "Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle", - "Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src" + "Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src", + "Chill\\Utils\\Rector\\": "utils/rector/src" } }, "autoload-dev": { "psr-4": { "App\\": "tests/app/src/", "Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", - "Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests" + "Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", + "Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests" } }, "config": { diff --git a/docs/source/_static/code/exports/BirthdateFilter.php b/docs/source/_static/code/exports/BirthdateFilter.php index 64c1d53a9..e25d8b42f 100644 --- a/docs/source/_static/code/exports/BirthdateFilter.php +++ b/docs/source/_static/code/exports/BirthdateFilter.php @@ -62,7 +62,6 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac { $builder->add('date_from', DateType::class, [ 'label' => 'Born after this date', - 'data' => new DateTime(), 'attr' => ['class' => 'datepicker'], 'widget' => 'single_text', 'format' => 'dd-MM-yyyy', @@ -70,12 +69,15 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac $builder->add('date_to', DateType::class, [ 'label' => 'Born before this date', - 'data' => new DateTime(), 'attr' => ['class' => 'datepicker'], 'widget' => 'single_text', 'format' => 'dd-MM-yyyy', ]); } + public function getFormDefaultData(): array + { + return ['date_from' => new DateTime(), 'date_to' => new DateTime()]; + } // here, we create a simple string which will describe the action of // the filter in the Response diff --git a/docs/source/_static/code/exports/CountPerson.php b/docs/source/_static/code/exports/CountPerson.php index afe19c73b..be800e52c 100644 --- a/docs/source/_static/code/exports/CountPerson.php +++ b/docs/source/_static/code/exports/CountPerson.php @@ -36,6 +36,10 @@ class CountPerson implements ExportInterface { // this export does not add any form } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes() { diff --git a/exports_alias_conventions.md b/exports_alias_conventions.md index fd7844691..64df91030 100644 --- a/exports_alias_conventions.md +++ b/exports_alias_conventions.md @@ -18,6 +18,7 @@ These are alias conventions : | | SocialIssue::class | acp.socialIssues | acpsocialissue | | | User::class | acp.user | acpuser | | | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories | +| | AccompanyingPeriodInfo::class | not existing (using custom WITH clause) | acpinfo | | AccompanyingPeriodWork::class | | | acpw | | | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval | | | User::class | acpw.referrers | acpwuser | @@ -28,6 +29,8 @@ These are alias conventions : | | Person::class | acppart.person | partperson | | AccompanyingPeriodWorkEvaluation::class | | | workeval | | | Evaluation::class | workeval.evaluation | eval | +| AccompanyingPeriodInfo::class | | | acpinfo | +| | User::class | acpinfo.user | acpinfo_user | | Goal::class | | | goal | | | Result::class | goal.results | goalresult | | Person::class | | | person | diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 56b7c2228..62dbe0468 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,6 +2,7 @@ parameters: level: 5 paths: - src/ + - utils/ tmpDir: .cache/ reportUnmatchedIgnoredErrors: false excludePaths: diff --git a/phpunit.rector.xml b/phpunit.rector.xml new file mode 100644 index 000000000..f8d9d3a11 --- /dev/null +++ b/phpunit.rector.xml @@ -0,0 +1,29 @@ + + + + + utils/rector/tests + + + + + + utils/rector/src + + + diff --git a/rector.php b/rector.php index ec9a0c684..c2e752c32 100644 --- a/rector.php +++ b/rector.php @@ -24,6 +24,9 @@ return static function (RectorConfig $rectorConfig): void { LevelSetList::UP_TO_PHP_74 ]); + // chill rules + $rectorConfig->rule(\Chill\Utils\Rector\Rector\ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector::class); + // skip some path... $rectorConfig->skip([ // make rector stuck for some files diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php index 5c6656009..c23db738e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php @@ -40,6 +40,10 @@ class ByActivityNumberAggregator implements AggregatorInterface { // No form needed } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByCreatorAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByCreatorAggregator.php index 69149737b..917459de3 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByCreatorAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByCreatorAggregator.php @@ -52,6 +52,10 @@ class ByCreatorAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php index 89732412d..1a75357ae 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php @@ -57,6 +57,10 @@ class BySocialActionAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php index 158e87664..9100a8c8f 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php @@ -57,6 +57,10 @@ class BySocialIssueAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByThirdpartyAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByThirdpartyAggregator.php index c3ca6d59c..ac05b153c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByThirdpartyAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByThirdpartyAggregator.php @@ -57,6 +57,10 @@ class ByThirdpartyAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/CreatorScopeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/CreatorScopeAggregator.php index 2c7ec1483..a4b5258e2 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/CreatorScopeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/CreatorScopeAggregator.php @@ -57,6 +57,10 @@ class CreatorScopeAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php index b4b23dc0b..4ede5b4c4 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/DateAggregator.php @@ -29,14 +29,6 @@ class DateAggregator implements AggregatorInterface private const DEFAULT_CHOICE = 'year'; - private TranslatorInterface $translator; - - public function __construct( - TranslatorInterface $translator - ) { - $this->translator = $translator; - } - public function addRole(): ?string { return null; @@ -84,9 +76,12 @@ class DateAggregator implements AggregatorInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['frequency' => self::DEFAULT_CHOICE]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/LocationTypeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/LocationTypeAggregator.php index ec4ce6315..c72609e2c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/LocationTypeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/LocationTypeAggregator.php @@ -57,6 +57,10 @@ class LocationTypeAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php index 7cd16718e..a74428b4a 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php @@ -60,6 +60,10 @@ class ActivityTypeAggregator implements AggregatorInterface { // no form required for this aggregator } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data): Closure { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php index 9bde692c6..2fab2af83 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php @@ -58,6 +58,10 @@ class ActivityUserAggregator implements AggregatorInterface { // nothing to add } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, $values, $data): Closure { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php index 139f2743e..e1e9f161d 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php @@ -56,6 +56,10 @@ class ActivityUsersAggregator implements AggregatorInterface { // nothing to add on the form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php index 5741a0e58..721078989 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php @@ -55,6 +55,10 @@ class ActivityUsersJobAggregator implements \Chill\MainBundle\Export\AggregatorI { // nothing to add in the form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php index 15da300be..d7932e9e8 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php @@ -55,6 +55,10 @@ class ActivityUsersScopeAggregator implements \Chill\MainBundle\Export\Aggregato { // nothing to add in the form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/ActivityReasonAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/ActivityReasonAggregator.php index eaccf95cb..2537f2cf6 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/ActivityReasonAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/ActivityReasonAggregator.php @@ -110,6 +110,10 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali ] ); } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php index 5f772e156..ae1dae375 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php @@ -47,6 +47,10 @@ class SentReceivedAggregator implements AggregatorInterface { // No form needed } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data): callable { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityDuration.php index 34771d077..6930784d3 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityDuration.php @@ -39,6 +39,10 @@ class AvgActivityDuration implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityVisitDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityVisitDuration.php index df21362cd..f1e926c64 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityVisitDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityVisitDuration.php @@ -40,6 +40,10 @@ class AvgActivityVisitDuration implements ExportInterface, GroupedExportInterfac { // TODO: Implement buildForm() method. } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountActivity.php index 6b7b1562d..d473a925a 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountActivity.php @@ -39,6 +39,10 @@ class CountActivity implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/ListActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/ListActivity.php index 026e16f90..9ed6bcda2 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/ListActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/ListActivity.php @@ -44,6 +44,10 @@ class ListActivity implements ListInterface, GroupedExportInterface { $this->helper->buildForm($builder); } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php index e916cab54..8adb3b77f 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php @@ -40,6 +40,10 @@ class SumActivityDuration implements ExportInterface, GroupedExportInterface { // TODO: Implement buildForm() method. } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php index 18a47289c..cc424e68b 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php @@ -40,6 +40,10 @@ class SumActivityVisitDuration implements ExportInterface, GroupedExportInterfac { // TODO: Implement buildForm() method. } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountActivity.php index 4246df173..6360251b3 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountActivity.php @@ -35,6 +35,10 @@ class CountActivity implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php index 60110c9a8..d14111ba7 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php @@ -88,6 +88,10 @@ class ListActivity implements ListInterface, GroupedExportInterface ])], ]); } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/StatActivityDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/StatActivityDuration.php index 050034954..e68d47cd3 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/StatActivityDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/StatActivityDuration.php @@ -53,6 +53,10 @@ class StatActivityDuration implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php index c6616a4c6..4ffc3b38c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php @@ -68,6 +68,10 @@ class ActivityTypeFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php index 322393f32..add9c7c3d 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php @@ -52,6 +52,10 @@ class ByCreatorFilter implements FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php index d0c1b0fc7..08f6bbbc3 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php @@ -60,6 +60,10 @@ class BySocialActionFilter implements FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php index bbb882a65..bd6e2ef4a 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php @@ -60,6 +60,10 @@ class BySocialIssueFilter implements FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/EmergencyFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/EmergencyFilter.php index b79c2ca10..807ba3903 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/EmergencyFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/EmergencyFilter.php @@ -68,9 +68,12 @@ class EmergencyFilter implements FilterInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['accepted_emergency' => self::DEFAULT_CHOICE]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php index 570f42ae0..b5dab4009 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php @@ -44,6 +44,10 @@ class HasNoActivityFilter implements FilterInterface { //no form needed } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php index 3d69d1633..312f3a6a0 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php @@ -46,6 +46,10 @@ class LocationFilter implements FilterInterface 'label' => 'pick location', ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationTypeFilter.php index 5fe928b6c..1c3415460 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationTypeFilter.php @@ -65,6 +65,10 @@ class LocationTypeFilter implements FilterInterface //'label' => false, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/SentReceivedFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/SentReceivedFilter.php index 8daa7a781..0f2a1c7e4 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/SentReceivedFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/SentReceivedFilter.php @@ -69,9 +69,12 @@ class SentReceivedFilter implements FilterInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['accepted_sentreceived' => self::DEFAULT_CHOICE]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserFilter.php index 6350f3ace..9ae988579 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserFilter.php @@ -61,6 +61,10 @@ class UserFilter implements FilterInterface 'label' => 'Creators', ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserScopeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserScopeFilter.php index 4319c100a..adb1e94f1 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserScopeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserScopeFilter.php @@ -71,6 +71,10 @@ class UserScopeFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php index f2216c929..e582acc12 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php @@ -80,11 +80,9 @@ class ActivityDateFilter implements FilterInterface $builder ->add('date_from', PickRollingDateType::class, [ 'label' => 'Activities after this date', - 'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), ]) ->add('date_to', PickRollingDateType::class, [ 'label' => 'Activities before this date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) { @@ -127,6 +125,10 @@ class ActivityDateFilter implements FilterInterface } }); } + public function getFormDefaultData(): array + { + return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php index b9d39c3ce..8dfcb543c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php @@ -78,6 +78,10 @@ class ActivityTypeFilter implements ExportElementValidatedInterface, FilterInter ], ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php index 2f6cd8462..a63ca2629 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php @@ -56,6 +56,10 @@ class ActivityUsersFilter implements FilterInterface 'label' => 'Users', ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php index c55d579e4..31cfde6b6 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php @@ -82,6 +82,10 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt 'expanded' => false, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php index e3c85fe9c..490b5fd0c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php @@ -112,7 +112,6 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt { $builder->add('date_from', DateType::class, [ 'label' => 'Implied in an activity after this date', - 'data' => new DateTime(), 'attr' => ['class' => 'datepicker'], 'widget' => 'single_text', 'format' => 'dd-MM-yyyy', @@ -120,7 +119,6 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt $builder->add('date_to', DateType::class, [ 'label' => 'Implied in an activity before this date', - 'data' => new DateTime(), 'attr' => ['class' => 'datepicker'], 'widget' => 'single_text', 'format' => 'dd-MM-yyyy', @@ -130,7 +128,6 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt 'class' => ActivityReason::class, 'choice_label' => fn (ActivityReason $reason): ?string => $this->translatableStringHelper->localize($reason->getName()), 'group_by' => fn (ActivityReason $reason): ?string => $this->translatableStringHelper->localize($reason->getCategory()->getName()), - 'data' => $this->activityReasonRepository->findAll(), 'multiple' => true, 'expanded' => false, 'label' => 'Activity reasons for those activities', @@ -176,6 +173,10 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt } }); } + public function getFormDefaultData(): array + { + return ['date_from' => new DateTime(), 'date_to' => new DateTime(), 'reasons' => $this->activityReasonRepository->findAll()]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php index b52ef441c..e85b2d247 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php @@ -60,6 +60,10 @@ class UsersJobFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php index 61b12264e..07ff509ce 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php @@ -67,6 +67,10 @@ class UsersScopeFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepository.php new file mode 100644 index 000000000..ce70409ba --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepository.php @@ -0,0 +1,198 @@ +buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext($person, $startDate, $endDate, $content); + + return $this->addFetchQueryByPersonACL($query, $person); + } + + public function buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery + { + $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); + $activityMetadata = $this->em->getClassMetadata(Activity::class); + + $query = new FetchQuery( + PersonActivityGenericDocProvider::KEY, + sprintf('jsonb_build_object(\'id\', stored_obj.%s, \'activity_id\', activity.%s)', $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()), + sprintf('stored_obj.%s', $storedObjectMetadata->getColumnName('createdAt')), + sprintf('%s AS stored_obj', $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName()) + ); + + $query->addJoinClause( + 'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = stored_obj.id' + ); + + $query->addJoinClause( + 'JOIN public.activity activity ON activity.id = activity_doc.activity_id' + ); + + $query->addWhereClause( + sprintf('activity.%s = ?', $activityMetadata->getSingleAssociationJoinColumnName('person')), + [$person->getId()], + [Types::INTEGER] + ); + + return $this->addWhereClauses($query, $startDate, $endDate, $content); + } + + public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery + { + $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); + $activityMetadata = $this->em->getClassMetadata(Activity::class); + + $query = new FetchQuery( + AccompanyingPeriodActivityGenericDocProvider::KEY, + sprintf('jsonb_build_object(\'id\', stored_obj.%s, \'activity_id\', activity.%s)', $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()), + sprintf('stored_obj.%s', $storedObjectMetadata->getColumnName('createdAt')), + sprintf('%s AS stored_obj', $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName()) + ); + + $query->addJoinClause( + 'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = stored_obj.id' + ); + + $query->addJoinClause( + 'JOIN public.activity activity ON activity.id = activity_doc.activity_id' + ); + + // add documents of activities from parcours context + $or = []; + $orParams = []; + $orTypes = []; + foreach ($person->getAccompanyingPeriodParticipations() as $participation) { + if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) { + continue; + } + + $or[] = sprintf( + '(activity.%s = ? AND stored_obj.%s BETWEEN ?::date AND COALESCE(?::date, \'infinity\'::date))', + $activityMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod'), + $storedObjectMetadata->getColumnName('createdAt') + ); + $orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(), + DateTimeImmutable::createFromInterface($participation->getStartDate()), + null === $participation->getEndDate() ? null : DateTimeImmutable::createFromInterface($participation->getEndDate())]; + $orTypes = [...$orTypes, Types::INTEGER, Types::DATE_IMMUTABLE, Types::DATE_IMMUTABLE]; + } + + if ([] === $or) { + $query->addWhereClause('TRUE = FALSE'); + + return $query; + } + + $query->addWhereClause(sprintf('(%s)', implode(' OR ', $or)), $orParams, $orTypes); + + return $this->addWhereClauses($query, $startDate, $endDate, $content); + } + + private function addWhereClauses(FetchQuery $query, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery + { + $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); + + if (null !== $startDate) { + $query->addWhereClause( + sprintf('stored_obj.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')), + [$startDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $endDate) { + $query->addWhereClause( + sprintf('stored_obj.%s < ?', $storedObjectMetadata->getColumnName('createdAt')), + [$endDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $content and '' !== $content) { + $query->addWhereClause( + 'stored_obj.title ilike ?', + ['%' . $content . '%'], + [Types::STRING] + ); + } + + return $query; + } + + private function addFetchQueryByPersonACL(FetchQuery $fetchQuery, Person $person): FetchQuery + { + $activityMetadata = $this->em->getClassMetadata(Activity::class); + + $reachableScopes = []; + + foreach ($this->centerResolverManager->resolveCenters($person) as $center) { + $reachableScopes = [ + ...$reachableScopes, + ...$this->authorizationHelperForCurrentUser->getReachableScopes(ActivityVoter::SEE, $center) + ]; + } + + if ([] === $reachableScopes) { + $fetchQuery->addWhereClause('FALSE = TRUE'); + + return $fetchQuery; + } + + $fetchQuery->addWhereClause( + sprintf( + 'activity.%s IN (%s)', + $activityMetadata->getSingleAssociationJoinColumnName('scope'), + implode(', ', array_fill(0, count($reachableScopes), '?')) + ), + array_map(static fn (Scope $s) => $s->getId(), $reachableScopes), + array_fill(0, count($reachableScopes), Types::INTEGER) + ); + + return $fetchQuery; + } +} diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepositoryInterface.php new file mode 100644 index 000000000..9f4a9c0f8 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepositoryInterface.php @@ -0,0 +1,37 @@ + +
+
+ {% if document.isPending %} +
{{ 'docgen.Doc generation is pending'|trans }}
+ {% elseif document.isFailure %} +
{{ 'docgen.Doc generation failed'|trans }}
+ {% endif %} + +
+ {% if activity.accompanyingPeriod is not null and context == 'person' %} + + {{ activity.accompanyingPeriod.id }} +   + {% endif %} +
+ + + {{ activity.type.name | localize_translatable_string }} + {% if activity.emergency %} + {{ 'Emergency'|trans|upper }} + {% endif %} + +
+
+
+ {{ document.title|chill_print_or_message("No title") }} +
+ {% if document.hasTemplate %} +
+

{{ document.template.name|localize_translatable_string }}

+
+ {% endif %} +
+ +
+
+
+ {{ document.createdAt|format_date('short') }} +
+
+
+
+ + +
+
+ {{ mmm.createdBy(document) }} +
+ + +
+ diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php index 624859eda..a20ca365b 100644 --- a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php +++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php @@ -24,6 +24,9 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Repository\PersonRepository; 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 Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -55,6 +58,10 @@ class ActivityContext implements private TranslatorInterface $translator; + private ThirdPartyRender $thirdPartyRender; + + private ThirdPartyRepository $thirdPartyRepository; + public function __construct( DocumentCategoryRepository $documentCategoryRepository, NormalizerInterface $normalizer, @@ -63,7 +70,9 @@ class ActivityContext implements PersonRenderInterface $personRender, PersonRepository $personRepository, TranslatorInterface $translator, - BaseContextData $baseContextData + BaseContextData $baseContextData, + ThirdPartyRender $thirdPartyRender, + ThirdPartyRepository $thirdPartyRepository ) { $this->documentCategoryRepository = $documentCategoryRepository; $this->normalizer = $normalizer; @@ -73,6 +82,8 @@ class ActivityContext implements $this->personRepository = $personRepository; $this->translator = $translator; $this->baseContextData = $baseContextData; + $this->thirdPartyRender = $thirdPartyRender; + $this->thirdPartyRepository = $thirdPartyRepository; } public function adminFormReverseTransform(array $data): array @@ -89,6 +100,8 @@ class ActivityContext implements 'person1Label' => $data['person1Label'] ?? $this->translator->trans('docgen.person 1'), 'person2' => $data['person2'] ?? false, '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, [ 'label' => 'person 2 label', '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 @@ -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; } @@ -165,9 +206,11 @@ class ActivityContext implements $normalized = []; 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; } @@ -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; } @@ -235,7 +285,7 @@ class ActivityContext implements { $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 diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php index 3da451f2e..045d09beb 100644 --- a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php +++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php @@ -140,26 +140,36 @@ class ListActivitiesByAccompanyingPeriodContext implements return $normalized; } + /** + * @return list + */ private function filterActivitiesByUser(array $activities, User $user): array { - return array_filter( - $activities, - function ($activity) use ($user) { - $activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []); - return in_array($user->getUsername(), $activityUsernames, true); - } + return array_values( + array_filter( + $activities, + function ($activity) use ($user) { + $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 { - return array_filter( - $works, - function ($work) use ($user) { - $workUsernames = array_map(static fn ($user) => $user['username'], $work['referrers'] ?? []); + return array_values( + array_filter( + $works, + 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); + } + ) ); } diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php new file mode 100644 index 000000000..334b5d2df --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php @@ -0,0 +1,114 @@ +em->getClassMetadata(StoredObject::class); + $activityMetadata = $this->em->getClassMetadata(Activity::class); + + $query = new FetchQuery( + self::KEY, + sprintf("jsonb_build_object('id', doc_obj.%s, 'activity_id', activity.%s)", $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()), + 'doc_obj.'.$storedObjectMetadata->getColumnName('createdAt'), + $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName().' AS doc_obj' + ); + + $query->addJoinClause( + 'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = doc_obj.id' + ); + + $query->addJoinClause( + 'JOIN public.activity activity ON activity.id = activity_doc.activity_id' + ); + + $query->addWhereClause( + 'activity.accompanyingperiod_id = ?', + [$accompanyingPeriod->getId()], + [Types::INTEGER] + ); + + if (null !== $startDate) { + $query->addWhereClause( + sprintf('doc_obj.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')), + [$startDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $endDate) { + $query->addWhereClause( + sprintf('doc_obj.%s < ?', $storedObjectMetadata->getColumnName('createdAt')), + [$endDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $content) { + $query->addWhereClause( + 'doc_obj.title ilike ?', + ['%' . $content . '%'], + [Types::STRING] + ); + } + + return $query; + } + + /** + * @param AccompanyingPeriod $accompanyingPeriod + * @return bool + */ + public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool + { + return $this->security->isGranted(ActivityVoter::SEE, $accompanyingPeriod); + } + + public function isAllowedForPerson(Person $person): bool + { + return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person); + } + + public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface + { + return $this->activityDocumentACLAwareRepository + ->buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext($person, $startDate, $endDate, $content); + } +} diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/PersonActivityGenericDocProvider.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/PersonActivityGenericDocProvider.php new file mode 100644 index 000000000..cf96449ab --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/PersonActivityGenericDocProvider.php @@ -0,0 +1,53 @@ +personActivityDocumentACLAwareRepository->buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext( + $person, + $startDate, + $endDate, + $content + ); + } + + /** + * @param Person $person + * @return bool + */ + public function isAllowedForPerson(Person $person): bool + { + return $this->security->isGranted(ActivityVoter::SEE, $person); + } +} diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php new file mode 100644 index 000000000..c465dbca1 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php @@ -0,0 +1,52 @@ +objectRepository = $storedObjectRepository; + $this->activityRepository = $activityRepository; + } + + public function supports(GenericDocDTO $genericDocDTO, $options = []): bool + { + return $genericDocDTO->key === AccompanyingPeriodActivityGenericDocProvider::KEY || $genericDocDTO->key === PersonActivityGenericDocProvider::KEY; + } + + public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string + { + return '@ChillActivity/GenericDoc/activity_document.html.twig'; + } + + public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array + { + return [ + 'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']), + 'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']), + 'context' => $genericDocDTO->getContext(), + ]; + } +} diff --git a/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityDocumentACLAwareRepositoryTest.php b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityDocumentACLAwareRepositoryTest.php new file mode 100644 index 000000000..ce4f318e3 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityDocumentACLAwareRepositoryTest.php @@ -0,0 +1,126 @@ +entityManager = self::$container->get(EntityManagerInterface::class); + $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class); + $this->authorizationHelperForCurrentUser = self::$container->get(AuthorizationHelperForCurrentUserInterface::class); + $this->security = self::$container->get(Security::class); + } + + /** + * @dataProvider provideDataForPerson + */ + public function testBuildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, array $reachableScopes, bool $_unused, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content): void + { + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $authorizationHelper->getReachableScopes(ActivityVoter::SEE, Argument::any()) + ->willReturn($reachableScopes); + + $repository = new ActivityDocumentACLAwareRepository( + $this->entityManager, + $this->centerResolverManager, + $authorizationHelper->reveal(), + $this->security + ); + + $query = $repository->buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext($person, $startDate, $endDate, $content); + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $nb = $this->entityManager->getConnection()->fetchOne("SELECT COUNT(*) FROM ({$sql}) sq", $params, $types); + + self::assertIsInt($nb); + } + + /** + * @dataProvider provideDataForPerson + */ + public function testBuildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, array $_unused, bool $canSeePeriod, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content): void + { + $security = $this->prophesize(Security::class); + $security->isGranted(ActivityVoter::SEE, Argument::type(AccompanyingPeriod::class)) + ->willReturn($canSeePeriod); + + $repository = new ActivityDocumentACLAwareRepository( + $this->entityManager, + $this->centerResolverManager, + $this->authorizationHelperForCurrentUser, + $security->reveal() + ); + + $query = $repository->buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext($person, $startDate, $endDate, $content); + + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $nb = $this->entityManager->getConnection()->fetchOne("SELECT COUNT(*) FROM ({$sql}) sq", $params, $types); + + self::assertIsInt($nb); + } + + public function provideDataForPerson(): iterable + { + $this->setUp(); + + if (null === $person = $this->entityManager->createQuery("SELECT p FROM " . Person::class . " p WHERE SIZE(p.accompanyingPeriodParticipations) > 0 ") + ->setMaxResults(1) + ->getSingleResult()) { + throw new \RuntimeException("no person in dtabase"); + } + + if ([] === $scopes = $this->entityManager->createQuery("SELECT s FROM " . Scope::class . " s ")->setMaxResults(5)->getResult()) { + throw new \RuntimeException("no scopes in database"); + } + + yield [$person, [], true, null, null, null]; + yield [$person, $scopes, true, null, null, null]; + yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), null, null]; + yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), null]; + yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), "content"]; + yield [$person, $scopes, true, null, new \DateTimeImmutable("1 week ago"), "content"]; + yield [$person, [], true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), "content"]; + } + +} diff --git a/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php b/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php index dad597676..2c9272b08 100644 --- a/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php +++ b/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php @@ -161,6 +161,7 @@ class TimelineActivityProvider implements TimelineProviderInterface // loop on reachable scopes foreach ($reachableScopes as $scope) { + /** @phpstan-ignore-next-line */ if (in_array($scope->getId(), $scopes_ids, true)) { continue; } diff --git a/src/Bundle/ChillActivityBundle/config/services.yaml b/src/Bundle/ChillActivityBundle/config/services.yaml index d55f86d4f..18be76ec9 100644 --- a/src/Bundle/ChillActivityBundle/config/services.yaml +++ b/src/Bundle/ChillActivityBundle/config/services.yaml @@ -38,3 +38,6 @@ services: Chill\ActivityBundle\Service\EntityInfo\: resource: '../Service/EntityInfo/' + + Chill\ActivityBundle\Service\GenericDoc\: + resource: '../Service/GenericDoc/' diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index e08e1fb62..55d755fd6 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -373,3 +373,8 @@ export: is sent: envoyé is received: reçu Group activity by sentreceived: Grouper les échanges par envoyé / reçu + +generic_doc: + filter: + keys: + accompanying_period_activity_document: Document des échanges des parcours diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php index 32418e3c3..8ae43534d 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php @@ -50,6 +50,10 @@ class ByActivityTypeAggregator implements AggregatorInterface { // No form needed } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php index d2fe7c2f2..f09a704e2 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php @@ -57,6 +57,10 @@ class ByUserJobAggregator implements AggregatorInterface { // nothing to add in the form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php index 6c06ee756..3dd465db2 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php @@ -57,6 +57,10 @@ class ByUserScopeAggregator implements AggregatorInterface { // nothing to add in the form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php index f3db629cb..2b28062f6 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php @@ -34,6 +34,10 @@ class AvgAsideActivityDuration implements ExportInterface, GroupedExportInterfac public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php index 9204fad4b..91210f764 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php @@ -34,6 +34,10 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php index aee168174..b46e120b5 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php @@ -73,6 +73,10 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php index af17a2591..741f129f1 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php @@ -34,6 +34,10 @@ class SumAsideActivityDuration implements ExportInterface, GroupedExportInterfac public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php index 3ad8e0e93..3d389f5b3 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php @@ -76,6 +76,10 @@ class ByActivityTypeFilter implements FilterInterface }, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php index 7a1b6f4dc..98a254bc4 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php @@ -72,11 +72,9 @@ class ByDateFilter implements FilterInterface $builder ->add('date_from', PickRollingDateType::class, [ 'label' => 'export.filter.Aside activities after this date', - 'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), ]) ->add('date_to', PickRollingDateType::class, [ 'label' => 'export.filter.Aside activities before this date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) { @@ -119,6 +117,10 @@ class ByDateFilter implements FilterInterface } }); } + public function getFormDefaultData(): array + { + return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php index 795c813cd..2858d3417 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php @@ -53,6 +53,10 @@ class ByUserFilter implements FilterInterface 'label' => 'Creators', ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php index 86194b123..55f477768 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php @@ -60,6 +60,10 @@ class ByUserJobFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php index 4342e11eb..8fa51866d 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php @@ -67,6 +67,10 @@ class ByUserScopeFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml b/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml index cc428390c..25d07bd22 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml +++ b/src/Bundle/ChillAsideActivityBundle/src/translations/messages.fr.yml @@ -176,11 +176,12 @@ export: agent_id: Utilisateur creator_id: Créateur main_scope: Service principal de l'utilisateur - main_center: Centre principal de l'utilisteur + main_center: Centre principal de l'utilisateur aside_activity_type: Catégorie d'activité annexe date: Date duration: Durée note: Note + id: Identifiant Exports of aside activities: Exports des activités annexes Count aside activities: Nombre d'activités annexes diff --git a/src/Bundle/ChillBudgetBundle/Calculator/CalculatorResult.php b/src/Bundle/ChillBudgetBundle/Calculator/CalculatorResult.php index 380e641a6..2a67486f0 100644 --- a/src/Bundle/ChillBudgetBundle/Calculator/CalculatorResult.php +++ b/src/Bundle/ChillBudgetBundle/Calculator/CalculatorResult.php @@ -21,7 +21,7 @@ class CalculatorResult public $label; - public $result; + public float $result; public $type; } diff --git a/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php b/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php index b90fb83d4..193b04934 100644 --- a/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php +++ b/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php @@ -18,9 +18,12 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Command; +use Chill\CalendarBundle\Exception\UserAbsenceSyncException; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\EventsOnUserSubscriptionCreator; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSGraphUserRepository; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync; +use Chill\MainBundle\Repository\UserRepositoryInterface; use DateInterval; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; @@ -30,32 +33,17 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class MapAndSubscribeUserCalendarCommand extends Command +final class MapAndSubscribeUserCalendarCommand extends Command { - private EntityManagerInterface $em; - - private EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator; - - private LoggerInterface $logger; - - private MapCalendarToUser $mapCalendarToUser; - - private MSGraphUserRepository $userRepository; - public function __construct( - EntityManagerInterface $em, - EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator, - LoggerInterface $logger, - MapCalendarToUser $mapCalendarToUser, - MSGraphUserRepository $userRepository + private readonly EntityManagerInterface $em, + private readonly EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator, + private readonly LoggerInterface $logger, + private readonly MapCalendarToUser $mapCalendarToUser, + private readonly UserRepositoryInterface $userRepository, + private readonly MSUserAbsenceSync $userAbsenceSync, ) { parent::__construct('chill:calendar:msgraph-user-map-subscribe'); - - $this->em = $em; - $this->eventsOnUserSubscriptionCreator = $eventsOnUserSubscriptionCreator; - $this->logger = $logger; - $this->mapCalendarToUser = $mapCalendarToUser; - $this->userRepository = $userRepository; } public function execute(InputInterface $input, OutputInterface $output): int @@ -67,83 +55,109 @@ class MapAndSubscribeUserCalendarCommand extends Command /** @var DateInterval $interval the interval before the end of the expiration */ $interval = new DateInterval('P1D'); $expiration = (new DateTimeImmutable('now'))->add(new DateInterval($input->getOption('subscription-duration'))); - $total = $this->userRepository->countByMostOldSubscriptionOrWithoutSubscriptionOrData($interval); + $users = $this->userRepository->findAllAsArray('fr'); $created = 0; $renewed = 0; - $this->logger->info(self::class . ' the number of user to get - renew', [ - 'total' => $total, + $this->logger->info(self::class . ' start user to get - renew', [ 'expiration' => $expiration->format(DateTimeImmutable::ATOM), ]); - while ($offset < $total) { - $users = $this->userRepository->findByMostOldSubscriptionOrWithoutSubscriptionOrData( - $interval, - $limit, - $offset - ); + foreach ($users as $u) { + ++$offset; - foreach ($users as $user) { - if (!$this->mapCalendarToUser->hasUserId($user)) { - $this->mapCalendarToUser->writeMetadata($user); - } - - if ($this->mapCalendarToUser->hasUserId($user)) { - // we first try to renew an existing subscription, if any. - // if not, or if it fails, we try to create a new one - if ($this->mapCalendarToUser->hasActiveSubscription($user)) { - $this->logger->debug(self::class . ' renew a subscription for', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - - ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] - = $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration); - $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); - - if (0 !== $expirationTs) { - ++$renewed; - } else { - $this->logger->warning(self::class . ' could not renew subscription for a user', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - } - } - - if (!$this->mapCalendarToUser->hasActiveSubscription($user)) { - $this->logger->debug(self::class . ' create a subscription for', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - - ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] - = $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration); - $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); - - if (0 !== $expirationTs) { - ++$created; - } else { - $this->logger->warning(self::class . ' could not create subscription for a user', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - } - } - } - - ++$offset; + if (false === $u['enabled']) { + continue; } - $this->em->flush(); - $this->em->clear(); + $user = $this->userRepository->find($u['id']); + + if (null === $user) { + $this->logger->error("could not find user by id", ['uid' => $u['id']]); + $output->writeln("could not find user by id : " . $u['id']); + continue; + } + + if (!$this->mapCalendarToUser->hasUserId($user)) { + $user = $this->mapCalendarToUser->writeMetadata($user); + + // if user still does not have userid, continue + if (!$this->mapCalendarToUser->hasUserId($user)) { + $this->logger->warning("user does not have a counterpart on ms api", ['userId' => $user->getId(), 'email' => $user->getEmail()]); + $output->writeln(sprintf("giving up for user with email %s and id %s", $user->getEmail(), $user->getId())); + + continue; + } + } + + // sync user absence + try { + $this->userAbsenceSync->syncUserAbsence($user); + } catch (UserAbsenceSyncException $e) { + $this->logger->error("could not sync user absence", ['userId' => $user->getId(), 'email' => $user->getEmail(), 'exception' => $e->getTraceAsString(), "message" => $e->getMessage()]); + $output->writeln(sprintf("Could not sync user absence: id: %s and email: %s", $user->getId(), $user->getEmail())); + throw $e; + } + + // we first try to renew an existing subscription, if any. + // if not, or if it fails, we try to create a new one + if ($this->mapCalendarToUser->hasActiveSubscription($user)) { + $this->logger->debug(self::class . ' renew a subscription for', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + + ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] + = $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration); + $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); + + if (0 !== $expirationTs) { + ++$renewed; + } else { + $this->logger->warning(self::class . ' could not renew subscription for a user', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + } + } + + if (!$this->mapCalendarToUser->hasActiveSubscription($user)) { + $this->logger->debug(self::class . ' create a subscription for', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + + ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] + = $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration); + $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); + + if (0 !== $expirationTs) { + ++$created; + } else { + $this->logger->warning(self::class . ' could not create subscription for a user', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + } + } + + + if (0 === $offset % $limit) { + $this->em->flush(); + $this->em->clear(); + } } + $this->em->flush(); + $this->em->clear(); + $this->logger->warning(self::class . ' process executed', [ 'created' => $created, 'renewed' => $renewed, ]); + $output->writeln("users synchronized"); + return 0; } @@ -152,7 +166,7 @@ class MapAndSubscribeUserCalendarCommand extends Command parent::configure(); $this - ->setDescription('MSGraph: collect user metadata and create subscription on events for users') + ->setDescription('MSGraph: collect user metadata and create subscription on events for users, and sync the user absence-presence') ->addOption( 'renew-before-end-interval', 'r', diff --git a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php new file mode 100644 index 000000000..a5e5a679a --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php @@ -0,0 +1,20 @@ + true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php index 59019ac03..4260ecd99 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php @@ -60,12 +60,12 @@ class BetweenDatesFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { $builder - ->add('date_from', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), - ]) - ->add('date_to', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), - ]); + ->add('date_from', PickRollingDateType::class, []) + ->add('date_to', PickRollingDateType::class, []); + } + public function getFormDefaultData(): array + { + return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; } public function describeAction($data, $format = 'string'): array diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php index d6c38e163..5ffb7c01f 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php @@ -69,9 +69,12 @@ class CalendarRangeFilter implements FilterInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['hasCalendarRange' => self::DEFAULT_CHOICE]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php index 69fb24447..d02bb8e9d 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php @@ -76,6 +76,10 @@ class JobFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php index 08d9ae023..d8a40da72 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php @@ -76,6 +76,10 @@ class ScopeFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php deleted file mode 100644 index c523a1e92..000000000 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php +++ /dev/null @@ -1,84 +0,0 @@ -'msgraph' ?? 'subscription_events_expiration' - OR (attributes->'msgraph' ?? 'subscription_events_expiration' AND (attributes->'msgraph'->>'subscription_events_expiration')::int < EXTRACT(EPOCH FROM (NOW() + :interval::interval))) - LIMIT :limit OFFSET :offset - ; - SQL; - - private EntityManagerInterface $entityManager; - - public function __construct(EntityManagerInterface $entityManager) - { - $this->entityManager = $entityManager; - } - - public function countByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval): int - { - $rsm = new ResultSetMapping(); - $rsm->addScalarResult('c', 'c'); - - $sql = strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, [ - '{select}' => 'COUNT(u) AS c', - 'LIMIT :limit OFFSET :offset' => '', - ]); - - return $this->entityManager->createNativeQuery($sql, $rsm)->setParameters([ - 'interval' => $interval, - ])->getSingleScalarResult(); - } - - /** - * @return array|User[] - */ - public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array - { - $rsm = new ResultSetMappingBuilder($this->entityManager); - $rsm->addRootEntityFromClassMetadata(User::class, 'u'); - - return $this->entityManager->createNativeQuery( - strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, ['{select}' => $rsm->generateSelectClause()]), - $rsm - )->setParameters([ - 'interval' => $interval, - 'limit' => $limit, - 'offset' => $offset, - ])->getResult(); - } -} diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php new file mode 100644 index 000000000..c70072a47 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -0,0 +1,69 @@ +mapCalendarToUser->getUserId($user); + + if (null === $id) { + return null; + } + + try { + $automaticRepliesSettings = $this->machineHttpClient + ->request('GET', 'users/' . $id . '/mailboxSettings/automaticRepliesSetting') + ->toArray(true); + } catch (ClientExceptionInterface|DecodingExceptionInterface|RedirectionExceptionInterface|TransportExceptionInterface $e) { + throw new UserAbsenceSyncException("Error receiving response for mailboxSettings", 0, $e); + } catch (ServerExceptionInterface $e) { + throw new UserAbsenceSyncException("Server error receiving response for mailboxSettings", 0, $e); + } + + if (!array_key_exists("status", $automaticRepliesSettings)) { + throw new \LogicException("no key \"status\" on automatic replies settings: " . json_encode($automaticRepliesSettings, JSON_THROW_ON_ERROR)); + } + + return match ($automaticRepliesSettings['status']) { + 'disabled' => false, + 'alwaysEnabled' => true, + 'scheduled' => + RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledStartDateTime']['dateTime']) < $this->clock->now() + && RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledEndDateTime']['dateTime']) > $this->clock->now(), + default => throw new UserAbsenceSyncException("this status is not documented by Microsoft") + }; + } + +} diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php new file mode 100644 index 000000000..a918bb7ea --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php @@ -0,0 +1,22 @@ +absenceReader->isUserAbsent($user); + + if (null === $absence) { + return; + } + + if ($absence === $user->isAbsent()) { + // nothing to do + return; + } + + $this->logger->info("will change user absence", ['userId' => $user->getId()]); + + if ($absence) { + $this->logger->debug("make user absent", ['userId' => $user->getId()]); + $user->setAbsenceStart($this->clock->now()); + } else { + $this->logger->debug("make user present", ['userId' => $user->getId()]); + $user->setAbsenceStart(null); + } + } +} diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php index f56735de7..6a986c9db 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php @@ -23,6 +23,8 @@ use Chill\CalendarBundle\Command\MapAndSubscribeUserCalendarCommand; use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceReaderInterface; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector; use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector; use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface; @@ -37,17 +39,13 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container) { $config = $container->getParameter('chill_calendar'); - $connector = null; - if (!$config['remote_calendars_sync']['enabled']) { - $connector = NullRemoteCalendarConnector::class; - } - - if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) { + if (true === $config['remote_calendars_sync']['microsoft_graph']['enabled']) { $connector = MSGraphRemoteCalendarConnector::class; $container->setAlias(HttpClientInterface::class . ' $machineHttpClient', MachineHttpClient::class); } else { + $connector = NullRemoteCalendarConnector::class; // remove services which cannot be loaded $container->removeDefinition(MapAndSubscribeUserCalendarCommand::class); $container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class); @@ -55,16 +53,14 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface $container->removeDefinition(MachineTokenStorage::class); $container->removeDefinition(MachineHttpClient::class); $container->removeDefinition(MSGraphRemoteCalendarConnector::class); + $container->removeDefinition(MSUserAbsenceReaderInterface::class); + $container->removeDefinition(MSUserAbsenceSync::class); } if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) { $container->setAlias(Azure::class, 'knpu.oauth2.provider.azure'); } - if (null === $connector) { - throw new RuntimeException('Could not configure remote calendar'); - } - foreach ([ NullRemoteCalendarConnector::class, MSGraphRemoteCalendarConnector::class, ] as $serviceId) { diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/chill/chill.js b/src/Bundle/ChillCalendarBundle/Resources/public/chill/chill.js new file mode 100644 index 000000000..56a8ce563 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/public/chill/chill.js @@ -0,0 +1 @@ +import './scss/badge.scss'; diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/badge.scss b/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/badge.scss new file mode 100644 index 000000000..ffcda8f0f --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/badge.scss @@ -0,0 +1,25 @@ +@import '~ChillPersonAssets/chill/scss/mixins.scss'; +@import '~ChillMainAssets/module/bootstrap/shared'; + +.badge-calendar { + display: inline-block; + background-color: #f3f3f3; + + .title_label { + @include chill_badge($chill-l-gray); + } + + .title_action { + padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x); + margin-right: 1rem; + + font-size: var(--bs-badge-font-size); + font-weight: var(--bs-badge-font-weight); + line-height: 1; + color: var(--bs-badge-color); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + } +} + diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/calendar.scss b/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/calendar.scss index a2c0c4b89..ce54b0fa8 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/calendar.scss +++ b/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/calendar.scss @@ -17,4 +17,4 @@ span.calendarRangeItems { text-decoration: none; padding: 3px; } -} \ No newline at end of file +} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig new file mode 100644 index 000000000..facf5be50 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig @@ -0,0 +1,75 @@ +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} +{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} +{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} + +{% set c = document.calendar %} + +
+
+
+ {% if document.storedObject.isPending %} +
{{ 'docgen.Doc generation is pending'|trans }}
+ {% elseif document.storedObject.isFailure %} +
{{ 'docgen.Doc generation failed'|trans }}
+ {% endif %} + +
+ {% if c.accompanyingPeriod is not null and context == 'person' %} + + {{ c.accompanyingPeriod.id }} +   + {% endif %} + + + + + {{ 'Calendar'|trans }} + {% if c.endDate.diff(c.startDate).days >= 1 %} + {{ c.startDate|format_datetime('short', 'short') }} + - {{ c.endDate|format_datetime('short', 'short') }} + {% else %} + {{ c.startDate|format_datetime('short', 'short') }} + - {{ c.endDate|format_datetime('none', 'short') }} + {% endif %} + + +
+ +
+ {{ document.storedObject.title|chill_print_or_message("No title") }} +
+ {% if document.storedObject.hasTemplate %} +
+

{{ document.storedObject.template.name|localize_translatable_string }}

+
+ {% endif %} +
+ +
+
+
+ {{ document.storedObject.createdAt|format_date('short') }} +
+
+
+
+ +
+
+ {{ mmm.createdBy(document) }} +
+ + +
+
diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php new file mode 100644 index 000000000..c33ccd853 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php @@ -0,0 +1,192 @@ +em->getClassMetadata(CalendarDoc::class); + $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); + $calendarMetadata = $this->em->getClassMetadata(Calendar::class); + + $query = new FetchQuery( + self::KEY, + sprintf("jsonb_build_object('id', cd.%s)", $classMetadata->getColumnName('id')), + 'cd.'.$storedObjectMetadata->getColumnName('createdAt'), + $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS cd' + ); + $query->addJoinClause( + sprintf( + 'JOIN %s doc_store ON doc_store.%s = cd.%s', + $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(), + $storedObjectMetadata->getColumnName('id'), + $classMetadata->getSingleAssociationJoinColumnName('storedObject') + ) + ); + + $query->addJoinClause( + sprintf( + 'JOIN %s calendar ON calendar.%s = cd.%s', + $calendarMetadata->getSchemaName().'.'.$calendarMetadata->getTableName(), + $calendarMetadata->getColumnName('id'), + $classMetadata->getSingleAssociationJoinColumnName('calendar') + ) + ); + + $query->addWhereClause( + sprintf( + 'calendar.%s = ?', + $calendarMetadata->getAssociationMapping('accompanyingPeriod')['joinColumns'][0]['name'] + ), + [$accompanyingPeriod->getId()], + [Types::INTEGER] + ); + + return $query; + } + + public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool + { + return $this->security->isGranted(CalendarVoter::SEE, $accompanyingPeriod); + } + + public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface + { + $classMetadata = $this->em->getClassMetadata(CalendarDoc::class); + $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); + $calendarMetadata = $this->em->getClassMetadata(Calendar::class); + + $query = new FetchQuery( + self::KEY, + sprintf("jsonb_build_object('id', cd.%s)", $classMetadata->getColumnName('id')), + 'cd.'.$storedObjectMetadata->getColumnName('createdAt'), + $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS cd' + ); + $query->addJoinClause( + sprintf( + 'JOIN %s doc_store ON doc_store.%s = cd.%s', + $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(), + $storedObjectMetadata->getColumnName('id'), + $classMetadata->getSingleAssociationJoinColumnName('storedObject') + ) + ); + + $query->addJoinClause( + sprintf( + 'JOIN %s calendar ON calendar.%s = cd.%s', + $calendarMetadata->getSchemaName().'.'.$calendarMetadata->getTableName(), + $calendarMetadata->getColumnName('id'), + $classMetadata->getSingleAssociationJoinColumnName('calendar') + ) + ); + + // get the documents associated with accompanying periods in which person participates + $or = []; + $orParams = []; + $orTypes = []; + foreach ($person->getAccompanyingPeriodParticipations() as $participation) { + if (!$this->security->isGranted(CalendarVoter::SEE, $participation->getAccompanyingPeriod())) { + continue; + } + + $or[] = sprintf( + '(calendar.%s = ? AND cd.%s BETWEEN ?::date AND COALESCE(?::date, \'infinity\'::date))', + $calendarMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod'), + $storedObjectMetadata->getColumnName('createdAt') + ); + $orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(), + DateTimeImmutable::createFromInterface($participation->getStartDate()), + null === $participation->getEndDate() ? null : DateTimeImmutable::createFromInterface($participation->getEndDate())]; + $orTypes = [...$orTypes, Types::INTEGER, Types::DATE_IMMUTABLE, Types::DATE_IMMUTABLE]; + } + + if ([] === $or) { + $query->addWhereClause('TRUE = FALSE'); + + return $query; + } + return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content); + } + + public function isAllowedForPerson(Person $person): bool + { + // check that the person is allowed to see an accompanying period. If yes, the + // ACL on each accompanying period will be checked when the query is build + return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person); + } + + private function addWhereClausesToQuery(FetchQuery $query, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?string $content): FetchQuery + { + $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); + + if (null !== $startDate) { + $query->addWhereClause( + sprintf('doc_store.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')), + [$startDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $endDate) { + $query->addWhereClause( + sprintf('doc_store.%s < ?', $storedObjectMetadata->getColumnName('createdAt')), + [$endDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $content) { + $query->addWhereClause( + sprintf('doc_store.%s ilike ?', $storedObjectMetadata->getColumnName('title')), + ['%' . $content . '%'], + [Types::STRING] + ); + } + + return $query; + } + + +} diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/PersonCalendarGenericDocProvider.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/PersonCalendarGenericDocProvider.php new file mode 100644 index 000000000..f5d4b3cbb --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/PersonCalendarGenericDocProvider.php @@ -0,0 +1,126 @@ +em->getClassMetadata(StoredObject::class); + + if (null !== $startDate) { + $query->addWhereClause( + sprintf('doc_store.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')), + [$startDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $endDate) { + $query->addWhereClause( + sprintf('doc_store.%s < ?', $storedObjectMetadata->getColumnName('createdAt')), + [$endDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $content) { + $query->addWhereClause( + sprintf('doc_store.%s ilike ?', $storedObjectMetadata->getColumnName('title')), + ['%' . $content . '%'], + [Types::STRING] + ); + } + + return $query; + } + + /** + * @throws MappingException + */ + public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface + { + $classMetadata = $this->em->getClassMetadata(CalendarDoc::class); + $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); + $calendarMetadata = $this->em->getClassMetadata(Calendar::class); + + $query = new FetchQuery( + self::KEY, + sprintf("jsonb_build_object('id', cd.%s)", $classMetadata->getColumnName('id')), + 'cd.'.$storedObjectMetadata->getColumnName('createdAt'), + $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS cd' + ); + $query->addJoinClause( + sprintf( + 'JOIN %s doc_store ON doc_store.%s = cd.%s', + $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(), + $storedObjectMetadata->getColumnName('id'), + $classMetadata->getSingleAssociationJoinColumnName('storedObject') + ) + ); + + $query->addJoinClause( + sprintf( + 'JOIN %s calendar ON calendar.%s = cd.%s', + $calendarMetadata->getSchemaName().'.'.$calendarMetadata->getTableName(), + $calendarMetadata->getColumnName('id'), + $classMetadata->getSingleAssociationJoinColumnName('calendar') + ) + ); + + $query->addWhereClause( + sprintf('calendar.%s = ?', $calendarMetadata->getSingleAssociationJoinColumnName('person')), + [$person->getId()], + [Types::INTEGER] + ); + + return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content); + } + + /** + * @param Person $person + * @return bool + */ + public function isAllowedForPerson(Person $person): bool + { + return $this->security->isGranted(CalendarVoter::SEE, $person); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php new file mode 100644 index 000000000..b15c091c6 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php @@ -0,0 +1,46 @@ +repository = $calendarDocRepository; + } + + public function supports(GenericDocDTO $genericDocDTO, $options = []): bool + { + return $genericDocDTO->key === AccompanyingPeriodCalendarGenericDocProvider::KEY || $genericDocDTO->key === PersonCalendarGenericDocProvider::KEY; + } + + public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string + { + return '@ChillCalendar/GenericDoc/calendar_document.html.twig'; + } + + public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array + { + return [ + 'document' => $this->repository->find($genericDocDTO->identifiers['id']), + 'context' => $genericDocDTO->getContext(), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php new file mode 100644 index 000000000..089477fda --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php @@ -0,0 +1,176 @@ +prophesize(MapCalendarToUser::class); + $mapUser->getUserId($user)->willReturn('1234'); + $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00')); + + $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock); + + self::assertEquals($expected, $absenceReader->isUserAbsent($user), $message); + } + + public function testIsUserAbsentWithoutRemoteId(): void + { + $user = new User(); + $client = new MockHttpClient(); + + $mapUser = $this->prophesize(MapCalendarToUser::class); + $mapUser->getUserId($user)->willReturn(null); + $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00')); + + $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock); + + self::assertNull($absenceReader->isUserAbsent($user), "when no user found, absence should be null"); + } + + public function provideDataTestUserAbsence(): iterable + { + // contains data that was retrieved from microsoft graph api on 2023-07-06 + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "disabled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T12:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-07T12:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + "User is present" + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-21T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + true, + 'User is absent with absence scheduled, we are within this period' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-08T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-21T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + 'User is present: absence is scheduled for later' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-05T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-06T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + 'User is present: absence is past' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "alwaysEnabled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T12:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-07T12:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + true, + "User is absent: absence is always enabled" + ]; + } + +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php new file mode 100644 index 000000000..1b0f1e416 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php @@ -0,0 +1,68 @@ +prophesize(MSUserAbsenceReaderInterface::class); + $userAbsenceReader->isUserAbsent($user)->willReturn($absenceFromMicrosoft); + + $clock = new MockClock(new \DateTimeImmutable('2023-07-01T12:00:00')); + + $syncer = new MSUserAbsenceSync($userAbsenceReader->reveal(), $clock, new NullLogger()); + + $syncer->syncUserAbsence($user); + + self::assertEquals($expectedAbsence, $user->isAbsent(), $message); + self::assertEquals($expectedAbsenceStart, $user->getAbsenceStart(), $message); + } + + public function provideDataTestSyncUserAbsence(): iterable + { + yield [new User(), false, false, null, "user present remains present"]; + yield [new User(), true, true, new \DateTimeImmutable('2023-07-01T12:00:00'), "user present becomes absent"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, true, true, $abs, "user absent remains absent"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, false, false, null, "user absent becomes present"]; + + yield [new User(), null, false, null, "user not syncable: presence do not change"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, null, true, $abs, "user not syncable: absence do not change"]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/chill.webpack.config.js b/src/Bundle/ChillCalendarBundle/chill.webpack.config.js index e82210087..9d45a3142 100644 --- a/src/Bundle/ChillCalendarBundle/chill.webpack.config.js +++ b/src/Bundle/ChillCalendarBundle/chill.webpack.config.js @@ -1,6 +1,8 @@ // this file loads all assets from the Chill calendar bundle module.exports = function(encore, entries) { + entries.push(__dirname + '/Resources/public/chill/chill.js'); + encore.addAliases({ ChillCalendarAssets: __dirname + '/Resources/public' }); diff --git a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml index a1228c381..95faec742 100644 --- a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml @@ -44,6 +44,7 @@ crud: title_edit: Modifier le motif d'annulation chill_calendar: + Document: Document d'un rendez-vous form: The main user is mandatory. He will organize the appointment.: L'utilisateur principal est obligatoire. Il est l'organisateur de l'événement. Create for referrer: Créer pour le référent @@ -66,6 +67,7 @@ chill_calendar: Document outdated: La date et l'heure du rendez-vous ont été modifiés après la création du document + remote_ms_graph: freebusy_statuses: busy: Occupé @@ -146,3 +148,9 @@ CHILL_CALENDAR_CALENDAR_EDIT: Modifier les rendez-vous CHILL_CALENDAR_CALENDAR_DELETE: Supprimer les rendez-vous CHILL_CALENDAR_CALENDAR_SEE: Voir les rendez-vous + +generic_doc: + filter: + keys: + accompanying_period_calendar_document: Document des rendez-vous des parcours + person_calendar_document: Document des rendez-vous de l'usager diff --git a/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php b/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php index 81c71f45f..8dcbe72c3 100644 --- a/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php +++ b/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php @@ -11,8 +11,21 @@ declare(strict_types=1); namespace Chill\DocStoreBundle; +use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; +use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class ChillDocStoreBundle extends Bundle { + public function build(ContainerBuilder $container) + { + $container->registerForAutoconfiguration(GenericDocForAccompanyingPeriodProviderInterface::class) + ->addTag('chill_doc_store.generic_doc_accompanying_period_provider'); + $container->registerForAutoconfiguration(GenericDocForPersonProviderInterface::class) + ->addTag('chill_doc_store.generic_doc_person_provider'); + $container->registerForAutoconfiguration(GenericDocRendererInterface::class) + ->addTag('chill_doc_store.generic_doc_renderer'); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php b/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php index 1bc3db221..384eeb510 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php @@ -39,10 +39,6 @@ class DocumentAccompanyingCourseController extends AbstractController protected TranslatorInterface $translator; - private AccompanyingCourseDocumentRepository $courseRepository; - - private PaginatorFactory $paginatorFactory; - /** * DocumentAccompanyingCourseController constructor. */ @@ -50,14 +46,10 @@ class DocumentAccompanyingCourseController extends AbstractController TranslatorInterface $translator, EventDispatcherInterface $eventDispatcher, AuthorizationHelper $authorizationHelper, - PaginatorFactory $paginatorFactory, - AccompanyingCourseDocumentRepository $courseRepository ) { $this->translator = $translator; $this->eventDispatcher = $eventDispatcher; $this->authorizationHelper = $authorizationHelper; - $this->paginatorFactory = $paginatorFactory; - $this->courseRepository = $courseRepository; } /** @@ -82,7 +74,7 @@ class DocumentAccompanyingCourseController extends AbstractController return $this->redirect($request->query->get('returnPath')); } - return $this->redirectToRoute('accompanying_course_document_index', ['course' => $course->getId()]); + return $this->redirectToRoute('chill_docstore_generic-doc_by-period_index', ['id' => $course->getId()]); } return $this->render( @@ -136,40 +128,6 @@ class DocumentAccompanyingCourseController extends AbstractController ); } - /** - * @Route("/", name="accompanying_course_document_index", methods="GET") - */ - public function index(AccompanyingPeriod $course): Response - { - $em = $this->getDoctrine()->getManager(); - - if (null === $course) { - throw $this->createNotFoundException('Accompanying period not found'); - } - - $this->denyAccessUnlessGranted(AccompanyingCourseDocumentVoter::SEE, $course); - - $total = $this->courseRepository->countByCourse($course); - $pagination = $this->paginatorFactory->create($total); - - $documents = $this->courseRepository - ->findBy( - ['course' => $course], - ['date' => 'DESC', 'id' => 'DESC'], - $pagination->getItemsPerPage(), - $pagination->getCurrentPageFirstItemNumber() - ); - - return $this->render( - 'ChillDocStoreBundle:AccompanyingCourseDocument:index.html.twig', - [ - 'documents' => $documents, - 'accompanyingCourse' => $course, - 'pagination' => $pagination, - ] - ); - } - /** * @Route("/new", name="accompanying_course_document_new", methods="GET|POST") */ @@ -202,7 +160,7 @@ class DocumentAccompanyingCourseController extends AbstractController $this->addFlash('success', $this->translator->trans('The document is successfully registered')); - return $this->redirectToRoute('accompanying_course_document_index', ['course' => $course->getId()]); + return $this->redirectToRoute('chill_docstore_generic-doc_by-period_index', ['id' => $course->getId()]); } if ($form->isSubmitted() && !$form->isValid()) { diff --git a/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php b/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php index 6fcb6a8e5..20e8e9b03 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php @@ -45,10 +45,6 @@ class DocumentPersonController extends AbstractController protected TranslatorInterface $translator; - private PaginatorFactory $paginatorFactory; - - private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository; - /** * DocumentPersonController constructor. */ @@ -56,14 +52,10 @@ class DocumentPersonController extends AbstractController TranslatorInterface $translator, EventDispatcherInterface $eventDispatcher, AuthorizationHelper $authorizationHelper, - PaginatorFactory $paginatorFactory, - PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository ) { $this->translator = $translator; $this->eventDispatcher = $eventDispatcher; $this->authorizationHelper = $authorizationHelper; - $this->paginatorFactory = $paginatorFactory; - $this->personDocumentACLAwareRepository = $personDocumentACLAwareRepository; } /** @@ -88,7 +80,7 @@ class DocumentPersonController extends AbstractController return $this->redirect($request->query->get('returnPath')); } - return $this->redirectToRoute('person_document_index', ['person' => $person->getId()]); + return $this->redirectToRoute('chill_docstore_generic-doc_by-person_index', ['id' => $person->getId()]); } return $this->render( @@ -160,45 +152,6 @@ class DocumentPersonController extends AbstractController ); } - /** - * @Route("/", name="person_document_index", methods="GET") - */ - public function index(Person $person): Response - { - $em = $this->getDoctrine()->getManager(); - - if (null === $person) { - throw $this->createNotFoundException('Person not found'); - } - - $this->denyAccessUnlessGranted(PersonVoter::SEE, $person); - - $total = $this->personDocumentACLAwareRepository->countByPerson($person); - $pagination = $this->paginatorFactory->create($total); - - $documents = $this->personDocumentACLAwareRepository->findByPerson( - $person, - ['date' => 'DESC', 'id' => 'DESC'], - $pagination->getItemsPerPage(), - $pagination->getCurrentPageFirstItemNumber() - ); - - $event = new PrivacyEvent($person, [ - 'element_class' => PersonDocument::class, - 'action' => 'index', - ]); - $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); - - return $this->render( - 'ChillDocStoreBundle:PersonDocument:index.html.twig', - [ - 'documents' => $documents, - 'person' => $person, - 'pagination' => $pagination, - ] - ); - } - /** * @Route("/new", name="person_document_new", methods="GET|POST") */ @@ -233,7 +186,7 @@ class DocumentPersonController extends AbstractController $this->addFlash('success', $this->translator->trans('The document is successfully registered')); - return $this->redirectToRoute('person_document_index', ['person' => $person->getId()]); + return $this->redirectToRoute('chill_docstore_generic-doc_by-person_index', ['id' => $person->getId()]); } if ($form->isSubmitted() && !$form->isValid()) { diff --git a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php new file mode 100644 index 000000000..70c41db50 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php @@ -0,0 +1,98 @@ +security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)) { + throw new AccessDeniedHttpException("not allowed to see the documents for accompanying period"); + } + + $filterBuilder = $this->filterOrderHelperFactory + ->create(self::class) + ->addSearchBox() + ->addDateRange('dateRange', 'generic_doc.filter.date-range'); + + if ([] !== $places = $this->manager->placesForAccompanyingPeriod($accompanyingPeriod)) { + $filterBuilder->addCheckbox('places', $places, [], array_map( + static fn (string $k) => 'generic_doc.filter.keys.' . $k, + $places + )); + } + + $filter = $filterBuilder + ->build(); + + ['to' => $endDate, 'from' => $startDate ] = $filter->getDateRangeData('dateRange'); + $content = $filter->getQueryString(); + + $nb = $this->manager->countDocForAccompanyingPeriod( + $accompanyingPeriod, + $startDate, + $endDate, + $content, + $filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : [] + ); + $paginator = $this->paginator->create($nb); + + $documents = $this->manager->findDocForAccompanyingPeriod( + $accompanyingPeriod, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), + $startDate, + $endDate, + $content, + $filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : [] + ); + + return new Response($this->twig->render( + '@ChillDocStore/GenericDoc/accompanying_period_list.html.twig', + [ + 'accompanyingCourse' => $accompanyingPeriod, + 'pagination' => $paginator, + 'documents' => iterator_to_array($documents), + 'filter' => $filter, + ] + )); + } + +} diff --git a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php new file mode 100644 index 000000000..3484e0904 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php @@ -0,0 +1,95 @@ +security->isGranted(PersonDocumentVoter::SEE, $person)) { + throw new AccessDeniedHttpException("not allowed to see the documents for person"); + } + + $filterBuilder = $this->filterOrderHelperFactory + ->create(self::class) + ->addSearchBox() + ->addDateRange('dateRange', 'generic_doc.filter.date-range'); + + if ([] !== $places = $this->manager->placesForPerson($person)) { + $filterBuilder->addCheckbox('places', $places, [], array_map( + static fn (string $k) => 'generic_doc.filter.keys.' . $k, + $places + )); + } + + $filter = $filterBuilder + ->build(); + + ['to' => $endDate, 'from' => $startDate ] = $filter->getDateRangeData('dateRange'); + $content = $filter->getQueryString(); + + $nb = $this->manager->countDocForPerson( + $person, + $startDate, + $endDate, + $content, + $filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : [] + ); + $paginator = $this->paginator->create($nb); + + $documents = $this->manager->findDocForPerson( + $person, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), + $startDate, + $endDate, + $content, + $filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : [] + ); + + return new Response($this->twig->render( + '@ChillDocStore/GenericDoc/person_list.html.twig', + [ + 'person' => $person, + 'pagination' => $paginator, + 'documents' => iterator_to_array($documents), + 'filter' => $filter, + ] + )); + } + +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQuery.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQuery.php new file mode 100644 index 000000000..30e07a841 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQuery.php @@ -0,0 +1,233 @@ + + */ + private array $joins = []; + + /** + * @var list> + */ + private array $joinParams = []; + + /** + * @var array> + */ + private array $joinTypes = []; + + /** + * @var array + */ + private array $wheres = []; + + /** + * @var array> + */ + private array $whereParams = []; + + /** + * @var array> + */ + private array $whereTypes = []; + + public function __construct( + private readonly string $selectKeyString, + private readonly string $selectIdentifierJsonB, + private readonly string $selectDate, + private string $from = '', + private array $selectIdentifierParams = [], + private array $selectIdentifierTypes = [], + private array $selectDateParams = [], + private array $selectDateTypes = [], + ) { + } + + public function addJoinClause(string $sql, array $params = [], array $types = []): int + { + $this->joins[] = $sql; + $this->joinParams[] = $params; + $this->joinTypes[] = $types; + + return count($this->joins) - 1; + } + + public function addWhereClause(string $sql, array $params = [], array $types = []): int + { + $this->wheres[] = $sql; + $this->whereParams[] = $params; + $this->whereTypes[] = $types; + + return count($this->wheres) - 1; + } + + public function removeWhereClause(int $index): void + { + if (!array_key_exists($index, $this->wheres)) { + throw new \UnexpectedValueException("this index does not exists"); + } + + unset($this->wheres[$index], $this->whereParams[$index], $this->whereTypes[$index]); + + } + + public function removeJoinClause(int $index): void + { + if (!array_key_exists($index, $this->joins)) { + throw new \UnexpectedValueException("this index does not exists"); + } + + unset($this->joins[$index], $this->joinParams[$index], $this->joinTypes[$index]); + + } + + public function getSelectKeyString(): string + { + return $this->selectKeyString; + } + + public function getSelectIdentifierJsonB(): string + { + return $this->selectIdentifierJsonB; + } + + /** + * @inheritDoc + */ + public function getSelectIdentifierParams(): array + { + return $this->selectIdentifierParams; + } + + public function getSelectIdentifiersTypes(): array + { + return $this->selectIdentifierTypes; + } + + public function getSelectDate(): string + { + return $this->selectDate; + } + + public function getSelectDateTypes(): array + { + return $this->selectDateTypes; + } + + /** + * @inheritDoc + */ + public function getSelectDateParams(): array + { + return $this->selectDateParams; + } + + public function getFromQuery(): string + { + return $this->from . " " . implode(' ', $this->joins); + } + + /** + * @inheritDoc + */ + public function getFromQueryParams(): array + { + $result = []; + + foreach ($this->joinParams as $params) { + $result = [...$result, ...$params]; + } + + return $result; + } + + public function getFromQueryTypes(): array + { + $result = []; + + foreach ($this->joinTypes as $types) { + $result = [...$result, ...$types]; + } + + return $result; + } + + public function getWhereQuery(): string + { + return implode(' AND ', $this->wheres); + } + + /** + * @inheritDoc + */ + public function getWhereQueryParams(): array + { + $result = []; + + foreach ($this->whereParams as $params) { + $result = [...$result, ...$params]; + } + + return $result; + } + + public function getWhereQueryTypes(): array + { + $result = []; + + foreach ($this->whereTypes as $types) { + $result = [...$result, ...$types]; + } + + return $result; + } + + public function setSelectIdentifierParams(array $selectIdentifierParams): self + { + $this->selectIdentifierParams = $selectIdentifierParams; + + return $this; + } + + public function setSelectDateParams(array $selectDateParams): self + { + $this->selectDateParams = $selectDateParams; + + return $this; + } + + public function setFrom(string $from): self + { + $this->from = $from; + + return $this; + } + + public function setSelectIdentifierTypes(array $selectIdentifierTypes): self + { + $this->selectIdentifierTypes = $selectIdentifierTypes; + + return $this; + } + + public function setSelectDateTypes(array $selectDateTypes): self + { + $this->selectDateTypes = $selectDateTypes; + + return $this; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryInterface.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryInterface.php new file mode 100644 index 000000000..e46795457 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryInterface.php @@ -0,0 +1,67 @@ + + */ + public function getSelectIdentifierParams(): array; + + /** + * @return list + */ + public function getSelectIdentifiersTypes(): array; + + public function getSelectDate(): string; + + /** + * @return list + */ + public function getSelectDateParams(): array; + + /** + * @return list + */ + public function getSelectDateTypes(): array; + + public function getFromQuery(): string; + + /** + * @return list + */ + public function getFromQueryParams(): array; + + /** + * @return list + */ + public function getFromQueryTypes(): array; + + public function getWhereQuery(): string; + + /** + * @return list + */ + public function getWhereQueryParams(): array; + + /** + * @return list + */ + public function getWhereQueryTypes(): array; +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryToSqlBuilder.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryToSqlBuilder.php new file mode 100644 index 000000000..2c0c59cff --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryToSqlBuilder.php @@ -0,0 +1,57 @@ +, types: list} + */ + public function toSql(FetchQueryInterface $query): array + { + $sql = strtr(self::SQL, [ + '{{ key }}' => $query->getSelectKeyString(), + '{{ identifiers }}' => $query->getSelectIdentifierJsonB(), + '{{ date }}' => $query->getSelectDate(), + '{{ from }}' => $query->getFromQuery(), + '{{ where }}' => '' === ($w = $query->getWhereQuery()) ? '' : 'WHERE ' . $w, + ]); + + $params = [ + ...$query->getSelectIdentifierParams(), + ...$query->getSelectDateParams(), + ...$query->getFromQueryParams(), + ...$query->getWhereQueryParams() + ]; + + $types = [ + ...$query->getSelectIdentifiersTypes(), + ...$query->getSelectDateTypes(), + ...$query->getFromQueryTypes(), + ...$query->getWhereQueryTypes(), + ]; + + return ['sql' => $sql, 'params' => $params, 'types' => $types]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocDTO.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocDTO.php new file mode 100644 index 000000000..fe9bf7e4f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocDTO.php @@ -0,0 +1,31 @@ +linked instanceof AccompanyingPeriod ? 'accompanying-period' : 'person'; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocForAccompanyingPeriodProviderInterface.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocForAccompanyingPeriodProviderInterface.php new file mode 100644 index 000000000..0d3cb1c32 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocForAccompanyingPeriodProviderInterface.php @@ -0,0 +1,31 @@ + + */ + private iterable $providersForAccompanyingPeriod, + + /** + * @var iterable + */ + private iterable $providersForPerson, + private Connection $connection, + ) { + $this->builder = new FetchQueryToSqlBuilder(); + } + + /** + * @param list $places + * @throws Exception + */ + public function countDocForAccompanyingPeriod( + AccompanyingPeriod $accompanyingPeriod, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null, + array $places = [] + ): int { + ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places); + + return $this->countDoc($sql, $params, $types); + } + + private function countDoc(string $sql, array $params, array $types): int + { + if ($sql === '') { + return 0; + } + + $countSql = "SELECT count(*) AS c FROM ({$sql}) AS sq"; + $result = $this->connection->executeQuery($countSql, $params, $types); + + $number = $result->fetchOne(); + + if (false === $number) { + throw new \UnexpectedValueException("number of documents failed to load"); + } + + return $number; + } + + public function countDocForPerson( + Person $person, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null, + array $places = [] + ): int { + ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places); + + return $this->countDoc($sql, $params, $types); + } + + /** + * @param list $places places to search. When empty, search in all places + * @return iterable + * @throws Exception + */ + public function findDocForAccompanyingPeriod( + AccompanyingPeriod $accompanyingPeriod, + int $offset = 0, + int $limit = 20, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null, + array $places = [] + ): iterable { + ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places); + + return $this->findDocs($accompanyingPeriod, $sql, $params, $types, $offset, $limit); + } + + /** + * @throws \JsonException + * @throws Exception + */ + private function findDocs(AccompanyingPeriod|Person $linked, string $sql, array $params, array $types, int $offset, int $limit): iterable + { + if ($sql === '') { + return []; + } + + $runSql = "{$sql} ORDER BY doc_date DESC LIMIT ? OFFSET ?"; + $runParams = [...$params, ...[$limit, $offset]]; + $runTypes = [...$types, ...[Types::INTEGER, Types::INTEGER]]; + + foreach ($this->connection->iterateAssociative($runSql, $runParams, $runTypes) as $row) { + yield new GenericDocDTO( + $row['key'], + json_decode($row['identifiers'], true, 512, JSON_THROW_ON_ERROR), + new \DateTimeImmutable($row['doc_date']), + $linked, + ); + } + } + + /** + * @param list $places places to search. When empty, search in all places + * @return iterable + */ + public function findDocForPerson( + Person $person, + int $offset = 0, + int $limit = 20, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null, + array $places = [] + ): iterable { + ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places); + + return $this->findDocs($person, $sql, $params, $types, $offset, $limit); + } + + public function placesForPerson(Person $person): array + { + ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person); + + return $this->places($sql, $params, $types); + } + + public function placesForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): array + { + ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod); + + return $this->places($sql, $params, $types); + } + + private function places(string $sql, array $params, array $types): array + { + if ($sql === '') { + return []; + } + + $runSql = "SELECT DISTINCT key FROM ({$sql}) AS sq ORDER BY key"; + + $keys = []; + + foreach ($this->connection->iterateAssociative($runSql, $params, $types) as $k) { + $keys[] = $k['key']; + } + + return $keys; + } + + /** + * @param list $places places to search. When empty, search in all places + */ + private function buildUnionQuery( + AccompanyingPeriod|Person $linked, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null, + array $places = [], + ): array { + $queries = []; + + if ($linked instanceof AccompanyingPeriod) { + foreach ($this->providersForAccompanyingPeriod as $provider) { + if (!$provider->isAllowedForAccompanyingPeriod($linked)) { + continue; + } + $queries[] = $provider->buildFetchQueryForAccompanyingPeriod($linked, $startDate, $endDate, $content); + } + } else { + foreach ($this->providersForPerson as $provider) { + if (!$provider->isAllowedForPerson($linked)) { + continue; + } + $queries[] = $provider->buildFetchQueryForPerson($linked, $startDate, $endDate, $content); + } + } + $sql = []; + $params = []; + $types = []; + + foreach ($queries as $query) { + if ([] !== $places and !in_array($query->getSelectKeyString(), $places, true)) { + continue; + } + + ['sql' => $q, 'params' => $p, 'types' => $t ] = $this->builder->toSql($query); + + $sql[] = $q; + $params = [...$params, ...$p]; + $types = [...$types, ...$t]; + } + + return ['sql' => implode(' UNION ', $sql), 'params' => $params, 'types' => $types]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php new file mode 100644 index 000000000..fd36f7976 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php @@ -0,0 +1,147 @@ +entityManager->getClassMetadata(AccompanyingCourseDocument::class); + + $query = new FetchQuery( + self::KEY, + sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]), + $classMetadata->getColumnName('date'), + $classMetadata->getSchemaName() . '.' . $classMetadata->getTableName() + ); + + $query->addWhereClause( + sprintf('%s = ?', $classMetadata->getSingleAssociationJoinColumnName('course')), + [$accompanyingPeriod->getId()], + [Types::INTEGER] + ); + + return $this->addWhereClause($query, $startDate, $endDate, $content); + } + + public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool + { + return $this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod); + } + + public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface + { + $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class); + + $query = new FetchQuery( + self::KEY, + sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]), + $classMetadata->getColumnName('date'), + $classMetadata->getSchemaName() . '.' . $classMetadata->getTableName() . ' AS acc_course_document' + ); + + $atLeastOne = false; + $or = []; + $orParams = []; + $orTypes = []; + + foreach ($person->getAccompanyingPeriodParticipations() as $participation) { + if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $participation->getAccompanyingPeriod())) { + continue; + } + + $atLeastOne = true; + + $or[] = sprintf( + "(acc_course_document.%s = ? AND acc_course_document.%s BETWEEN ? AND COALESCE(?, 'infinity'::date))", + $classMetadata->getSingleAssociationJoinColumnName('course'), + $classMetadata->getColumnName('date') + ); + $orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(), $participation->getStartDate(), $participation->getEndDate()]; + $orTypes = [...$orTypes, Types::INTEGER, Types::DATE_MUTABLE, Types::DATE_MUTABLE]; + } + + if (!$atLeastOne) { + // there aren't any period allowed to be seen. Add an unreachable condition + $query->addWhereClause('TRUE = FALSE'); + + return $query; + } + + $query->addWhereClause('(' . implode(' OR ', $or) . ')', $orParams, $orTypes); + + return $this->addWhereClause($query, $startDate, $endDate, $content); + } + + public function isAllowedForPerson(Person $person): bool + { + return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person); + } + + private function addWhereClause(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery + { + $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class); + + if (null !== $startDate) { + $query->addWhereClause( + sprintf('? <= %s', $classMetadata->getColumnName('date')), + [$startDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $endDate) { + $query->addWhereClause( + sprintf('? >= %s', $classMetadata->getColumnName('date')), + [$endDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $content and '' !== $content) { + $query->addWhereClause( + sprintf( + '(%s ilike ? OR %s ilike ?)', + $classMetadata->getColumnName('title'), + $classMetadata->getColumnName('description') + ), + ['%' . $content . '%', '%' . $content . '%'], + [Types::STRING, Types::STRING] + ); + } + + return $query; + } + +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php new file mode 100644 index 000000000..613f8d758 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php @@ -0,0 +1,65 @@ +personDocumentACLAwareRepository->buildFetchQueryForPerson( + $person, + $startDate, + $endDate, + $content + ); + } + + public function isAllowedForPerson(Person $person): bool + { + return $this->security->isGranted(PersonDocumentVoter::SEE, $person); + } + + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface + { + return $this->personDocumentACLAwareRepository->buildFetchQueryForAccompanyingPeriod($accompanyingPeriod, $startDate, $endDate, $content); + } + + public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool + { + // we assume that the user is allowed to see at least one person of the course + // this will be double checked when running the query + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php new file mode 100644 index 000000000..c32620030 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php @@ -0,0 +1,59 @@ +key === AccompanyingCourseDocumentGenericDocProvider::KEY + || $genericDocDTO->key === PersonDocumentGenericDocProvider::KEY; + } + + public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string + { + return '@ChillDocStore/List/list_item.html.twig'; + } + + public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array + { + if (AccompanyingCourseDocumentGenericDocProvider::KEY === $genericDocDTO->key) { + return [ + 'document' => $doc = $this->accompanyingCourseDocumentRepository->find($genericDocDTO->identifiers['id']), + 'accompanyingCourse' => $doc->getCourse(), + 'options' => $options, + 'context' => $genericDocDTO->getContext(), + ]; + } + // this is a person + return [ + 'document' => $doc = $this->personDocumentRepository->find($genericDocDTO->identifiers['id']), + 'person' => $doc->getPerson(), + 'options' => $options, + 'context' => $genericDocDTO->getContext(), + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtension.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtension.php new file mode 100644 index 000000000..308d85cd7 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtension.php @@ -0,0 +1,28 @@ + true, + 'is_safe' => ['html'], + ]) + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtensionRuntime.php new file mode 100644 index 000000000..2dee0ed0b --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtensionRuntime.php @@ -0,0 +1,50 @@ + + */ + private iterable $renderers, + ) { + } + + /** + * @throws RuntimeError + * @throws SyntaxError + * @throws LoaderError + */ + public function renderGenericDoc(Environment $twig, GenericDocDTO $genericDocDTO, array $options = []): string + { + foreach ($this->renderers as $renderer) { + if ($renderer->supports($genericDocDTO)) { + return $twig->render( + $renderer->getTemplate($genericDocDTO, $options), + $renderer->getTemplateData($genericDocDTO, $options), + ); + } + } + + throw new \LogicException("no renderer found"); + } + +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php new file mode 100644 index 000000000..940001f4a --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php @@ -0,0 +1,24 @@ +security->isGranted(AccompanyingCourseDocumentVoter::SEE, $course)) { $menu->addChild($this->translator->trans('Documents'), [ - 'route' => 'accompanying_course_document_index', + 'route' => 'chill_docstore_generic-doc_by-period_index', 'routeParameters' => [ - 'course' => $course->getId(), + 'id' => $course->getId(), ], ]) ->setExtras([ @@ -80,9 +80,9 @@ final class MenuBuilder implements LocalMenuBuilderInterface if ($this->security->isGranted(PersonDocumentVoter::SEE, $person)) { $menu->addChild($this->translator->trans('Documents'), [ - 'route' => 'person_document_index', + 'route' => 'chill_docstore_generic-doc_by-person_index', 'routeParameters' => [ - 'person' => $person->getId(), + 'id' => $person->getId(), ], ]) ->setExtras([ diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php index 23dcc4e0b..26a42b894 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php @@ -12,30 +12,33 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Repository; use Chill\DocStoreBundle\Entity\PersonDocument; +use Chill\DocStoreBundle\GenericDoc\FetchQuery; +use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; +use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher; +use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; +use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\Person; +use DateTimeImmutable; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Security\Core\Security; -class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareRepositoryInterface +final readonly class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareRepositoryInterface { - private AuthorizationHelperInterface $authorizationHelper; - - private CenterResolverDispatcher $centerResolverDispatcher; - - private EntityManagerInterface $em; - - private Security $security; - - public function __construct(EntityManagerInterface $em, AuthorizationHelperInterface $authorizationHelper, CenterResolverDispatcher $centerResolverDispatcher, Security $security) - { - $this->em = $em; - $this->authorizationHelper = $authorizationHelper; - $this->centerResolverDispatcher = $centerResolverDispatcher; - $this->security = $security; + public function __construct( + private EntityManagerInterface $em, + private CenterResolverManagerInterface $centerResolverManager, + private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser, + private Security $security, + ) { } public function buildQueryByPerson(Person $person): QueryBuilder @@ -49,6 +52,128 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito return $qb; } + + public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface + { + $query = $this->buildBaseFetchQueryForPerson($person, $startDate, $endDate, $content); + + return $this->addFetchQueryByPersonACL($query, $person); + } + + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface + { + $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); + $participationMetadata = $this->em->getClassMetadata(AccompanyingPeriodParticipation::class); + + $query = new FetchQuery( + PersonDocumentGenericDocProvider::KEY, + sprintf('jsonb_build_object(\'id\', person_document.%s)', $personDocMetadata->getSingleIdentifierColumnName()), + sprintf('person_document.%s', $personDocMetadata->getColumnName('date')), + sprintf('%s AS person_document', $personDocMetadata->getSchemaName().'.'.$personDocMetadata->getTableName()) + ); + + $query->addJoinClause( + sprintf( + 'JOIN %s AS participation ON participation.%s = person_document.%s '. + 'AND person_document.%s BETWEEN participation.%s AND COALESCE(participation.%s, \'infinity\'::date)', + $participationMetadata->getTableName(), + $participationMetadata->getSingleAssociationJoinColumnName('person'), + $personDocMetadata->getSingleAssociationJoinColumnName('person'), + $personDocMetadata->getColumnName('date'), + $participationMetadata->getColumnName('startDate'), + $participationMetadata->getColumnName('endDate') + ) + ); + + $query->addWhereClause( + sprintf('participation.%s = ?', $participationMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod')), + [$period->getId()], + [Types::INTEGER] + ); + + // can we see the document for this person ? + $orPersonId = []; + foreach ($period->getParticipations() as $participation) { + if (!$this->security->isGranted(PersonDocumentVoter::SEE, $participation->getPerson())) { + continue; + } + $orPersonId[] = $participation->getPerson()->getId(); + + } + + if ([] === $orPersonId) { + $query->addWhereClause('FALSE = TRUE'); + + return $query; + } + + $query->addWhereClause( + sprintf( + 'participation.%s IN (%s)', + $participationMetadata->getSingleAssociationJoinColumnName('person'), + implode(', ', array_fill(0, count($orPersonId), '?')) + ), + $orPersonId, + array_fill(0, count($orPersonId), Types::INTEGER) + ); + + return $this->addFilterClauses($query, $startDate, $endDate, $content); + } + + public function buildBaseFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery + { + $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); + + $query = new FetchQuery( + PersonDocumentGenericDocProvider::KEY, + sprintf('jsonb_build_object(\'id\', person_document.%s)', $personDocMetadata->getSingleIdentifierColumnName()), + sprintf('person_document.%s', $personDocMetadata->getColumnName('date')), + sprintf('%s AS person_document', $personDocMetadata->getSchemaName().'.'.$personDocMetadata->getTableName()) + ); + + $query->addWhereClause( + sprintf('person_document.%s = ?', $personDocMetadata->getSingleAssociationJoinColumnName('person')), + [$person->getId()], + [Types::INTEGER] + ); + + return $this->addFilterClauses($query, $startDate, $endDate, $content); + } + + private function addFilterClauses(FetchQuery $query, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery + { + $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); + + if (null !== $startDate) { + $query->addWhereClause( + sprintf('? <= %s', $personDocMetadata->getColumnName('date')), + [$startDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $endDate) { + $query->addWhereClause( + sprintf('? >= %s', $personDocMetadata->getColumnName('date')), + [$endDate], + [Types::DATE_IMMUTABLE] + ); + } + + if (null !== $content and '' !== $content) { + $query->addWhereClause( + sprintf( + '(%s ilike ? OR %s ilike ?)', + $personDocMetadata->getColumnName('title'), + $personDocMetadata->getColumnName('description') + ), + ['%' . $content . '%', '%' . $content . '%'], + [Types::STRING, Types::STRING] + ); + } + return $query; + } + public function countByPerson(Person $person): int { $qb = $this->buildQueryByPerson($person)->select('COUNT(d)'); @@ -75,16 +200,58 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito private function addACL(QueryBuilder $qb, Person $person): void { - $center = $this->centerResolverDispatcher->resolveCenter($person); + $reachableScopes = []; - $reachableScopes = $this->authorizationHelper - ->getReachableScopes( - $this->security->getUser(), - PersonDocumentVoter::SEE, - $center - ); + foreach ($this->centerResolverManager->resolveCenters($person) as $center) { + $reachableScopes = [ + ...$reachableScopes, + ...$this->authorizationHelperForCurrentUser + ->getReachableScopes( + PersonDocumentVoter::SEE, + $center + ) + ]; + } + + if ([] === $reachableScopes) { + $qb->andWhere("'FALSE' = 'TRUE'"); + + return; + } $qb->andWhere($qb->expr()->in('d.scope', ':scopes')) ->setParameter('scopes', $reachableScopes); } + + private function addFetchQueryByPersonACL(FetchQuery $fetchQuery, Person $person): FetchQuery + { + $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); + + $reachableScopes = []; + + foreach ($this->centerResolverManager->resolveCenters($person) as $center) { + $reachableScopes = [ + ...$reachableScopes, + ...$this->authorizationHelperForCurrentUser->getReachableScopes(PersonDocumentVoter::SEE, $center) + ]; + } + + if ([] === $reachableScopes) { + $fetchQuery->addWhereClause('FALSE = TRUE'); + + return $fetchQuery; + } + + $fetchQuery->addWhereClause( + sprintf( + 'person_document.%s IN (%s)', + $personDocMetadata->getSingleAssociationJoinColumnName('scope'), + implode(', ', array_fill(0, count($reachableScopes), '?')) + ), + array_map(static fn (Scope $s) => $s->getId(), $reachableScopes), + array_fill(0, count($reachableScopes), Types::INTEGER) + ); + + return $fetchQuery; + } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php index 6c4bd2e9a..f1bc70812 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php @@ -11,11 +11,33 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Repository; +use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; interface PersonDocumentACLAwareRepositoryInterface { + /** + * @deprecated use fetch query for listing and counting person documents + */ public function countByPerson(Person $person): int; + /** + * @deprecated use fetch query for listing and counting person documents + */ public function findByPerson(Person $person, array $orderBy = [], int $limit = 20, int $offset = 0): array; + + public function buildFetchQueryForPerson( + Person $person, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null + ): FetchQueryInterface; + + public function buildFetchQueryForAccompanyingPeriod( + AccompanyingPeriod $period, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null + ): FetchQueryInterface; } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php new file mode 100644 index 000000000..40afdc220 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php @@ -0,0 +1,57 @@ + + */ +readonly class PersonDocumentRepository implements ObjectRepository +{ + private EntityRepository $repository; + + public function __construct( + private EntityManagerInterface $entityManager + ) { + $this->repository = $this->entityManager->getRepository($this->getClassName()); + } + + public function find($id): ?PersonDocument + { + return $this->repository->find($id); + } + + public function findAll() + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?PersonDocument + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return PersonDocument::class; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue index aa99a223f..be482cf7e 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/ConvertButton.vue @@ -44,3 +44,9 @@ async function download_and_open(event: Event): Promise { } + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue index b22035bee..0248ef30b 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue @@ -1,52 +1,96 @@ + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue index d68f60f86..ea244192e 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue @@ -40,5 +40,8 @@ async function beforeLeave(event: Event): Promise { diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts index 5b61a108d..c0b0fa940 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers.ts @@ -149,16 +149,21 @@ async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKe } if (iv.length === 0) { + console.log('returning document immediatly'); return rawResponse.blob(); } + console.log('start decrypting doc'); + const rawBuffer = await rawResponse.arrayBuffer(); try { const key = await window.crypto.subtle .importKey('jwk', keyData, { name: algo }, false, ['decrypt']); + console.log('key created'); const decrypted = await window.crypto.subtle .decrypt({ name: algo, iv: iv }, key, rawBuffer); + console.log('doc decrypted'); return Promise.resolve(new Blob([decrypted])); } catch (e) { diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/delete.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/delete.html.twig index c9bb608cf..d6f23d09d 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/delete.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/delete.html.twig @@ -31,8 +31,8 @@ 'title' : 'Delete document ?'|trans, 'display_content' : block('docdescription'), 'confirm_question' : 'Are you sure you want to remove this document ?'|trans, - 'cancel_route' : 'accompanying_course_document_index', - 'cancel_parameters' : {'course' : accompanyingCourse.id, 'id': document.id}, + 'cancel_route' : 'chill_docstore_generic-doc_by-period_index', + 'cancel_parameters' : {'id' : accompanyingCourse.id}, 'form' : delete_form } ) }} {% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/edit.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/edit.html.twig index 0ca5661fc..326814502 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/edit.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/edit.html.twig @@ -21,7 +21,7 @@ diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig deleted file mode 100644 index 7a013260c..000000000 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig +++ /dev/null @@ -1,52 +0,0 @@ -{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} - -{% set activeRouteKey = '' %} - -{% block title %} - {{ 'Documents' }} -{% endblock %} - -{% block js %} - {{ parent() }} - {{ encore_entry_script_tags('mod_docgen_picktemplate') }} - {{ encore_entry_script_tags('mod_entity_workflow_pick') }} - {{ encore_entry_script_tags('mod_document_action_buttons_group') }} -{% endblock %} - -{% block css %} - {{ parent() }} - {{ encore_entry_link_tags('mod_docgen_picktemplate') }} - {{ encore_entry_link_tags('mod_entity_workflow_pick') }} - {{ encore_entry_link_tags('mod_document_action_buttons_group') }} -{% endblock %} - -{% block content %} -
-

{{ 'Documents' }}

- - {% if documents|length == 0 %} -

{{ 'No documents'|trans }}

- {% else %} -
- {% for document in documents %} - {% include '@ChillDocStore/List/list_item.html.twig' %} - {% endfor %} -
- {% endif %} - - {{ chill_pagination(pagination) }} - -
- - {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', accompanyingCourse) %} - - {% endif %} - -
-{% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/new.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/new.html.twig index 01be1a5d7..3fb692c78 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/new.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/new.html.twig @@ -25,7 +25,7 @@
  • - + {{ 'Back to the list' | trans }}
  • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig index 3c62451a9..9590dbb78 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig @@ -46,7 +46,7 @@
    • - + {{ 'Back to the list' | trans }}
    • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig index f83cafd51..2babe1ad9 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Button/button_group.html.twig @@ -1,5 +1,5 @@ {%- import "@ChillDocStore/Macro/macro.html.twig" as m -%} -
      +

      {{ 'Documents' }}

      + + {{ filter|chill_render_filter_order_helper }} + + {% if documents|length == 0 %} +

      {{ 'No documents'|trans }}

      + {% else %} +
      + {% for document in documents %} + {{ document|chill_generic_doc_render }} + {% endfor %} +
      + {% endif %} + + {{ chill_pagination(pagination) }} + +
      + + {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', accompanyingCourse) %} + + {% endif %} + +
      +{% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/GenericDoc/person_list.html.twig similarity index 95% rename from src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig rename to src/Bundle/ChillDocStoreBundle/Resources/views/GenericDoc/person_list.html.twig index 8d201605e..a4aa4fbbc 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/index.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/GenericDoc/person_list.html.twig @@ -44,12 +44,14 @@

      {{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}

      + {{ filter|chill_render_filter_order_helper }} + {% if documents|length == 0 %}

      {{ 'No documents'|trans }}

      {% else %}
      {% for document in documents %} - {% include 'ChillDocStoreBundle:List:list_item.html.twig' %} + {{ document|chill_generic_doc_render }} {% endfor %}
      {% endif %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig index 8dea4bbe7..58504b095 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig @@ -10,8 +10,23 @@ {% elseif document.object.isFailure %}
      {{ 'docgen.Doc generation failed'|trans }}
      {% endif %} + + {% if context == 'person' and accompanyingCourse is defined %} +
      + + {{ accompanyingCourse.id }} +   +
      + {% elseif context == 'accompanying-period' and person is defined %} +
      + + {{ 'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }} +   +
      + + {% endif %}
      - {{ document.title }} + {{ document.title|chill_print_or_message("No title") }}
      {% if document.object.type is not empty %}
      diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/delete.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/delete.html.twig index bfdd87dc3..41c229faa 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/delete.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/delete.html.twig @@ -36,8 +36,8 @@ 'title' : 'Delete document ?'|trans, 'display_content' : block('docdescription'), 'confirm_question' : 'Are you sure you want to remove this document ?'|trans, - 'cancel_route' : 'person_document_index', - 'cancel_parameters' : {'person' : person.id, 'id': document.id}, + 'cancel_route' : 'chill_docstore_generic-doc_by-person_index', + 'cancel_parameters' : {'id' : person.id}, 'form' : delete_form } ) }} {% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/edit.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/edit.html.twig index 416a35e35..17ce9d774 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/edit.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/edit.html.twig @@ -38,7 +38,7 @@
      • - + {{ 'Back to the list' | trans }}
      • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/new.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/new.html.twig index 3187714a6..faf8895e7 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/new.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/new.html.twig @@ -42,7 +42,7 @@
        • - + {{ 'Back to the list' | trans }}
        • diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig index c276e067e..1c0aa5e64 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/show.html.twig @@ -63,7 +63,7 @@
          • - + {{ 'Back to the list' | trans }}
          • diff --git a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/FetchQueryToSqlBuilderTest.php b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/FetchQueryToSqlBuilderTest.php new file mode 100644 index 000000000..02be9460f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/FetchQueryToSqlBuilderTest.php @@ -0,0 +1,89 @@ +addJoinClause('LEFT JOIN other b ON a.id = b.foreign_id', ['foo'], [Types::STRING]); + $index = $query->addJoinClause('LEFT JOIN other c ON a.id = c.foreign_id', ['bar'], [Types::STRING]); + $query->addJoinClause('LEFT JOIN other d ON a.id = d.foreign_id', ['bar_baz'], [Types::STRING]); + $query->removeJoinClause($index); + $query->addWhereClause('b.item = ?', ['baz'], [Types::STRING]); + $index = $query->addWhereClause('b.cancel', [ 'foz'], [Types::STRING]); + $query->removeWhereClause($index); + + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $filteredSql = + implode(" ", array_filter( + explode(" ", str_replace("\n", "", $sql)), + fn (string $tok) => $tok !== "" + )) + ; + + self::assertEquals( + "SELECT 'test' AS key, jsonb_build_object('id', a.column) AS identifiers, ". + "a.datecolumn::date AS doc_date FROM my_table a LEFT JOIN other b ON a.id = b.foreign_id LEFT JOIN other d ON a.id = d.foreign_id WHERE b.item = ?", + $filteredSql + ); + self::assertEquals(['foo', 'bar_baz', 'baz'], $params); + self::assertEquals([Types::STRING, Types::STRING, Types::STRING], $types); + + + + } + + public function testToSqlWithoutWhere(): void + { + $query = new FetchQuery( + 'test', + 'jsonb_build_object(\'id\', a.column)', + 'a.datecolumn', + 'my_table a' + ); + + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $filteredSql = + implode(" ", array_filter( + explode(" ", str_replace("\n", "", $sql)), + fn (string $tok) => $tok !== "" + )) + ; + + self::assertEquals( + "SELECT 'test' AS key, jsonb_build_object('id', a.column) AS identifiers, ". + "a.datecolumn::date AS doc_date FROM my_table a", + $filteredSql + ); + self::assertEquals([], $params); + self::assertEquals([], $types); + } + +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/ManagerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/ManagerTest.php new file mode 100644 index 000000000..f59779374 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/ManagerTest.php @@ -0,0 +1,217 @@ +em = self::$container->get(EntityManagerInterface::class); + $this->connection = self::$container->get(Connection::class); + } + + public function testCountByAccompanyingPeriod(): void + { + $period = $this->em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a') + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $period) { + throw new \UnexpectedValueException("period not found"); + } + + $manager = new Manager( + [new SimpleGenericDocAccompanyingPeriodProvider()], + [new SimpleGenericDocPersonProvider()], + $this->connection, + ); + + $nb = $manager->countDocForAccompanyingPeriod($period); + + self::assertIsInt($nb); + } + + public function testCountByPerson(): void + { + $person = $this->em->createQuery('SELECT a FROM '.Person::class.' a') + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $person) { + throw new \UnexpectedValueException("person found"); + } + + $manager = new Manager( + [new SimpleGenericDocAccompanyingPeriodProvider()], + [new SimpleGenericDocPersonProvider()], + $this->connection, + ); + + $nb = $manager->countDocForPerson($person); + + self::assertIsInt($nb); + } + + public function testFindDocByAccompanyingPeriod(): void + { + $period = $this->em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a') + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $period) { + throw new \UnexpectedValueException("period not found"); + } + + $manager = new Manager( + [new SimpleGenericDocAccompanyingPeriodProvider()], + [new SimpleGenericDocPersonProvider()], + $this->connection, + ); + + foreach ($manager->findDocForAccompanyingPeriod($period) as $doc) { + self::assertInstanceOf(GenericDocDTO::class, $doc); + } + } + + public function testFindDocByPerson(): void + { + $person = $this->em->createQuery('SELECT a FROM '.Person::class.' a') + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $person) { + throw new \UnexpectedValueException("person not found"); + } + + $manager = new Manager( + [new SimpleGenericDocAccompanyingPeriodProvider()], + [new SimpleGenericDocPersonProvider()], + $this->connection, + ); + + foreach ($manager->findDocForPerson($person) as $doc) { + self::assertInstanceOf(GenericDocDTO::class, $doc); + } + } + + public function testPlacesForPerson(): void + { + $person = $this->em->createQuery('SELECT a FROM '.Person::class.' a') + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $person) { + throw new \UnexpectedValueException("person not found"); + } + + $manager = new Manager( + [new SimpleGenericDocAccompanyingPeriodProvider()], + [new SimpleGenericDocPersonProvider()], + $this->connection, + ); + + $places = $manager->placesForPerson($person); + + self::assertEquals(['dummy_person_doc'], $places); + } + + public function testPlacesForAccompanyingPeriod(): void + { + $period = $this->em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a') + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $period) { + throw new \UnexpectedValueException("period not found"); + } + + $manager = new Manager( + [new SimpleGenericDocAccompanyingPeriodProvider()], + [new SimpleGenericDocPersonProvider()], + $this->connection, + ); + + $places = $manager->placesForAccompanyingPeriod($period); + + self::assertEquals(['accompanying_course_document_dummy'], $places); + } +} + +final readonly class SimpleGenericDocAccompanyingPeriodProvider implements GenericDocForAccompanyingPeriodProviderInterface +{ + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface + { + $query = new FetchQuery( + 'accompanying_course_document_dummy', + sprintf('jsonb_build_object(\'id\', %s)', 'id'), + 'd', + '(VALUES (1, \'2023-05-01\'::date), (2, \'2023-05-01\'::date)) AS sq (id, d)', + ); + + $query->addWhereClause('d > ?', [new \DateTimeImmutable('2023-01-01')], [Types::DATE_IMMUTABLE]); + + return $query; + } + + public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool + { + return true; + } +} + +final readonly class SimpleGenericDocPersonProvider implements GenericDocForPersonProviderInterface +{ + public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface + { + $query = new FetchQuery( + 'dummy_person_doc', + sprintf('jsonb_build_object(\'id\', %s)', 'id'), + 'd', + '(VALUES (1, \'2023-05-01\'::date), (2, \'2023-05-01\'::date)) AS sq (id, d)', + ); + + $query->addWhereClause('d > ?', [new \DateTimeImmutable('2023-01-01')], [Types::DATE_IMMUTABLE]); + + return $query; + } + + public function isAllowedForPerson(Person $person): bool + { + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProviderTest.php b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProviderTest.php new file mode 100644 index 000000000..79ced9258 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProviderTest.php @@ -0,0 +1,79 @@ +entityManager = self::$container->get(EntityManagerInterface::class); + } + + /** + * @dataProvider provideSearchArguments + */ + public function testWithoutAnyArgument(?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content = null): void + { + $period = $this->entityManager->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a') + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $period) { + throw new \UnexpectedValueException("period not found"); + } + + $security = $this->prophesize(Security::class); + $security->isGranted(AccompanyingCourseDocumentVoter::SEE, $period) + ->willReturn(true); + + $provider = new AccompanyingCourseDocumentGenericDocProvider( + $security->reveal(), + $this->entityManager + ); + + $query = $provider->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content); + + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $nb = $this->entityManager->getConnection()->executeQuery('SELECT COUNT(*) FROM ('.$sql.') AS sq', $params, $types) + ->fetchOne(); + + self::assertIsInt($nb); + } + + public function provideSearchArguments(): iterable + { + yield [null, null, null]; + yield [new \DateTimeImmutable('1 month ago'), null, null]; + yield [new \DateTimeImmutable('1 month ago'), new \DateTimeImmutable('now'), null]; + yield [null, null, 'test']; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/PersonDocumentGenericDocProviderTest.php b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/PersonDocumentGenericDocProviderTest.php new file mode 100644 index 000000000..577357820 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/PersonDocumentGenericDocProviderTest.php @@ -0,0 +1,84 @@ +entityManager = self::$container->get(EntityManagerInterface::class); + $this->personDocumentACLAwareRepository = self::$container->get(PersonDocumentACLAwareRepositoryInterface::class); + } + + /** + * @dataProvider provideDataBuildFetchQueryForPerson + * @throws \Doctrine\DBAL\Exception + * @throws \Doctrine\ORM\NoResultException + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function testBuildFetchQueryForPerson(?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?string $content): void + { + $security = $this->prophesize(Security::class); + $person = $this->entityManager->createQuery('SELECT a FROM '.Person::class.' a') + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $person) { + throw new \UnexpectedValueException("person found"); + } + + $provider = new PersonDocumentGenericDocProvider( + $security->reveal(), + $this->personDocumentACLAwareRepository + ); + + $query = $provider->buildFetchQueryForPerson($person, $startDate, $endDate, $content); + + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $nb = $this->entityManager->getConnection() + ->fetchOne("SELECT COUNT(*) AS nb FROM (${sql}) AS sq", $params, $types); + + self::assertIsInt($nb, "Test that the query is syntactically correct"); + } + + public function provideDataBuildFetchQueryForPerson(): iterable + { + yield [null, null, null]; + yield [new DateTimeImmutable('1 year ago'), null, null]; + yield [null, new DateTimeImmutable('1 year ago'), null]; + yield [new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), null]; + yield [null, null, 'test']; + yield [new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), 'test']; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Repository/PersonDocumentACLAwareRepositoryTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Repository/PersonDocumentACLAwareRepositoryTest.php new file mode 100644 index 000000000..fd611042c --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Repository/PersonDocumentACLAwareRepositoryTest.php @@ -0,0 +1,158 @@ +entityManager = self::$container->get(EntityManagerInterface::class); + $this->scopeRepository = self::$container->get(ScopeRepositoryInterface::class); + } + + /** + * @dataProvider provideDataBuildFetchQueryForPerson + * @throws \Doctrine\DBAL\Exception + * @throws \Doctrine\ORM\NoResultException + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function testBuildFetchQueryForPerson(?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): void + { + $centerManager = $this->prophesize(CenterResolverManagerInterface::class); + $centerManager->resolveCenters(Argument::type(Person::class)) + ->willReturn([new Center()]); + + $scopes = $this->scopeRepository->findAll(); + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $authorizationHelper->getReachableScopes(PersonDocumentVoter::SEE, Argument::any())->willReturn($scopes); + + $repository = new PersonDocumentACLAwareRepository( + $this->entityManager, + $centerManager->reveal(), + $authorizationHelper->reveal(), + $this->prophesize(Security::class)->reveal() + ); + + $person = $this->entityManager->createQuery("SELECT p FROM " . Person::class . " p") + ->setMaxResults(1) + ->getSingleResult(); + + if (null === $person) { + throw new \RuntimeException("person not exists in database"); + } + + $query = $repository->buildFetchQueryForPerson($person, $startDate, $endDate, $content); + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $nb = $this->entityManager->getConnection() + ->fetchOne("SELECT COUNT(*) FROM ({$sql}) AS sq", $params, $types); + + self::assertIsInt($nb, "test that the query could be executed"); + } + + /** + * @dataProvider provideDateForFetchQueryForAccompanyingPeriod + */ + public function testBuildFetchQueryForAccompanyingPeriod( + AccompanyingPeriod $period, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null + ): void { + $centerManager = $this->prophesize(CenterResolverManagerInterface::class); + $centerManager->resolveCenters(Argument::type(Person::class)) + ->willReturn([new Center()]); + + $scopes = $this->scopeRepository->findAll(); + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $authorizationHelper->getReachableScopes(PersonDocumentVoter::SEE, Argument::any())->willReturn($scopes); + + $security = $this->prophesize(Security::class); + $security->isGranted(PersonDocumentVoter::SEE, Argument::type(Person::class))->willReturn(true); + + $repository = new PersonDocumentACLAwareRepository( + $this->entityManager, + $centerManager->reveal(), + $authorizationHelper->reveal(), + $security->reveal() + ); + + $query = $repository->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content); + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $nb = $this->entityManager->getConnection() + ->fetchOne("SELECT COUNT(*) FROM ({$sql}) AS sq", $params, $types); + + self::assertIsInt($nb, "test that the query could be executed"); + } + + public function provideDateForFetchQueryForAccompanyingPeriod(): iterable + { + $this->setUp(); + + if (null === $period = $this->entityManager->createQuery( + "SELECT p FROM " . AccompanyingPeriod::class . " p WHERE SIZE(p.participations) > 0" + ) + ->setMaxResults(1)->getSingleResult()) { + throw new \RuntimeException("no course found"); + } + + yield [$period, null, null, null]; + yield [$period, new DateTimeImmutable('1 year ago'), null, null]; + yield [$period, null, new DateTimeImmutable('1 year ago'), null]; + yield [$period, new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), null]; + yield [$period, null, null, 'test']; + yield [$period, new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), 'test']; + + } + + public function provideDataBuildFetchQueryForPerson(): iterable + { + yield [null, null, null]; + yield [new DateTimeImmutable('1 year ago'), null, null]; + yield [null, new DateTimeImmutable('1 year ago'), null]; + yield [new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), null]; + yield [null, null, 'test']; + yield [new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), 'test']; + } + +} diff --git a/src/Bundle/ChillDocStoreBundle/config/services.yaml b/src/Bundle/ChillDocStoreBundle/config/services.yaml index 860495677..04fc3ace3 100644 --- a/src/Bundle/ChillDocStoreBundle/config/services.yaml +++ b/src/Bundle/ChillDocStoreBundle/config/services.yaml @@ -2,46 +2,72 @@ parameters: # cl_chill_person.example.class: Chill\PersonBundle\Example services: - Chill\DocStoreBundle\Repository\: - autowire: true - autoconfigure: true - resource: "../Repository/" + Chill\DocStoreBundle\Repository\: + autowire: true + autoconfigure: true + resource: "../Repository/" - Chill\DocStoreBundle\Form\DocumentCategoryType: - class: Chill\DocStoreBundle\Form\DocumentCategoryType - arguments: ["%kernel.bundles%"] - tags: - - { name: form.type } + Chill\DocStoreBundle\Form\DocumentCategoryType: + class: Chill\DocStoreBundle\Form\DocumentCategoryType + arguments: [ "%kernel.bundles%" ] + tags: + - { name: form.type } - Chill\DocStoreBundle\Form\PersonDocumentType: - class: Chill\DocStoreBundle\Form\PersonDocumentType - autowire: true - autoconfigure: true - # arguments: - # - "@chill.main.helper.translatable_string" - tags: - - { name: form.type, alias: chill_docstorebundle_form_document } + Chill\DocStoreBundle\Form\PersonDocumentType: + class: Chill\DocStoreBundle\Form\PersonDocumentType + autowire: true + autoconfigure: true + # arguments: + # - "@chill.main.helper.translatable_string" + tags: + - { name: form.type, alias: chill_docstorebundle_form_document } - Chill\DocStoreBundle\Security\Authorization\: - resource: "./../Security/Authorization" - autowire: true - autoconfigure: true - tags: - - { name: chill.role } + Chill\DocStoreBundle\Security\Authorization\: + resource: "./../Security/Authorization" + autowire: true + autoconfigure: true + tags: + - { name: chill.role } - Chill\DocStoreBundle\Workflow\: - resource: './../Workflow/' - autoconfigure: true - autowire: true + Chill\DocStoreBundle\Workflow\: + resource: './../Workflow/' + autoconfigure: true + autowire: true - Chill\DocStoreBundle\Serializer\Normalizer\: - autowire: true - resource: '../Serializer/Normalizer/' - tags: - - { name: 'serializer.normalizer', priority: 16 } + Chill\DocStoreBundle\Serializer\Normalizer\: + autowire: true + resource: '../Serializer/Normalizer/' + tags: + - { name: 'serializer.normalizer', priority: 16 } - Chill\DocStoreBundle\Service\: - autowire: true - autoconfigure: true - resource: '../Service/' + Chill\DocStoreBundle\Service\: + autowire: true + autoconfigure: true + resource: '../Service/' + Chill\DocStoreBundle\GenericDoc\Manager: + autowire: true + autoconfigure: true + arguments: + $providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider + $providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_provider + + Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension: + autoconfigure: true + autowire: true + + Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtensionRuntime: + autoconfigure: true + autowire: true + arguments: + $renderers: !tagged_iterator chill_doc_store.generic_doc_renderer + + Chill\DocStoreBundle\GenericDoc\Providers\: + autowire: true + autoconfigure: true + resource: '../GenericDoc/Providers/' + + Chill\DocStoreBundle\GenericDoc\Renderer\: + autowire: true + autoconfigure: true + resource: '../GenericDoc/Renderer/' diff --git a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml index 1d06c58e4..ffabdaa59 100644 --- a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml @@ -18,10 +18,19 @@ No document found: Aucun document trouvé The document is successfully registered: Le document est enregistré The document is successfully updated: Le document est mis à jour Any description: Aucune description +Document from person %name%: Document de l'usager %name% +See the document: Voir le document document: Any title: Aucun titre +generic_doc: + filter: + keys: + accompanying_course_document: Document du parcours + person_document: Documents de l'usager + date-range: Date du document + # delete Delete document ?: Supprimer le document ? Are you sure you want to remove this document ?: Êtes-vous sûr·e de vouloir supprimer ce document ? diff --git a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php index 1929beac1..a2a82c88f 100644 --- a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php +++ b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php @@ -16,6 +16,8 @@ use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Form\ParticipationType; use Chill\EventBundle\Security\Authorization\ParticipationVoter; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ReadableCollection; use LogicException; use Psr\Log\LoggerInterface; use RuntimeException; @@ -509,7 +511,7 @@ class ParticipationController extends AbstractController /** * @return \Symfony\Component\Form\FormInterface */ - protected function createEditFormMultiple(ArrayIterator $participations, Event $event) + protected function createEditFormMultiple(Collection $participations, Event $event) { $form = $this->createForm( \Symfony\Component\Form\Extension\Core\Type\FormType::class, @@ -638,6 +640,7 @@ class ParticipationController extends AbstractController $ignoredParticipations = $newParticipations = []; foreach ($participations as $participation) { + /** @var Participation $participation */ // check for authorization $this->denyAccessUnlessGranted( ParticipationVoter::CREATE, diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index d55a7b8a8..cc762e979 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -160,11 +160,11 @@ class Event implements HasCenterInterface, HasScopeInterface } /** - * @return ArrayIterator|Collection|Traversable + * @return Collection */ public function getParticipations() { - return $this->getParticipationsOrdered(); + return new ArrayCollection(iterator_to_array($this->getParticipationsOrdered())); } /** diff --git a/src/Bundle/ChillMainBundle/Controller/ExportController.php b/src/Bundle/ChillMainBundle/Controller/ExportController.php index 6c4ea0269..e8ee9b97b 100644 --- a/src/Bundle/ChillMainBundle/Controller/ExportController.php +++ b/src/Bundle/ChillMainBundle/Controller/ExportController.php @@ -13,12 +13,16 @@ namespace Chill\MainBundle\Controller; use Chill\MainBundle\Entity\SavedExport; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Export\DirectExportInterface; +use Chill\MainBundle\Export\ExportFormHelper; +use Chill\MainBundle\Export\ExportInterface; use Chill\MainBundle\Export\ExportManager; use Chill\MainBundle\Form\SavedExportType; use Chill\MainBundle\Form\Type\Export\ExportType; use Chill\MainBundle\Form\Type\Export\FormatterType; use Chill\MainBundle\Form\Type\Export\PickCenterType; use Chill\MainBundle\Redis\ChillRedis; +use Chill\MainBundle\Repository\SavedExportRepositoryInterface; use Chill\MainBundle\Security\Authorization\SavedExportVoter; use Doctrine\ORM\EntityManagerInterface; use LogicException; @@ -36,6 +40,7 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; use function count; use function serialize; @@ -86,7 +91,10 @@ class ExportController extends AbstractController LoggerInterface $logger, SessionInterface $session, TranslatorInterface $translator, - EntityManagerInterface $entityManager + EntityManagerInterface $entityManager, + private readonly ExportFormHelper $exportFormHelper, + private readonly SavedExportRepositoryInterface $savedExportRepository, + private readonly Security $security, ) { $this->entityManager = $entityManager; $this->redis = $chillRedis; @@ -101,12 +109,11 @@ class ExportController extends AbstractController { /** @var \Chill\MainBundle\Export\ExportManager $exportManager */ $exportManager = $this->exportManager; - $export = $exportManager->getExport($alias); - $key = $request->query->get('key', null); + $savedExport = $this->getSavedExportFromRequest($request); - [$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key); + [$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport); $formatterAlias = $exportManager->getFormatterAlias($dataExport['export']); @@ -120,6 +127,7 @@ class ExportController extends AbstractController 'alias' => $alias, 'export' => $export, 'export_group' => $this->getExportGroup($export), + 'saved_export' => $savedExport, ]; if ($formater instanceof \Chill\MainBundle\Export\Formatter\CSVListFormatter) { @@ -144,8 +152,9 @@ class ExportController extends AbstractController /** @var \Chill\MainBundle\Export\ExportManager $exportManager */ $exportManager = $this->exportManager; $key = $request->query->get('key', null); + $savedExport = $this->getSavedExportFromRequest($request); - [$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key); + [$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport); return $exportManager->generate( $alias, @@ -204,12 +213,8 @@ class ExportController extends AbstractController * 3. 'generate': gather data from session from the previous steps, and * make a redirection to the "generate" action with data in query (HTTP GET) * - * @param string $request - * @param Request $alias - * - * @return \Symfony\Component\HttpFoundation\Response */ - public function newAction(Request $request, $alias) + public function newAction(Request $request, string $alias): Response { // first check for ACL $exportManager = $this->exportManager; @@ -219,26 +224,50 @@ class ExportController extends AbstractController throw $this->createAccessDeniedException('The user does not have access to this export'); } + $savedExport = $this->getSavedExportFromRequest($request); + $step = $request->query->getAlpha('step', 'centers'); switch ($step) { case 'centers': - return $this->selectCentersStep($request, $export, $alias); + return $this->selectCentersStep($request, $export, $alias, $savedExport); case 'export': - return $this->exportFormStep($request, $export, $alias); + return $this->exportFormStep($request, $export, $alias, $savedExport); case 'formatter': - return $this->formatterFormStep($request, $export, $alias); + return $this->formatterFormStep($request, $export, $alias, $savedExport); case 'generate': - return $this->forwardToGenerate($request, $export, $alias); + return $this->forwardToGenerate($request, $export, $alias, $savedExport); default: throw $this->createNotFoundException("The given step '{$step}' is invalid"); } } + /** + * @Route("/{_locale}/export/saved/update-from-key/{id}/{key}", name="chill_main_export_saved_edit_options_from_key") + */ + public function editSavedExportOptionsFromKey(SavedExport $savedExport, string $key): Response + { + $this->denyAccessUnlessGranted('ROLE_USER'); + $user = $this->getUser(); + + if (!$user instanceof User) { + throw new AccessDeniedHttpException(); + } + + $data = $this->rebuildRawData($key); + + $savedExport + ->setOptions($data); + + $this->entityManager->flush(); + + return $this->redirectToRoute('chill_main_export_saved_edit', ['id' => $savedExport->getId()]); + } + /** * @Route("/{_locale}/export/save-from-key/{alias}/{key}", name="chill_main_export_save_from_key") */ @@ -282,45 +311,55 @@ class ExportController extends AbstractController /** * create a form to show on different steps. * - * @param string $alias * @param array $data the data from previous step. Required for steps 'formatter' and 'generate_formatter' - * @param mixed $step */ - protected function createCreateFormExport($alias, $step, $data = []): FormInterface + protected function createCreateFormExport(string $alias, string $step, array $data, ?SavedExport $savedExport): FormInterface { /** @var \Chill\MainBundle\Export\ExportManager $exportManager */ $exportManager = $this->exportManager; $isGenerate = strpos($step, 'generate_') === 0; - $builder = $this->formFactory - ->createNamedBuilder(null, FormType::class, [], [ - 'method' => $isGenerate ? 'GET' : 'POST', - 'csrf_protection' => $isGenerate ? false : true, - ]); + $options = match ($step) { + 'export', 'generate_export' => [ + 'export_alias' => $alias, + 'picked_centers' => $exportManager->getPickedCenters($data['centers']) + ], + 'formatter', 'generate_formatter' => [ + 'export_alias' => $alias, + 'formatter_alias' => $exportManager->getFormatterAlias($data['export']), + 'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']), + ], + default => [ + 'export_alias' => $alias, + ], + }; - // TODO: add a condition to be able to select a regroupment of centers? + $defaultFormData = match ($savedExport) { + null => $this->exportFormHelper->getDefaultData($step, $exportManager->getExport($alias), $options), + default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step, $options), + }; + + $builder = $this->formFactory + ->createNamedBuilder( + null, + FormType::class, + $defaultFormData, + [ + 'method' => $isGenerate ? 'GET' : 'POST', + 'csrf_protection' => !$isGenerate, + ] + ); if ('centers' === $step || 'generate_centers' === $step) { - $builder->add('centers', PickCenterType::class, [ - 'export_alias' => $alias, - ]); + $builder->add('centers', PickCenterType::class, $options); } if ('export' === $step || 'generate_export' === $step) { - $builder->add('export', ExportType::class, [ - 'export_alias' => $alias, - 'picked_centers' => $exportManager->getPickedCenters($data['centers']), - ]); + $builder->add('export', ExportType::class, $options); } if ('formatter' === $step || 'generate_formatter' === $step) { - $builder->add('formatter', FormatterType::class, [ - 'formatter_alias' => $exportManager - ->getFormatterAlias($data['export']), - 'export_alias' => $alias, - 'aggregator_aliases' => $exportManager - ->getUsedAggregatorsAliases($data['export']), - ]); + $builder->add('formatter', FormatterType::class, $options); } $builder->add('submit', SubmitType::class, [ @@ -335,13 +374,8 @@ class ExportController extends AbstractController * * When the method is POST, the form is stored if valid, and a redirection * is done to next step. - * - * @param string $alias - * @param \Chill\MainBundle\Export\DirectExportInterface|\Chill\MainBundle\Export\ExportInterface $export - * - * @return \Symfony\Component\HttpFoundation\Response */ - protected function exportFormStep(Request $request, $export, $alias) + private function exportFormStep(Request $request, DirectExportInterface|ExportInterface $export, string $alias, ?SavedExport $savedExport = null): Response { $exportManager = $this->exportManager; @@ -357,7 +391,7 @@ class ExportController extends AbstractController $export = $exportManager->getExport($alias); - $form = $this->createCreateFormExport($alias, 'export', $data); + $form = $this->createCreateFormExport($alias, 'export', $data, $savedExport); if ($request->getMethod() === 'POST') { $form->handleRequest($request); @@ -379,6 +413,7 @@ class ExportController extends AbstractController $this->generateUrl('chill_main_export_new', [ 'step' => $this->getNextStep('export', $export), 'alias' => $alias, + 'from_saved' => $request->get('from_saved', '') ]) ); } @@ -399,13 +434,8 @@ class ExportController extends AbstractController * * If the form is posted and valid, store the data in session and * redirect to the next step. - * - * @param \Chill\MainBundle\Export\DirectExportInterface|\Chill\MainBundle\Export\ExportInterface $export - * @param string $alias - * - * @return \Symfony\Component\HttpFoundation\Response */ - protected function formatterFormStep(Request $request, $export, $alias) + private function formatterFormStep(Request $request, DirectExportInterface|ExportInterface $export, string $alias, ?SavedExport $savedExport = null): Response { // check we have data from the previous step (export step) $data = $this->session->get('export_step', null); @@ -417,7 +447,7 @@ class ExportController extends AbstractController ]); } - $form = $this->createCreateFormExport($alias, 'formatter', $data); + $form = $this->createCreateFormExport($alias, 'formatter', $data, $savedExport); if ($request->getMethod() === 'POST') { $form->handleRequest($request); @@ -436,6 +466,7 @@ class ExportController extends AbstractController [ 'alias' => $alias, 'step' => $this->getNextStep('formatter', $export), + 'from_saved' => $request->get('from_saved', ''), ] )); } @@ -462,7 +493,7 @@ class ExportController extends AbstractController * * @return \Symfony\Component\HttpFoundation\RedirectResponse */ - protected function forwardToGenerate(Request $request, $export, $alias) + private function forwardToGenerate(Request $request, $export, $alias, ?SavedExport $savedExport) { $dataCenters = $this->session->get('centers_step_raw', null); $dataFormatter = $this->session->get('formatter_step_raw', null); @@ -470,7 +501,9 @@ class ExportController extends AbstractController if (null === $dataFormatter && $export instanceof \Chill\MainBundle\Export\ExportInterface) { return $this->redirectToRoute('chill_main_export_new', [ - 'alias' => $alias, 'step' => $this->getNextStep('generate', $export, true), + 'alias' => $alias, + 'step' => $this->getNextStep('generate', $export, true), + 'from_saved' => $savedExport?->getId() ?? '', ]); } @@ -491,20 +524,24 @@ class ExportController extends AbstractController $this->session->remove('formatter_step_raw'); $this->session->remove('formatter_step'); - return $this->redirectToRoute('chill_main_export_download', ['key' => $key, 'alias' => $alias]); + return $this->redirectToRoute('chill_main_export_download', [ + 'key' => $key, + 'alias' => $alias, + 'from_saved' => $savedExport?->getId(), + ]); } - protected function rebuildData($key) + private function rebuildData($key, ?SavedExport $savedExport) { $rawData = $this->rebuildRawData($key); $alias = $rawData['alias']; - $formCenters = $this->createCreateFormExport($alias, 'generate_centers'); + $formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], $savedExport); $formCenters->submit($rawData['centers']); $dataCenters = $formCenters->getData(); - $formExport = $this->createCreateFormExport($alias, 'generate_export', $dataCenters); + $formExport = $this->createCreateFormExport($alias, 'generate_export', $dataCenters, $savedExport); $formExport->submit($rawData['export']); $dataExport = $formExport->getData(); @@ -512,7 +549,8 @@ class ExportController extends AbstractController $formFormatter = $this->createCreateFormExport( $alias, 'generate_formatter', - $dataExport + $dataExport, + $savedExport ); $formFormatter->submit($rawData['formatter']); $dataFormatter = $formFormatter->getData(); @@ -527,12 +565,12 @@ class ExportController extends AbstractController * * @return Response */ - protected function selectCentersStep(Request $request, $export, $alias) + private function selectCentersStep(Request $request, $export, $alias, ?SavedExport $savedExport = null) { /** @var \Chill\MainBundle\Export\ExportManager $exportManager */ $exportManager = $this->exportManager; - $form = $this->createCreateFormExport($alias, 'centers'); + $form = $this->createCreateFormExport($alias, 'centers', [], $savedExport); if ($request->getMethod() === 'POST') { $form->handleRequest($request); @@ -564,6 +602,7 @@ class ExportController extends AbstractController return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('centers', $export), 'alias' => $alias, + 'from_saved' => $request->get('from_saved', ''), ]); } } @@ -670,4 +709,18 @@ class ExportController extends AbstractController return $rawData; } + + private function getSavedExportFromRequest(Request $request): ?SavedExport + { + $savedExport = match ($savedExportId = $request->query->get('from_saved', '')) { + '' => null, + default => $this->savedExportRepository->find($savedExportId), + }; + + if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) { + throw new AccessDeniedHttpException("saved export edition not allowed"); + } + + return $savedExport; + } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php index 29642dc15..d39c1451c 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php @@ -90,16 +90,14 @@ class OverlapsI extends FunctionNode if ($part instanceof PathExpression) { return sprintf( - "CASE WHEN %s IS NOT NULL THEN %s ELSE '%s'::date END", - $part->dispatch($sqlWalker), + "COALESCE(%s, '%s'::date)", $part->dispatch($sqlWalker), $p ); } return sprintf( - "CASE WHEN %s::date IS NOT NULL THEN %s::date ELSE '%s'::date END", - $part->dispatch($sqlWalker), + "COALESCE(%s::date, '%s'::date)", $part->dispatch($sqlWalker), $p ); diff --git a/src/Bundle/ChillMainBundle/Entity/Center.php b/src/Bundle/ChillMainBundle/Entity/Center.php index 5ca051ec0..0d5402409 100644 --- a/src/Bundle/ChillMainBundle/Entity/Center.php +++ b/src/Bundle/ChillMainBundle/Entity/Center.php @@ -48,12 +48,19 @@ class Center implements HasCenterInterface */ private string $name = ''; + /** + * @var Collection + * @ORM\ManyToMany(targetEntity=Regroupment::class, mappedBy="centers") + */ + private Collection $regroupments; + /** * Center constructor. */ 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 Collection + */ + public function getRegroupments(): Collection + { + return $this->regroupments; + } + /** * @param $name * diff --git a/src/Bundle/ChillMainBundle/Entity/Regroupment.php b/src/Bundle/ChillMainBundle/Entity/Regroupment.php index 96953abf5..c5d9525cf 100644 --- a/src/Bundle/ChillMainBundle/Entity/Regroupment.php +++ b/src/Bundle/ChillMainBundle/Entity/Regroupment.php @@ -22,11 +22,12 @@ use Doctrine\ORM\Mapping as ORM; class Regroupment { /** - * @var Center * @ORM\ManyToMany( - * targetEntity=Center::class + * targetEntity=Center::class, + * inversedBy="regroupments" * ) * @ORM\Id + * @var Collection
            */ private Collection $centers; @@ -52,6 +53,26 @@ class Regroupment $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 { return $this->centers; diff --git a/src/Bundle/ChillMainBundle/Export/AggregatorInterface.php b/src/Bundle/ChillMainBundle/Export/AggregatorInterface.php index 850d29838..8d805f612 100644 --- a/src/Bundle/ChillMainBundle/Export/AggregatorInterface.php +++ b/src/Bundle/ChillMainBundle/Export/AggregatorInterface.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; use Closure; +use Symfony\Component\Form\FormBuilderInterface; /** * Interface for Aggregators. @@ -21,6 +22,16 @@ use Closure; */ interface AggregatorInterface extends ModifierInterface { + /** + * Add a form to collect data from the user. + */ + public function buildForm(FormBuilderInterface $builder); + + /** + * Get the default data, that can be use as "data" for the form + */ + public function getFormDefaultData(): array; + /** * get a callable which will be able to transform the results into * viewable and understable string. diff --git a/src/Bundle/ChillMainBundle/Export/DirectExportInterface.php b/src/Bundle/ChillMainBundle/Export/DirectExportInterface.php index 9703a42de..5bad5bb42 100644 --- a/src/Bundle/ChillMainBundle/Export/DirectExportInterface.php +++ b/src/Bundle/ChillMainBundle/Export/DirectExportInterface.php @@ -11,10 +11,21 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\Response; interface DirectExportInterface extends ExportElementInterface { + /** + * Add a form to collect data from the user. + */ + public function buildForm(FormBuilderInterface $builder); + + /** + * Get the default data, that can be use as "data" for the form + */ + public function getFormDefaultData(): array; + /** * Generate the export. */ diff --git a/src/Bundle/ChillMainBundle/Export/ExportElementInterface.php b/src/Bundle/ChillMainBundle/Export/ExportElementInterface.php index 3b0402f42..b8cca4ed0 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportElementInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportElementInterface.php @@ -19,11 +19,6 @@ use Symfony\Component\Form\FormBuilderInterface; */ interface ExportElementInterface { - /** - * Add a form to collect data from the user. - */ - public function buildForm(FormBuilderInterface $builder); - /** * get a title, which will be used in UI (and translated). * diff --git a/src/Bundle/ChillMainBundle/Export/ExportFormHelper.php b/src/Bundle/ChillMainBundle/Export/ExportFormHelper.php new file mode 100644 index 000000000..5271fb223 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/ExportFormHelper.php @@ -0,0 +1,171 @@ + ['centers' => $this->authorizationHelper->getReachableCenters($export->requiredRole())], + 'export', 'generate_export' => ['export' => $this->getDefaultDataStepExport($export, $options)], + 'formatter', 'generate_formatter' => ['formatter' => $this->getDefaultDataStepFormatter($options)], + default => throw new \LogicException("step not allowed : " . $step), + }; + } + + private function getDefaultDataStepFormatter(array $options): array + { + $formatter = $this->exportManager->getFormatter($options['formatter_alias']); + + return $formatter->getFormDefaultData($options['aggregator_aliases']); + } + + private function getDefaultDataStepExport(ExportInterface|DirectExportInterface $export, array $options): array + { + $data = [ + ExportType::EXPORT_KEY => $export->getFormDefaultData(), + ExportType::FILTER_KEY => [], + ExportType::AGGREGATOR_KEY => [], + ExportType::PICK_FORMATTER_KEY => [], + ]; + + $filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']); + foreach ($filters as $alias => $filter) { + $data[ExportType::FILTER_KEY][$alias] = [ + FilterType::ENABLED_FIELD => false, + 'form' => $filter->getFormDefaultData() + ]; + } + + $aggregators = $this->exportManager + ->getAggregatorsApplyingOn($export, $options['picked_centers']); + foreach ($aggregators as $alias => $aggregator) { + $data[ExportType::AGGREGATOR_KEY][$alias] = [ + 'enabled' => false, + 'form' => $aggregator->getFormDefaultData(), + ]; + } + + if ($export instanceof ExportInterface) { + $allowedFormatters = $this->exportManager + ->getFormattersByTypes($export->getAllowedFormattersTypes()); + $choices = []; + foreach (array_keys(iterator_to_array($allowedFormatters)) as $alias) { + $choices[] = $alias; + } + + $data[ExportType::PICK_FORMATTER_KEY]['alias'] = match (count($choices)) { + 1 => $choices[0], + default => null, + }; + } else { + unset($data[ExportType::PICK_FORMATTER_KEY]); + } + + return $data; + } + + public function savedExportDataToFormData( + SavedExport $savedExport, + string $step, + array $formOptions = [], + ): array { + return match ($step) { + 'centers', 'generate_centers' => $this->savedExportDataToFormDataStepCenter($savedExport), + 'export', 'generate_export' => $this->savedExportDataToFormDataStepExport($savedExport, $formOptions), + 'formatter', 'generate_formatter' => $this->savedExportDataToFormDataStepFormatter($savedExport, $formOptions), + default => throw new \LogicException("this step is not allowed: " . $step), + }; + } + + private function savedExportDataToFormDataStepCenter( + SavedExport $savedExport, + ): array { + $builder = $this->formFactory + ->createBuilder( + FormType::class, + [], + [ + 'csrf_protection' => false, + ] + ); + + $builder->add('centers', PickCenterType::class, [ + 'export_alias' => $savedExport->getExportAlias(), + ]); + $form = $builder->getForm(); + $form->submit($savedExport->getOptions()['centers']); + + return $form->getData(); + } + + private function savedExportDataToFormDataStepExport( + SavedExport $savedExport, + array $formOptions + ): array { + $builder = $this->formFactory + ->createBuilder( + FormType::class, + [], + [ + 'csrf_protection' => false, + ] + ); + + $builder->add('export', ExportType::class, [ + 'export_alias' => $savedExport->getExportAlias(), ...$formOptions + ]); + $form = $builder->getForm(); + $form->submit($savedExport->getOptions()['export']); + + return $form->getData(); + } + + private function savedExportDataToFormDataStepFormatter( + SavedExport $savedExport, + array $formOptions + ): array { + $builder = $this->formFactory + ->createBuilder( + FormType::class, + [], + [ + 'csrf_protection' => false, + ] + ); + + $builder->add('formatter', FormatterType::class, [ + 'export_alias' => $savedExport->getExportAlias(), ...$formOptions + ]); + $form = $builder->getForm(); + $form->submit($savedExport->getOptions()['formatter']); + + return $form->getData(); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/ExportInterface.php b/src/Bundle/ChillMainBundle/Export/ExportInterface.php index d4e456ca6..f357a9fdb 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportInterface.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Export; use Doctrine\ORM\NativeQuery; use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Form\FormBuilderInterface; /** * Interface for Export. @@ -28,6 +29,16 @@ use Doctrine\ORM\QueryBuilder; */ interface ExportInterface extends ExportElementInterface { + /** + * Add a form to collect data from the user. + */ + public function buildForm(FormBuilderInterface $builder); + + /** + * Get the default data, that can be use as "data" for the form + */ + public function getFormDefaultData(): array; + /** * Return which formatter type is allowed for this report. * diff --git a/src/Bundle/ChillMainBundle/Export/ExportManager.php b/src/Bundle/ChillMainBundle/Export/ExportManager.php index f39926083..3f9c839a5 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportManager.php +++ b/src/Bundle/ChillMainBundle/Export/ExportManager.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; use Chill\MainBundle\Form\Type\Export\ExportType; -use Chill\MainBundle\Form\Type\Export\PickCenterType; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Doctrine\ORM\QueryBuilder; use Generator; @@ -114,8 +113,12 @@ class ExportManager * * @return FilterInterface[] a \Generator that contains filters. The key is the filter's alias */ - public function &getFiltersApplyingOn(ExportInterface $export, ?array $centers = null) + public function &getFiltersApplyingOn(ExportInterface|DirectExportInterface $export, ?array $centers = null): iterable { + if ($export instanceof DirectExportInterface) { + return; + } + foreach ($this->filters as $alias => $filter) { if ( in_array($filter->applyOn(), $export->supportsModifiers(), true) @@ -131,11 +134,11 @@ class ExportManager * * @internal This class check the interface implemented by export, and, if ´ListInterface´ is used, return an empty array * - * @return AggregatorInterface[] a \Generator that contains aggretagors. The key is the filter's alias + * @return null|iterable a \Generator that contains aggretagors. The key is the filter's alias */ - public function &getAggregatorsApplyingOn(ExportInterface $export, ?array $centers = null) + public function &getAggregatorsApplyingOn(ExportInterface|DirectExportInterface $export, ?array $centers = null): ?iterable { - if ($export instanceof ListInterface) { + if ($export instanceof ListInterface || $export instanceof DirectExportInterface) { return; } @@ -149,7 +152,7 @@ class ExportManager } } - public function addExportElementsProvider(ExportElementsProviderInterface $provider, $prefix) + public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void { foreach ($provider->getExportElements() as $suffix => $element) { $alias = $prefix . '_' . $suffix; @@ -173,23 +176,16 @@ class ExportManager * add a formatter. * * @internal used by DI - * - * @param string $alias */ - public function addFormatter(FormatterInterface $formatter, $alias) + public function addFormatter(FormatterInterface $formatter, string $alias) { $this->formatters[$alias] = $formatter; } /** * Generate a response which contains the requested data. - * - * @param string $exportAlias - * @param mixed[] $data - * - * @return Response */ - public function generate($exportAlias, array $pickedCentersData, array $data, array $formatterData) + public function generate(string $exportAlias, array $pickedCentersData, array $data, array $formatterData): Response { $export = $this->getExport($exportAlias); $centers = $this->getPickedCenters($pickedCentersData); @@ -288,7 +284,11 @@ class ExportManager return $this->aggregators[$alias]; } - public function getAggregators(array $aliases) + /** + * @param array $aliases + * @return iterable + */ + public function getAggregators(array $aliases): iterable { foreach ($aliases as $alias) { yield $alias => $this->getAggregator($alias); @@ -296,9 +296,11 @@ class ExportManager } /** - * @return string[] the existing type for known exports + * Get the types for known exports + * + * @return list the existing type for known exports */ - public function getExistingExportsTypes() + public function getExistingExportsTypes(): array { $existingTypes = []; @@ -317,10 +319,8 @@ class ExportManager * @param string $alias * * @throws RuntimeException - * - * @return ExportInterface */ - public function getExport($alias) + public function getExport($alias): ExportInterface|DirectExportInterface { if (!array_key_exists($alias, $this->exports)) { throw new RuntimeException("The export with alias {$alias} is not known."); @@ -334,9 +334,9 @@ class ExportManager * * @param bool $whereUserIsGranted if true (default), restrict to user which are granted the right to execute the export * - * @return ExportInterface[] an array where export's alias are keys + * @return iterable an array where export's alias are keys */ - public function getExports($whereUserIsGranted = true) + public function getExports($whereUserIsGranted = true): iterable { foreach ($this->exports as $alias => $export) { if ($whereUserIsGranted) { @@ -354,9 +354,9 @@ class ExportManager * * @param bool $whereUserIsGranted * - * @return array where keys are the groups's name and value is an array of exports + * @return array> where keys are the groups's name and value is an array of exports */ - public function getExportsGrouped($whereUserIsGranted = true): array + public function getExportsGrouped(bool $whereUserIsGranted = true): array { $groups = ['_' => []]; @@ -375,10 +375,8 @@ class ExportManager * @param string $alias * * @throws RuntimeException if the filter is not known - * - * @return FilterInterface */ - public function getFilter($alias) + public function getFilter(string $alias): FilterInterface { if (!array_key_exists($alias, $this->filters)) { throw new RuntimeException("The filter with alias {$alias} is not known."); @@ -390,16 +388,17 @@ class ExportManager /** * get all filters. * - * @param Generator $aliases + * @param array $aliases + * @return iterable $aliases */ - public function getFilters(array $aliases) + public function getFilters(array $aliases): iterable { foreach ($aliases as $alias) { yield $alias => $this->getFilter($alias); } } - public function getFormatter($alias) + public function getFormatter(string $alias): FormatterInterface { if (!array_key_exists($alias, $this->formatters)) { throw new RuntimeException("The formatter with alias {$alias} is not known."); @@ -414,7 +413,7 @@ class ExportManager * @param array $data the data from the export form * @string the formatter alias|null */ - public function getFormatterAlias(array $data) + public function getFormatterAlias(array $data): ?string { if (array_key_exists(ExportType::PICK_FORMATTER_KEY, $data)) { return $data[ExportType::PICK_FORMATTER_KEY]['alias']; @@ -426,9 +425,9 @@ class ExportManager /** * Get all formatters which supports one of the given types. * - * @return Generator + * @return iterable */ - public function getFormattersByTypes(array $types) + public function getFormattersByTypes(array $types): iterable { foreach ($this->formatters as $alias => $formatter) { if (in_array($formatter->getType(), $types, true)) { @@ -445,7 +444,7 @@ class ExportManager * * @return \Chill\MainBundle\Entity\Center[] the picked center */ - public function getPickedCenters(array $data) + public function getPickedCenters(array $data): array { return $data; } @@ -455,9 +454,9 @@ class ExportManager * * @param array $data the data from the export form * - * @return string[] + * @return list */ - public function getUsedAggregatorsAliases(array $data) + public function getUsedAggregatorsAliases(array $data): array { $aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]); @@ -471,9 +470,8 @@ class ExportManager * @param \Chill\MainBundle\Export\ExportElementInterface $element * @param DirectExportInterface|ExportInterface $export * - * @return bool */ - public function isGrantedForElement(ExportElementInterface $element, ?ExportElementInterface $export = null, ?array $centers = null) + public function isGrantedForElement(ExportElementInterface $element, ?ExportElementInterface $export = null, ?array $centers = null): bool { if ($element instanceof ExportInterface || $element instanceof DirectExportInterface) { $role = $element->requiredRole(); @@ -548,13 +546,12 @@ class ExportManager * Check for acl. If an user is not authorized to see an aggregator, throw an * UnauthorizedException. * - * @param type $data * @throw UnauthorizedHttpException if the user is not authorized */ private function handleAggregators( ExportInterface $export, QueryBuilder $qb, - $data, + array $data, array $center ) { $aggregators = $this->retrieveUsedAggregators($data); @@ -600,9 +597,9 @@ class ExportManager /** * @param mixed $data * - * @return AggregatorInterface[] + * @return iterable */ - private function retrieveUsedAggregators($data) + private function retrieveUsedAggregators($data): iterable { if (null === $data) { return []; diff --git a/src/Bundle/ChillMainBundle/Export/FilterInterface.php b/src/Bundle/ChillMainBundle/Export/FilterInterface.php index 5e398c30d..7db850108 100644 --- a/src/Bundle/ChillMainBundle/Export/FilterInterface.php +++ b/src/Bundle/ChillMainBundle/Export/FilterInterface.php @@ -11,6 +11,8 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; +use Symfony\Component\Form\FormBuilderInterface; + /** * Interface for filters. * @@ -23,6 +25,16 @@ interface FilterInterface extends ModifierInterface { public const STRING_FORMAT = 'string'; + /** + * Add a form to collect data from the user. + */ + public function buildForm(FormBuilderInterface $builder); + + /** + * Get the default data, that can be use as "data" for the form + */ + public function getFormDefaultData(): array; + /** * Describe the filtering action. * diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php index c8b20a88b..0e4114849 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php @@ -86,6 +86,11 @@ class CSVFormatter implements FormatterInterface } } + public function getFormDefaultData(array $aggregatorAliases): array + { + return []; + } + public function gatherFiltersDescriptions() { $descriptions = []; diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php index b84c80118..d11aea068 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php @@ -85,10 +85,14 @@ class CSVListFormatter implements FormatterInterface 'expanded' => true, 'multiple' => false, 'label' => 'Add a number on first column', - 'data' => true, ]); } + public function getFormDefaultData(array $aggregatorAliases): array + { + return ['numerotation' => true]; + } + public function getName() { return 'CSV vertical list'; diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/CSVPivotedListFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/CSVPivotedListFormatter.php index 63d691443..ae4d3629f 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/CSVPivotedListFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/CSVPivotedListFormatter.php @@ -84,6 +84,11 @@ class CSVPivotedListFormatter implements FormatterInterface ]); } + public function getFormDefaultData(array $aggregatorAliases): array + { + return ['numerotation' => true]; + } + public function getName() { return 'CSV horizontal list'; diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php index dc09c55a1..6a4b38e84 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php @@ -173,7 +173,19 @@ class SpreadSheetFormatter implements FormatterInterface } } - public function getName() + public function getFormDefaultData(array $aggregatorAliases): array + { + $data = ['format' => 'xlsx']; + + $aggregators = iterator_to_array($this->exportManager->getAggregators($aggregatorAliases)); + foreach (array_keys($aggregators) as $index => $alias) { + $data[$alias] = ['order' => $index + 1]; + } + + return $data; + } + + public function getName(): string { return 'SpreadSheet (xlsx, ods)'; } @@ -185,7 +197,7 @@ class SpreadSheetFormatter implements FormatterInterface array $exportData, array $filtersData, array $aggregatorsData - ) { + ): Response { // store all data when the process is initiated $this->result = $result; $this->formatterData = $formatterData; @@ -574,10 +586,8 @@ class SpreadSheetFormatter implements FormatterInterface * * This form allow to choose the aggregator position (row or column) and * the ordering - * - * @param string $nbAggregators */ - private function appendAggregatorForm(FormBuilderInterface $builder, $nbAggregators) + private function appendAggregatorForm(FormBuilderInterface $builder, int $nbAggregators): void { $builder->add('order', ChoiceType::class, [ 'choices' => array_combine( diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php index 0e5e339ea..b82ab8f61 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php @@ -104,10 +104,14 @@ class SpreadsheetListFormatter implements FormatterInterface 'expanded' => true, 'multiple' => false, 'label' => 'Add a number on first column', - 'data' => true, ]); } + public function getFormDefaultData(array $aggregatorAliases): array + { + return ['numerotation' => true, 'format' => 'xlsx']; + } + public function getName() { return 'Spreadsheet list formatter (.xlsx, .ods)'; diff --git a/src/Bundle/ChillMainBundle/Export/FormatterInterface.php b/src/Bundle/ChillMainBundle/Export/FormatterInterface.php index 1f9df225a..e939c47f2 100644 --- a/src/Bundle/ChillMainBundle/Export/FormatterInterface.php +++ b/src/Bundle/ChillMainBundle/Export/FormatterInterface.php @@ -34,6 +34,11 @@ interface FormatterInterface array $aggregatorAliases ); + /** + * get the default data for the form build by buildForm + */ + public function getFormDefaultData(array $aggregatorAliases): array; + public function getName(); /** diff --git a/src/Bundle/ChillMainBundle/Form/DataMapper/ExportPickCenterDataMapper.php b/src/Bundle/ChillMainBundle/Form/DataMapper/ExportPickCenterDataMapper.php index 01303bfbb..d4dc4719d 100644 --- a/src/Bundle/ChillMainBundle/Form/DataMapper/ExportPickCenterDataMapper.php +++ b/src/Bundle/ChillMainBundle/Form/DataMapper/ExportPickCenterDataMapper.php @@ -20,35 +20,23 @@ use Symfony\Component\Form\FormInterface; use function array_key_exists; use function count; -class ExportPickCenterDataMapper implements DataMapperInterface +final readonly class ExportPickCenterDataMapper implements DataMapperInterface { - protected RegroupmentRepository $regroupmentRepository; - - public function mapDataToForms($data, $forms): void + public function mapDataToForms($viewData, $forms): void { - if (null === $data) { + if (null === $viewData) { return; } /** @var array $form */ $form = iterator_to_array($forms); - $pickedRegroupment = []; + $form['center']->setData($viewData); - foreach ($this->regroupmentRepository->findAll() as $regroupment) { - /** @phpstan-ignore-next-line */ - [$contained, $notContained] = $regroupment->getCenters()->partition(static fn (Center $center): bool => false); - - if (0 === count($notContained)) { - $pickedRegroupment[] = $regroupment; - } - } - - $form['regroupment']->setData($pickedRegroupment); - $form['centers']->setData($data); + // NOTE: we do not map back the regroupments } - public function mapFormsToData($forms, &$data): void + public function mapFormsToData($forms, &$viewData): void { /** @var array $forms */ $forms = iterator_to_array($forms); @@ -68,6 +56,6 @@ class ExportPickCenterDataMapper implements DataMapperInterface } } - $data = array_values($centers); + $viewData = array_values($centers); } } diff --git a/src/Bundle/ChillMainBundle/Form/RegroupmentType.php b/src/Bundle/ChillMainBundle/Form/RegroupmentType.php index bc8e6684f..22814598a 100644 --- a/src/Bundle/ChillMainBundle/Form/RegroupmentType.php +++ b/src/Bundle/ChillMainBundle/Form/RegroupmentType.php @@ -31,7 +31,7 @@ class RegroupmentType extends AbstractType ->add('centers', EntityType::class, [ 'class' => Center::class, 'multiple' => true, - 'attr' => ['class' => 'select2'], + 'expanded' => true, ]) ->add('isActive', CheckboxType::class, [ 'label' => 'Actif ?', diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php index eff303573..1ea01d5f8 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php @@ -32,7 +32,6 @@ class AggregatorType extends AbstractType ->add('enabled', CheckboxType::class, [ 'value' => true, 'required' => false, - 'data' => false, ]); $filterFormBuilder = $builder->create('form', FormType::class, [ diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php index 153f8946b..7994881d5 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php @@ -33,7 +33,6 @@ class FilterType extends AbstractType $builder ->add(self::ENABLED_FIELD, CheckboxType::class, [ 'value' => true, - 'data' => false, 'required' => false, ]); diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php index c89907cf3..c51676b32 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php @@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Export\ExportManager; use Chill\MainBundle\Form\DataMapper\ExportPickCenterDataMapper; use Chill\MainBundle\Repository\RegroupmentRepository; +use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; @@ -33,22 +34,18 @@ final class PickCenterType extends AbstractType { public const CENTERS_IDENTIFIERS = 'c'; - private AuthorizationHelperInterface $authorizationHelper; + private AuthorizationHelperForCurrentUserInterface $authorizationHelper; private ExportManager $exportManager; private RegroupmentRepository $regroupmentRepository; - private UserInterface $user; - public function __construct( - TokenStorageInterface $tokenStorage, ExportManager $exportManager, RegroupmentRepository $regroupmentRepository, - AuthorizationHelperInterface $authorizationHelper + AuthorizationHelperForCurrentUserInterface $authorizationHelper ) { $this->exportManager = $exportManager; - $this->user = $tokenStorage->getToken()->getUser(); $this->authorizationHelper = $authorizationHelper; $this->regroupmentRepository = $regroupmentRepository; } @@ -57,18 +54,19 @@ final class PickCenterType extends AbstractType { $export = $this->exportManager->getExport($options['export_alias']); $centers = $this->authorizationHelper->getReachableCenters( - $this->user, $export->requiredRole() ); + // order alphabetically + usort($centers, fn (Center $a, Center $b) => $a->getCenter() <=> $b->getName()); + $builder->add('center', EntityType::class, [ 'class' => Center::class, - 'label' => 'center', 'choices' => $centers, + 'label' => 'center', 'multiple' => true, 'expanded' => true, 'choice_label' => static fn (Center $c) => $c->getName(), - 'data' => $centers, ]); if (count($this->regroupmentRepository->findAllActive()) > 0) { diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/PickFormatterType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/PickFormatterType.php index ce442231e..b3253ec42 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/PickFormatterType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/PickFormatterType.php @@ -47,8 +47,6 @@ class PickFormatterType extends AbstractType 'multiple' => false, 'placeholder' => 'Choose a format', ]); - - //$builder->get('type')->addModelTransformer($transformer); } public function configureOptions(OptionsResolver $resolver) diff --git a/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php b/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php index c09b845d7..a6e02c4ea 100644 --- a/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php +++ b/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php @@ -34,9 +34,13 @@ class NotificationPresence $this->notificationRepository = $notificationRepository; } - public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array + /** + * @param list $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]; } @@ -46,21 +50,25 @@ class NotificationPresence $counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated( $relatedEntityClass, $relatedEntityId, - $user + $user, + $more ); - $this->cache[$relatedEntityClass][$relatedEntityId] = $counter; + if ([] === $more) { + $this->cache[$relatedEntityClass][$relatedEntityId] = $counter; + } return $counter; } - return ['unread' => 0, 'read' => 0]; + return ['unread' => 0, 'sent' => 0, 'total' => 0]; } /** + * @param list $more * @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(); @@ -68,7 +76,8 @@ class NotificationPresence return $this->notificationRepository->findNotificationByRelatedEntityAndUserAssociated( $relatedEntityClass, $relatedEntityId, - $user + $user, + $more ); } diff --git a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php index 0720c6da6..a8751374e 100644 --- a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php +++ b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php @@ -34,24 +34,30 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface $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( '@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 $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 $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) { return ''; diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php index 72f9ea5d1..fe42578d9 100644 --- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php @@ -29,6 +29,15 @@ final class NotificationRepository implements ObjectRepository 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) { $this->em = $entityManager; @@ -51,29 +60,45 @@ final class NotificationRepository implements ObjectRepository ->getSingleScalarResult(); } - public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array + /** + * @param list $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) { - $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'; + $sqlParams = ['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()]; - $this->notificationByRelatedEntityAndUserAssociatedStatement = - $this->em->getConnection()->prepare($sql); + if ([] === $more) { + 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 - ->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()]); - - $result = $results->fetchAssociative(); - - $results->free(); - - return $result; + return array_map(fn (?int $number) => $number ?? 0, $result); } public function countUnreadByUser(User $user): int @@ -167,8 +192,8 @@ final class NotificationRepository implements ObjectRepository } /** - * @param mixed|null $limit - * @param mixed|null $offset + * @param int|null $limit + * @param int|null $offset * * @return Notification[] */ @@ -178,13 +203,15 @@ final class NotificationRepository implements ObjectRepository } /** + * @param list $more * @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 - $this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user) + $this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user, $more) ->select('n') + ->addOrderBy('n.date', 'DESC') ->getQuery() ->getResult(); } @@ -222,13 +249,36 @@ final class NotificationRepository implements ObjectRepository return Notification::class; } - private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): QueryBuilder + /** + * @param list $more + */ + private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user, array $more = []): QueryBuilder { $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 - ->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass')) - ->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')) + ->setParameter('relatedEntityClass', $relatedEntityClass) + ->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()->orX( @@ -236,8 +286,6 @@ final class NotificationRepository implements ObjectRepository $qb->expr()->eq('n.sender', ':user') ) ) - ->setParameter('relatedEntityClass', $relatedEntityClass) - ->setParameter('relatedEntityId', $relatedEntityId) ->setParameter('user', $user); return $qb; diff --git a/src/Bundle/ChillMainBundle/Repository/RegroupmentRepository.php b/src/Bundle/ChillMainBundle/Repository/RegroupmentRepository.php index a28f2f341..c115d5d98 100644 --- a/src/Bundle/ChillMainBundle/Repository/RegroupmentRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/RegroupmentRepository.php @@ -14,6 +14,8 @@ namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\Regroupment; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ObjectRepository; final class RegroupmentRepository implements ObjectRepository @@ -59,6 +61,30 @@ final class RegroupmentRepository implements ObjectRepository 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 + */ + public function findRegroupmentAssociatedToNoCenter(): array + { + return $this->repository->createQueryBuilder('r') + ->where('SIZE(r.centers) = 0') + ->getQuery() + ->getResult(); + } + public function getClassName() { return Regroupment::class; diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/entity-notification/api.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/entity-notification/api.ts new file mode 100644 index 000000000..ce0671d82 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/entity-notification/api.ts @@ -0,0 +1,20 @@ +const buildLinkCreate = function (relatedEntityClass: string, relatedEntityId: number, to: number | null, returnPath: string | null): string +{ + const params = new URLSearchParams(); + params.append('entityClass', relatedEntityClass); + params.append('entityId', relatedEntityId.toString()); + + if (null !== to) { + params.append('tos[0]', to.toString()); + } + + if (null !== returnPath) { + params.append('returnPath', returnPath); + } + + return `/fr/notification/create?${params.toString()}`; +} + +export { + buildLinkCreate, +} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Export/download.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Export/download.html.twig index 1e69c5a49..bd7dbf7e9 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Export/download.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Export/download.html.twig @@ -1,5 +1,5 @@ {# - * Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS, + * Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS, / * * This program is free software: you can redistribute it and/or modify @@ -19,7 +19,7 @@ {% extends "@ChillMain/layout.html.twig" %} {% block title "Download export"|trans ~ export.title|trans %} - + {% block js %} {% endblock %} - + {% block content %}
            - + {{ include('@ChillMain/Export/_breadcrumb.html.twig') }} - +

            {{ export.title|trans }}

            {{ "Download export"|trans }}

            - +
            {{ 'Back to the list'|trans }} {% if not app.request.query.has('prevent_save') %} + {% if saved_export is null %}
          • {{ 'Save'|trans }}
          • + {% else %} +
          • + +
          • + {% endif %} {% endif %}
      -{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig index d92e4230e..0e64e9f6a 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig @@ -9,4 +9,4 @@ {{ 'notification.counter unread notifications'|trans({'unread': counter.unread }) }} {% endif %} -
      \ No newline at end of file + diff --git a/src/Bundle/ChillMainBundle/Resources/views/SavedExport/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/index.html.twig index fd0cda2a7..7e0517e31 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/SavedExport/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/SavedExport/index.html.twig @@ -22,18 +22,28 @@

      {{ s.export.title|trans }}

      {{ s.saved.title }}

      - +
      {{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}
      - +
      {{ s.saved.description|chill_markdown_to_html }}
      - +
      @@ -54,13 +64,13 @@

      {{ s.export.title|trans }}

      {{ s.saved.title }}

      - +
      {{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}
      - +
      {{ s.saved.description|chill_markdown_to_html }}
      - +
      • diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php index 1105c9d8a..be884de94 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php @@ -195,10 +195,6 @@ class AuthorizationHelper implements AuthorizationHelperInterface /** * Return all reachable scope for a given user, center and role. - * - * @param Center|Center[] $center - * - * @return array|Scope[] */ public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array { diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperForCurrentUserInterface.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperForCurrentUserInterface.php index f0d3f9fba..54e30c244 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperForCurrentUserInterface.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperForCurrentUserInterface.php @@ -25,7 +25,8 @@ interface AuthorizationHelperForCurrentUserInterface public function getReachableCenters(string $role, ?Scope $scope = null): array; /** - * @param array|Center|Center[] $center + * @param list
        |Center $center + * @return list */ - public function getReachableScopes(string $role, $center): array; + public function getReachableScopes(string $role, array|Center $center): array; } diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php index 1176cf1fa..1dc9668ec 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php @@ -26,7 +26,8 @@ interface AuthorizationHelperInterface public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array; /** - * @param Center|list
        $center + * @param Center|array
        $center + * @return list */ public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array; } diff --git a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php index 026ff7a8a..72ad89c58 100644 --- a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php +++ b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php @@ -18,8 +18,12 @@ use UnexpectedValueException; class RollingDateConverter implements RollingDateConverterInterface { - public function convert(RollingDate $rollingDate): DateTimeImmutable + public function convert(?RollingDate $rollingDate): ?DateTimeImmutable { + if (null === $rollingDate) { + return null; + } + switch ($rollingDate->getRoll()) { case RollingDate::T_MONTH_CURRENT_START: return $this->toBeginOfMonth($rollingDate->getPivotDate()); diff --git a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php index b20a5ced2..6c7d9a5bd 100644 --- a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php +++ b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php @@ -15,5 +15,9 @@ use DateTimeImmutable; interface RollingDateConverterInterface { - public function convert(RollingDate $rollingDate): DateTimeImmutable; + /** + * @param RollingDate|null $rollingDate + * @return ($rollingDate is null ? null : DateTimeImmutable) + */ + public function convert(?RollingDate $rollingDate): ?DateTimeImmutable; } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 367cc0861..94aa3c50b 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -91,6 +91,11 @@ class FilterOrderHelper return $this->checkboxes; } + public function hasCheckBox(string $name): bool + { + return array_key_exists($name, $this->checkboxes); + } + /** * @return array<'to': DateTimeImmutable, 'from': DateTimeImmutable> */ diff --git a/src/Bundle/ChillMainBundle/config/services/export.yaml b/src/Bundle/ChillMainBundle/config/services/export.yaml index ea7328839..b0dbf934d 100644 --- a/src/Bundle/ChillMainBundle/config/services/export.yaml +++ b/src/Bundle/ChillMainBundle/config/services/export.yaml @@ -6,6 +6,8 @@ services: Chill\MainBundle\Export\Helper\: resource: '../../Export/Helper' + Chill\MainBundle\Export\ExportFormHelper: ~ + chill.main.export_element_validator: class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator tags: diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 78de523c4..6a6136a41 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -42,6 +42,7 @@ by_user: "par " lifecycleUpdate: Evenements de création et mise à jour address_fields: Données liées à l'adresse Datas: Données +No title: Aucun titre inactive: inactif @@ -598,7 +599,10 @@ saved_export: Export is deleted: L'export est supprimé Saved export is saved!: L'export est enregistré Created on %date%: Créé le %date% - + update_title_and_description: Modifier le titre et la description + update_filters_aggregators_and_execute: Modifier les filtres et regroupements et télécharger + execute: Télécharger + Update existing: Mettre à jour le rapport enregistré existant absence: # single letter for absence diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php index 8d7249928..50e7dcabb 100644 --- a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php @@ -51,7 +51,10 @@ class UserRefEventSubscriber implements EventSubscriberInterface 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()); } } diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodController.php index b32454387..8b4c0b27a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodController.php @@ -219,13 +219,13 @@ class AccompanyingPeriodController extends AbstractController ]); $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); - $accompanyingPeriodsRaw = $this->accompanyingPeriodACLAwareRepository - ->findByPerson($person, AccompanyingPeriodVoter::SEE); + $accompanyingPeriods = $this->accompanyingPeriodACLAwareRepository + ->findByPerson($person, AccompanyingPeriodVoter::SEE, ["openingDate" => "DESC", "id" => "DESC"]); - usort($accompanyingPeriodsRaw, static fn ($a, $b) => $b->getOpeningDate() > $a->getOpeningDate()); + //usort($accompanyingPeriodsRaw, static fn ($a, $b) => $b->getOpeningDate() <=> $a->getOpeningDate()); // filter visible or not visible - $accompanyingPeriods = array_filter($accompanyingPeriodsRaw, fn (AccompanyingPeriod $ap) => $this->isGranted(AccompanyingPeriodVoter::SEE, $ap)); + //$accompanyingPeriods = array_filter($accompanyingPeriodsRaw, fn (AccompanyingPeriod $ap) => $this->isGranted(AccompanyingPeriodVoter::SEE, $ap)); return $this->render('@ChillPerson/AccompanyingPeriod/list.html.twig', [ 'accompanying_periods' => $accompanyingPeriods, diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php index 6bbb6c368..cd550ef59 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php @@ -78,6 +78,7 @@ class AccompanyingPeriodRegulationListController $form['jobs']->getData(), $form['services']->getData(), $form['locations']->getData(), + ['openingDate' => 'DESC', 'id' => 'DESC'], $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber() ); diff --git a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php index bafc4b1cb..3e5b59c2a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php @@ -20,6 +20,7 @@ use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Templating\Entity\UserRender; use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface; use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\CallbackTransformer; @@ -30,6 +31,7 @@ use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Security; @@ -85,8 +87,8 @@ class ReassignAccompanyingPeriodController extends AbstractController */ public function listAction(Request $request): Response { - if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) { - throw new AccessDeniedException(); + if (!$this->security->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK)) { + throw new AccessDeniedHttpException('no right to reassign bulk'); } $form = $this->buildFilterForm(); @@ -96,7 +98,7 @@ class ReassignAccompanyingPeriodController extends AbstractController $userFrom = $form['user']->getData(); $postalCodes = $form['postal_code']->getData() instanceof PostalCode ? [$form['postal_code']->getData()] : []; - $total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod($userFrom); + $total = $this->accompanyingPeriodACLAwareRepository->countByUserAndPostalCodesOpenedAccompanyingPeriod($userFrom, $postalCodes); $paginator = $this->paginatorFactory->create($total); $paginator->setItemsPerPage(50); $periods = $this->accompanyingPeriodACLAwareRepository diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWorkSocialActionApiController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWorkSocialActionApiController.php index 4fe877a72..ae70d55f9 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWorkSocialActionApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWorkSocialActionApiController.php @@ -16,21 +16,19 @@ use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Serializer\Model\Collection; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use function count; -class SocialWorkSocialActionApiController extends ApiController +final class SocialWorkSocialActionApiController extends ApiController { - private PaginatorFactory $paginator; - - private SocialIssueRepository $socialIssueRepository; - - public function __construct(SocialIssueRepository $socialIssueRepository, PaginatorFactory $paginator) - { - $this->socialIssueRepository = $socialIssueRepository; - $this->paginator = $paginator; + public function __construct( + private readonly SocialIssueRepository $socialIssueRepository, + private readonly PaginatorFactory $paginator, + private readonly ClockInterface $clock, + ) { } public function listBySocialIssueApi($id, Request $request) @@ -42,7 +40,10 @@ class SocialWorkSocialActionApiController extends ApiController throw $this->createNotFoundException('socialIssue not found'); } - $socialActions = $socialIssue->getRecursiveSocialActions()->toArray(); + $socialActions = SocialAction::filterRemoveDeactivatedActions( + $socialIssue->getRecursiveSocialActions()->toArray(), + \DateTime::createFromImmutable($this->clock->now()) + ); usort($socialActions, static fn (SocialAction $sa, SocialAction $sb) => $sa->getOrdering() <=> $sb->getOrdering()); diff --git a/src/Bundle/ChillPersonBundle/Controller/UserAccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/UserAccompanyingPeriodController.php index 658fdb7be..1b67014b3 100644 --- a/src/Bundle/ChillPersonBundle/Controller/UserAccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/UserAccompanyingPeriodController.php @@ -12,9 +12,11 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class UserAccompanyingPeriodController extends AbstractController @@ -32,12 +34,24 @@ class UserAccompanyingPeriodController extends AbstractController /** * @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); $accompanyingPeriods = $this->accompanyingPeriodRepository->findBy( - ['user' => $this->getUser(), 'step' => ['CONFIRMED', 'CLOSED']], + ['user' => $this->getUser(), 'step' => $steps], ['openingDate' => 'DESC'], $pagination->getItemsPerPage(), $pagination->getCurrentPageFirstItemNumber() @@ -46,13 +60,14 @@ class UserAccompanyingPeriodController extends AbstractController return $this->render('@ChillPerson/AccompanyingPeriod/user_periods_list.html.twig', [ 'accompanyingPeriods' => $accompanyingPeriods, 'pagination' => $pagination, + 'active' => $active, ]); } /** * @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']); $pagination = $this->paginatorFactory->create($total); diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 569dd1502..121bbba14 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -983,11 +983,8 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac AccompanyingPeriodVoter::EDIT, AccompanyingPeriodVoter::DELETE, ], - AccompanyingPeriodVoter::REASSIGN_BULK => [ - AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, - ], - AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL => [ - AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, + AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL_ALL => [ + AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, ], ], ]); diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index a8e191df3..07e1bafe4 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -185,7 +185,9 @@ class AccompanyingPeriod implements * cascade={"persist", "remove"}, * orphanRemoval=true * ) + * @ORM\OrderBy({"createdAt": "DESC", "id": "DESC"}) * @Assert\NotBlank(groups={AccompanyingPeriod::STEP_DRAFT}) + * @var Collection */ private Collection $comments; @@ -269,6 +271,7 @@ class AccompanyingPeriod implements * cascade={"persist", "refresh", "remove", "merge", "detach"}) * @Groups({"read", "docgen:read"}) * @ParticipationOverlap(groups={AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED}) + * @var Collection */ private Collection $participations; @@ -705,10 +708,11 @@ class AccompanyingPeriod implements ->comments ->filter( static fn (Comment $c): bool => $c !== $pinnedComment - ); + ) + ; } - public function getCreatedAt(): ?DateTime + public function getCreatedAt(): DateTimeInterface { return $this->createdAt; } @@ -782,7 +786,7 @@ class AccompanyingPeriod implements } /** - * @return Collection|AccompanyingPeriodLocationHistory[] + * @return Collection */ public function getLocationHistories(): Collection { @@ -793,6 +797,7 @@ class AccompanyingPeriod implements * Get where the location is. * * @Groups({"read"}) + * @return 'person'|'address'|'none' */ public function getLocationStatus(): string { @@ -870,6 +875,7 @@ class AccompanyingPeriod implements /** * Get Participations Collection. + * @return Collection */ public function getParticipations(): Collection { @@ -1204,6 +1210,7 @@ class AccompanyingPeriod implements $this->addressLocation = $addressLocation; if (null !== $addressLocation) { + $this->setPersonLocation(null); $locationHistory = new AccompanyingPeriodLocationHistory(); $locationHistory ->setStartDate(new DateTimeImmutable('now')) @@ -1322,6 +1329,7 @@ class AccompanyingPeriod implements $this->personLocation = $person; if (null !== $person) { + $this->setAddressLocation(null); $locationHistory = new AccompanyingPeriodLocationHistory(); $locationHistory ->setStartDate(new DateTimeImmutable('now')) diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php index 5361012b3..e3b2883be 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php @@ -59,6 +59,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues * ) * @Serializer\Groups({"read", "docgen:read"}) * @ORM\OrderBy({"startDate": "DESC", "id": "DESC"}) + * @var Collection * * @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer` */ @@ -278,6 +279,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues return $this->accompanyingPeriod; } + /** + * @return Collection + */ public function getAccompanyingPeriodWorkEvaluations(): Collection { return $this->accompanyingPeriodWorkEvaluations; diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluation.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluation.php index 8780f7d17..d35ffc900 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluation.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluation.php @@ -79,6 +79,7 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU * ) * @ORM\OrderBy({"createdAt": "DESC", "id": "DESC"}) * @Serializer\Groups({"read"}) + * @var Collection */ private Collection $documents; @@ -204,7 +205,7 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU } /** - * @return Collection + * @return Collection */ public function getDocuments() { diff --git a/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterCurrent.php b/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterCurrent.php index 6d67fbbbb..6de73de0d 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterCurrent.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person/PersonCenterCurrent.php @@ -45,7 +45,7 @@ class PersonCenterCurrent private ?int $id = null; /** - * @ORM\ManyToOne(targetEntity=Person::class, inversedBy="centerCurrent") + * @ORM\OneToOne(targetEntity=Person::class, inversedBy="centerCurrent") */ private Person $person; diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php index a8c68c8ce..b5b0f9101 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php @@ -15,6 +15,7 @@ use DateInterval; use DateTimeInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ReadableCollection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation as Serializer; @@ -295,6 +296,19 @@ class SocialAction return 0 < $this->getChildren()->count(); } + public function isDesactivated(\DateTime $atDate): bool + { + if (null !== $this->desactivationDate && $this->desactivationDate < $atDate) { + return true; + } + + if ($this->hasParent()) { + return $this->parent->isDesactivated($atDate); + } + + return false; + } + public function hasParent(): bool { return $this->getParent() instanceof self; @@ -401,4 +415,14 @@ class SocialAction return $this; } + + public static function filterRemoveDeactivatedActions(ReadableCollection|array $actions, \DateTime $comparisonDate): ReadableCollection|array + { + $filterFn = fn (SocialAction $socialAction) => !$socialAction->isDesactivated($comparisonDate); + + return match ($actions instanceof ReadableCollection) { + true => $actions->filter($filterFn), + false => array_filter($actions, $filterFn) + }; + } } diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php index 675a1a923..f12627ae3 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php @@ -253,7 +253,7 @@ class SocialIssue } /** - * @return Collection|SocialAction[] All the descendant social actions of all + * @return Collection All the descendant social actions of all * the descendants of the entity */ public function getRecursiveSocialActions(): Collection @@ -272,7 +272,7 @@ class SocialIssue } /** - * @return Collection|SocialAction[] + * @return Collection */ public function getSocialActions(): Collection { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/AdministrativeLocationAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/AdministrativeLocationAggregator.php index 96deac574..56fdc07d7 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/AdministrativeLocationAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/AdministrativeLocationAggregator.php @@ -57,6 +57,10 @@ class AdministrativeLocationAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByActionNumberAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByActionNumberAggregator.php index 2dffccbbb..104b934ca 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByActionNumberAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByActionNumberAggregator.php @@ -39,6 +39,10 @@ class ByActionNumberAggregator implements AggregatorInterface { // No form needed } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingMotiveAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingMotiveAggregator.php index 342958361..73cfdc7b9 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingMotiveAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingMotiveAggregator.php @@ -52,6 +52,10 @@ class ClosingMotiveAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ConfidentialAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ConfidentialAggregator.php index e9b9e28fa..4ad18030e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ConfidentialAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ConfidentialAggregator.php @@ -48,6 +48,10 @@ class ConfidentialAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/CreatorJobAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/CreatorJobAggregator.php index c336a8887..5a060bcd7 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/CreatorJobAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/CreatorJobAggregator.php @@ -57,6 +57,10 @@ class CreatorJobAggregator implements AggregatorInterface { // No form needed } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php index 0dc66e81e..581c7d6e5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php @@ -84,6 +84,10 @@ final class DurationAggregator implements AggregatorInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EmergencyAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EmergencyAggregator.php index 02f9f5cd9..4429c7b62 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EmergencyAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EmergencyAggregator.php @@ -48,6 +48,10 @@ class EmergencyAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EvaluationAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EvaluationAggregator.php index b6436efc0..119fa50b7 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EvaluationAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EvaluationAggregator.php @@ -61,6 +61,10 @@ final class EvaluationAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregator.php index d001cbef7..a83634830 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregator.php @@ -121,7 +121,6 @@ final class GeographicalUnitStatAggregator implements AggregatorInterface ->add('date_calc', PickRollingDateType::class, [ 'label' => 'Compute geographical location at date', 'required' => true, - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('level', EntityType::class, [ 'label' => 'Geographical layer', @@ -133,6 +132,10 @@ final class GeographicalUnitStatAggregator implements AggregatorInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/IntensityAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/IntensityAggregator.php index a3618f40f..ac55d7734 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/IntensityAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/IntensityAggregator.php @@ -48,6 +48,10 @@ class IntensityAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/JobWorkingOnCourseAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/JobWorkingOnCourseAggregator.php new file mode 100644 index 000000000..e93300e85 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/JobWorkingOnCourseAggregator.php @@ -0,0 +1,100 @@ +userJobRepository->find((int) $jobId)) { + return ''; + } + + return $this->translatableStringHelper->localize($job->getLabel()); + }; + } + + public function getQueryKeys($data) + { + return [self::COLUMN_NAME]; + } + + public function getTitle() + { + return 'export.aggregator.course.by_job_working.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('acpinfo', $qb->getAllAliases(), true)) { + $qb->leftJoin( + AccompanyingPeriodInfo::class, + 'acpinfo', + Join::WITH, + 'acp.id = IDENTITY(acpinfo.accompanyingPeriod)' + ); + } + + if (!in_array('acpinfo_user', $qb->getAllAliases(), true)) { + $qb->leftJoin('acpinfo.user', 'acpinfo_user'); + } + + $qb->addSelect('IDENTITY(acpinfo_user.userJob) AS ' . self::COLUMN_NAME); + $qb->addGroupBy(self::COLUMN_NAME); + } + + public function applyOn() + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OriginAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OriginAggregator.php index 37d0c7b20..1d01920d5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OriginAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OriginAggregator.php @@ -59,6 +59,10 @@ final class OriginAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php index a09097fd2..7b826600a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php @@ -81,11 +81,14 @@ final class ReferrerAggregator implements AggregatorInterface { $builder ->add('date_calc', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), 'label' => 'export.aggregator.course.by_referrer.Computation date for referrer', 'required' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php index ccbd21da1..adfd9b489 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php @@ -88,11 +88,14 @@ class ReferrerScopeAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { $builder->add('date_calc', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), 'label' => 'export.aggregator.course.by_user_scope.Computation date for referrer', 'required' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/RequestorAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/RequestorAggregator.php index 7b5cf0edd..3baf9bd33 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/RequestorAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/RequestorAggregator.php @@ -69,6 +69,10 @@ final class RequestorAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeAggregator.php index 8f34661bb..253727c89 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeAggregator.php @@ -57,6 +57,10 @@ final class ScopeAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeWorkingOnCourseAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeWorkingOnCourseAggregator.php new file mode 100644 index 000000000..b9f493af9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeWorkingOnCourseAggregator.php @@ -0,0 +1,101 @@ +scopeRepository->find((int) $scopeId)) { + return ''; + } + + return $this->translatableStringHelper->localize($scope->getName()); + }; + } + + public function getQueryKeys($data) + { + return [self::COLUMN_NAME]; + } + + public function getTitle() + { + return 'export.aggregator.course.by_scope_working.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('acpinfo', $qb->getAllAliases(), true)) { + $qb->leftJoin( + AccompanyingPeriodInfo::class, + 'acpinfo', + Join::WITH, + 'acp.id = IDENTITY(acpinfo.accompanyingPeriod)' + ); + } + + if (!in_array('acpinfo_user', $qb->getAllAliases(), true)) { + $qb->leftJoin('acpinfo.user', 'acpinfo_user'); + } + + $qb->addSelect('IDENTITY(acpinfo_user.mainScope) AS ' . self::COLUMN_NAME); + $qb->addGroupBy(self::COLUMN_NAME); + } + + public function applyOn() + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialActionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialActionAggregator.php index 3bdd6549e..b82b47ad0 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialActionAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialActionAggregator.php @@ -58,6 +58,10 @@ final class SocialActionAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialIssueAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialIssueAggregator.php index 12fa5a454..29fcba4a2 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialIssueAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialIssueAggregator.php @@ -58,6 +58,10 @@ final class SocialIssueAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php index 85cfc3190..7632f72f0 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php @@ -76,9 +76,11 @@ final class StepAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('on_date', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), - ]); + $builder->add('on_date', PickRollingDateType::class, []); + } + public function getFormDefaultData(): array + { + return ['on_date' => new RollingDate(RollingDate::T_TODAY)]; } public function getLabels($key, array $values, $data) diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php index a8acdcf12..13d1e3f83 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php @@ -57,6 +57,10 @@ final class UserJobAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserWorkingOnCourseAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserWorkingOnCourseAggregator.php new file mode 100644 index 000000000..b4941fa01 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserWorkingOnCourseAggregator.php @@ -0,0 +1,100 @@ +userRepository->find((int) $userId)) { + return ''; + } + + return $this->userRender->renderString($user, []); + }; + } + + public function getQueryKeys($data) + { + return [self::COLUMN_NAME]; + } + + public function getTitle() + { + return 'export.aggregator.course.by_user_working.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('acpinfo', $qb->getAllAliases(), true)) { + $qb->leftJoin( + AccompanyingPeriodInfo::class, + 'acpinfo', + Join::WITH, + 'acp.id = IDENTITY(acpinfo.accompanyingPeriod)' + ); + } + + if (!in_array('acpinfo_user', $qb->getAllAliases(), true)) { + $qb->leftJoin('acpinfo.user', 'acpinfo_user'); + } + + $qb->addSelect('acpinfo_user.id AS ' . self::COLUMN_NAME); + $qb->addGroupBy('acpinfo_user.id'); + } + + public function applyOn() + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByEndDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByEndDateAggregator.php index 2f4275c49..cb66fa600 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByEndDateAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByEndDateAggregator.php @@ -75,9 +75,12 @@ class ByEndDateAggregator implements AggregatorInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['frequency' => self::DEFAULT_CHOICE]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByMaxDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByMaxDateAggregator.php index 9283fd9dc..af6dfc465 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByMaxDateAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByMaxDateAggregator.php @@ -75,9 +75,12 @@ class ByMaxDateAggregator implements AggregatorInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['frequency' => self::DEFAULT_CHOICE]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByStartDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByStartDateAggregator.php index cd183b25e..87a6a672b 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByStartDateAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByStartDateAggregator.php @@ -75,9 +75,12 @@ class ByStartDateAggregator implements AggregatorInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['frequency' => self::DEFAULT_CHOICE]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/EvaluationTypeAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/EvaluationTypeAggregator.php index 0c13611cb..a48ed763d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/EvaluationTypeAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/EvaluationTypeAggregator.php @@ -52,6 +52,10 @@ class EvaluationTypeAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/HavingEndDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/HavingEndDateAggregator.php index e33bb326f..e710949fd 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/HavingEndDateAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/HavingEndDateAggregator.php @@ -48,6 +48,10 @@ class HavingEndDateAggregator implements AggregatorInterface { // No form needed } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/ChildrenNumberAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/ChildrenNumberAggregator.php index 0c9bc623f..1c5b3dc2c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/ChildrenNumberAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/ChildrenNumberAggregator.php @@ -72,9 +72,11 @@ class ChildrenNumberAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('on_date', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), - ]); + $builder->add('on_date', PickRollingDateType::class, []); + } + public function getFormDefaultData(): array + { + return ['on_date' => new RollingDate(RollingDate::T_TODAY)]; } public function getLabels($key, array $values, $data) diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/CompositionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/CompositionAggregator.php index 52bcd88e5..62fc20173 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/CompositionAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/CompositionAggregator.php @@ -77,9 +77,11 @@ class CompositionAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('on_date', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), - ]); + $builder->add('on_date', PickRollingDateType::class, []); + } + public function getFormDefaultData(): array + { + return ['on_date' => new RollingDate(RollingDate::T_TODAY)]; } public function getLabels($key, array $values, $data) diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/AgeAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/AgeAggregator.php index 365a73704..e3d364fa2 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/AgeAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/AgeAggregator.php @@ -13,20 +13,18 @@ namespace Chill\PersonBundle\Export\Aggregator\PersonAggregators; use Chill\MainBundle\Export\AggregatorInterface; use Chill\MainBundle\Export\ExportElementValidatedInterface; -use DateTime; +use Chill\MainBundle\Form\Type\PickRollingDateType; +use Chill\MainBundle\Service\RollingDate\RollingDate; +use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Doctrine\ORM\QueryBuilder; -use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Contracts\Translation\TranslatorInterface; -final class AgeAggregator implements AggregatorInterface, ExportElementValidatedInterface +final readonly class AgeAggregator implements AggregatorInterface, ExportElementValidatedInterface { - private TranslatorInterface $translator; - - public function __construct(TranslatorInterface $translator) - { - $this->translator = $translator; + public function __construct( + private RollingDateConverterInterface $rollingDateConverter, + ) { } public function addRole(): ?string @@ -37,7 +35,7 @@ final class AgeAggregator implements AggregatorInterface, ExportElementValidated public function alterQuery(QueryBuilder $qb, $data) { $qb->addSelect('DATE_DIFF(:date_age_calculation, person.birthdate)/365 as person_age'); - $qb->setParameter('date_age_calculation', $data['date_age_calculation']); + $qb->setParameter('date_age_calculation', $this->rollingDateConverter->convert($data['date_age_calculation'])); $qb->addGroupBy('person_age'); } @@ -48,14 +46,14 @@ final class AgeAggregator implements AggregatorInterface, ExportElementValidated public function buildForm(FormBuilderInterface $builder) { - $builder->add('date_age_calculation', DateType::class, [ + $builder->add('date_age_calculation', PickRollingDateType::class, [ 'label' => 'Calculate age in relation to this date', - 'data' => new DateTime(), - 'attr' => ['class' => 'datepicker'], - 'widget' => 'single_text', - 'format' => 'dd-MM-yyyy', ]); } + public function getFormDefaultData(): array + { + return ['date_age_calculation' => new RollingDate(RollingDate::T_TODAY)]; + } public function getLabels($key, array $values, $data) { @@ -64,11 +62,7 @@ final class AgeAggregator implements AggregatorInterface, ExportElementValidated return 'Age'; } - if (null === $value) { - return $this->translator->trans('without data'); - } - - return $value; + return $value ?? ''; }; } diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/ByHouseholdCompositionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/ByHouseholdCompositionAggregator.php index 80f0faa17..16b62c2b5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/ByHouseholdCompositionAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/ByHouseholdCompositionAggregator.php @@ -94,9 +94,12 @@ class ByHouseholdCompositionAggregator implements AggregatorInterface { $builder->add('date_calc', PickRollingDateType::class, [ 'label' => 'export.aggregator.person.by_household_composition.Calc date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php new file mode 100644 index 000000000..9be0b0c7e --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php @@ -0,0 +1,103 @@ +add('at_date', PickRollingDateType::class, [ + 'label' => 'export.aggregator.person.by_center.at_date', + ]); + } + + public function getFormDefaultData(): array + { + return [ + 'at_date' => new RollingDate(RollingDate::T_TODAY) + ]; + } + + public function getLabels($key, array $values, $data): Closure + { + return function (int|string|null $value) { + if (null === $value || '' === $value) { + return ''; + } + + if ('_header' === $value) { + return 'export.aggregator.person.by_center.center'; + } + + return (string) $this->centerRepository->find((int) $value)?->getName(); + }; + } + + public function getQueryKeys($data) + { + return [self::COLUMN_NAME]; + } + + public function getTitle() + { + return 'export.aggregator.person.by_center.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $alias = 'pers_center_agg'; + $atDate = 'pers_center_agg_at_date'; + + $qb->leftJoin('person.centerHistory', $alias); + $qb + ->andWhere( + $qb->expr()->lte($alias.'.startDate', ':'.$atDate), + )->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull($alias.'.endDate'), + $qb->expr()->gt($alias.'.endDate', ':'.$atDate) + ) + ); + $qb->setParameter($atDate, $this->rollingDateConverter->convert($data['at_date'])); + + $qb->addSelect("IDENTITY({$alias}.center) AS " . self::COLUMN_NAME); + $qb->addGroupBy(self::COLUMN_NAME); + } + + public function applyOn() + { + return Declarations::PERSON_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CountryOfBirthAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CountryOfBirthAggregator.php index 3d785b586..90882245e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CountryOfBirthAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CountryOfBirthAggregator.php @@ -108,6 +108,10 @@ final class CountryOfBirthAggregator implements AggregatorInterface, ExportEleme 'multiple' => false, ]); } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GenderAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GenderAggregator.php index f76420047..dbe3d19d3 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GenderAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GenderAggregator.php @@ -48,6 +48,10 @@ final class GenderAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GeographicalUnitAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GeographicalUnitAggregator.php index 5d73c1fdd..a72d91db2 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GeographicalUnitAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GeographicalUnitAggregator.php @@ -93,7 +93,6 @@ class GeographicalUnitAggregator implements AggregatorInterface ->add('date_calc', PickRollingDateType::class, [ 'label' => 'Address valid at this date', 'required' => true, - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('level', EntityType::class, [ 'label' => 'Geographical layer', @@ -105,6 +104,10 @@ class GeographicalUnitAggregator implements AggregatorInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public static function getDefaultAlias(): string { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/HouseholdPositionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/HouseholdPositionAggregator.php index 107634803..63d84d4da 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/HouseholdPositionAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/HouseholdPositionAggregator.php @@ -90,9 +90,12 @@ final class HouseholdPositionAggregator implements AggregatorInterface, ExportEl { $builder->add('date_position', PickRollingDateType::class, [ 'label' => 'Household position in relation to this date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['date_position' => new RollingDate(RollingDate::T_TODAY)]; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/MaritalStatusAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/MaritalStatusAggregator.php index 08bde4bda..508efd3ad 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/MaritalStatusAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/MaritalStatusAggregator.php @@ -56,6 +56,11 @@ final class MaritalStatusAggregator implements AggregatorInterface // no form } + public function getFormDefaultData(): array + { + return []; + } + public function getLabels($key, array $values, $data) { return function ($value): string { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/NationalityAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/NationalityAggregator.php index d7c4097af..57721f55c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/NationalityAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/NationalityAggregator.php @@ -103,6 +103,13 @@ final class NationalityAggregator implements AggregatorInterface, ExportElementV ]); } + public function getFormDefaultData(): array + { + return [ + 'group_by_level' => 'country', + ]; + } + public function getLabels($key, array $values, $data) { $labels = []; diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ActionTypeAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ActionTypeAggregator.php index c1fccb968..66c87d94f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ActionTypeAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ActionTypeAggregator.php @@ -75,6 +75,10 @@ final class ActionTypeAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CurrentActionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CurrentActionAggregator.php index 58fcb2874..539c9f286 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CurrentActionAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CurrentActionAggregator.php @@ -51,6 +51,10 @@ class CurrentActionAggregator implements AggregatorInterface { // No form needed } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalAggregator.php index 8cc6a25da..4c0e8b393 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalAggregator.php @@ -55,6 +55,10 @@ final class GoalAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalResultAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalResultAggregator.php index 17b263d7e..c99ee85ea 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalResultAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalResultAggregator.php @@ -68,6 +68,10 @@ class GoalResultAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/JobAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/JobAggregator.php index cbf07091f..8271c90e3 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/JobAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/JobAggregator.php @@ -57,6 +57,10 @@ final class JobAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ReferrerAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ReferrerAggregator.php index 0fe53b707..047504224 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ReferrerAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ReferrerAggregator.php @@ -57,6 +57,10 @@ final class ReferrerAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ResultAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ResultAggregator.php index 2e46673da..3cce7b283 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ResultAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ResultAggregator.php @@ -55,6 +55,10 @@ final class ResultAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ScopeAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ScopeAggregator.php index 243263b83..0828b5d0d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ScopeAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ScopeAggregator.php @@ -57,6 +57,10 @@ final class ScopeAggregator implements AggregatorInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function getLabels($key, array $values, $data) { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php index 978f88a10..58ef24e42 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php @@ -39,6 +39,10 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface { // Nothing to add here } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { @@ -97,7 +101,6 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface ->andWhere('acp.step != :count_acp_step') ->leftJoin('acp.participations', 'acppart') ->leftJoin('acppart.person', 'person') - ->andWhere('acppart.startDate != acppart.endDate OR acppart.endDate IS NULL') ->andWhere( $qb->expr()->exists( 'SELECT 1 FROM ' . PersonCenterHistory::class . ' acl_count_person_history WHERE acl_count_person_history.person = person diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingPeriodWork.php b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingPeriodWork.php index 122ee14d9..cf9feb3af 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingPeriodWork.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingPeriodWork.php @@ -38,6 +38,10 @@ class CountAccompanyingPeriodWork implements ExportInterface, GroupedExportInter { // No form necessary? } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { @@ -97,7 +101,6 @@ class CountAccompanyingPeriodWork implements ExportInterface, GroupedExportInter ->join('acpw.accompanyingPeriod', 'acp') ->join('acp.participations', 'acppart') ->join('acppart.person', 'person') - ->andWhere('acppart.startDate != acppart.endDate OR acppart.endDate IS NULL') ->andWhere( $qb->expr()->exists( 'SELECT 1 FROM ' . PersonCenterHistory::class . ' acl_count_person_history WHERE acl_count_person_history.person = person diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountEvaluation.php b/src/Bundle/ChillPersonBundle/Export/Export/CountEvaluation.php index 1613b63d3..d9795e3ba 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/CountEvaluation.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountEvaluation.php @@ -37,6 +37,10 @@ class CountEvaluation implements ExportInterface, GroupedExportInterface { // TODO: Implement buildForm() method. } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { @@ -97,7 +101,6 @@ class CountEvaluation implements ExportInterface, GroupedExportInterface ->join('acpw.accompanyingPeriod', 'acp') ->join('acp.participations', 'acppart') ->join('acppart.person', 'person') - ->andWhere('acppart.startDate != acppart.endDate OR acppart.endDate IS NULL') ->andWhere( $qb->expr()->exists( 'SELECT 1 FROM ' . PersonCenterHistory::class . ' acl_count_person_history WHERE acl_count_person_history.person = person diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountHousehold.php b/src/Bundle/ChillPersonBundle/Export/Export/CountHousehold.php index 0b693b9d8..05e32fde6 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/CountHousehold.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountHousehold.php @@ -46,11 +46,14 @@ class CountHousehold implements ExportInterface, GroupedExportInterface { $builder ->add('calc_date', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), 'label' => self::TR_PREFIX . 'Date of calculation of household members', 'required' => false, ]); } + public function getFormDefaultData(): array + { + return ['calc_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountPerson.php b/src/Bundle/ChillPersonBundle/Export/Export/CountPerson.php index 60429ae55..c1800395c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/CountPerson.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountPerson.php @@ -38,6 +38,11 @@ class CountPerson implements ExportInterface, GroupedExportInterface // No form necessary } + public function getFormDefaultData(): array + { + return []; + } + public function getAllowedFormattersTypes() { return [FormatterInterface::TYPE_TABULAR]; @@ -115,9 +120,9 @@ class CountPerson implements ExportInterface, GroupedExportInterface public function supportsModifiers() { return [ - Declarations::PERSON_TYPE, - Declarations::PERSON_IMPLIED_IN, - //Declarations::ACP_TYPE + 'abcde', + //Declarations::PERSON_TYPE, + //Declarations::PERSON_IMPLIED_IN, ]; } } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountPersonWithAccompanyingCourse.php b/src/Bundle/ChillPersonBundle/Export/Export/CountPersonWithAccompanyingCourse.php index e52cc83b1..ae7238d4e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/CountPersonWithAccompanyingCourse.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountPersonWithAccompanyingCourse.php @@ -39,6 +39,10 @@ class CountPersonWithAccompanyingCourse implements ExportInterface, GroupedExpor { // TODO: Implement buildForm() method. } + public function getFormDefaultData(): array + { + return []; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php index 0647460dd..af66ab312 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php @@ -20,6 +20,7 @@ use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Export\Helper\UserHelper; use Chill\MainBundle\Export\ListInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; +use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; @@ -144,6 +145,12 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface 'required' => true, ]); } + public function getFormDefaultData(): array + { + return [ + 'calc_date' => new RollingDate(RollingDate::T_TODAY) + ]; + } public function getAllowedFormattersTypes() { @@ -409,15 +416,11 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface ) ) ) - ->leftJoin(PersonHouseholdAddress::class, 'personAddress', Join::WITH, 'locationHistory.personLocation = personAddress.person') - ->andWhere( - $qb->expr()->orX( - $qb->expr()->isNull('personAddress'), - $qb->expr()->andX( - $qb->expr()->lte('personAddress.validFrom', ':calcDate'), - $qb->expr()->orX($qb->expr()->isNull('personAddress.validTo'), $qb->expr()->gt('personAddress.validTo', ':calcDate')) - ) - ) + ->leftJoin( + PersonHouseholdAddress::class, + 'personAddress', + Join::WITH, + 'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))' ) ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id'); diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriodWork.php b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriodWork.php index 00fa8adb1..c437454c2 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriodWork.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriodWork.php @@ -135,9 +135,12 @@ class ListAccompanyingPeriodWork implements ListInterface, GroupedExportInterfac 'label' => 'export.list.acpw.Date of calculation for associated elements', 'help' => 'export.list.acpw.help_description', 'required' => true, - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['calc_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListEvaluation.php b/src/Bundle/ChillPersonBundle/Export/Export/ListEvaluation.php index f0985436b..c3e273733 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListEvaluation.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListEvaluation.php @@ -123,9 +123,12 @@ class ListEvaluation implements ListInterface, GroupedExportInterface 'label' => 'export.list.eval.Date of calculation for associated elements', 'help' => 'export.list.eval.help_description', 'required' => true, - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['calc_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListHouseholdInPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListHouseholdInPeriod.php index 8894d145d..4dcc89495 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListHouseholdInPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListHouseholdInPeriod.php @@ -75,10 +75,13 @@ class ListHouseholdInPeriod implements ListInterface, GroupedExportInterface ->add('calc_date', PickRollingDateType::class, [ 'label' => 'export.list.household.Date of calculation for associated elements', 'help' => 'export.list.household.help_description', - 'data' => new RollingDate(RollingDate::T_TODAY), 'required' => true, ]); } + public function getFormDefaultData(): array + { + return ['calc_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php index 8145fd658..467bc02ea 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php @@ -107,17 +107,27 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou } }, ])], - 'data' => array_values($choices), ]); // add a date field for addresses $builder->add('address_date', ChillDateType::class, [ 'label' => 'Data valid at this date', 'help' => 'Data regarding center, addresses, and so on will be computed at this date', - 'data' => new DateTimeImmutable(), 'input' => 'datetime_immutable', ]); } + public function getFormDefaultData(): array + { + $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); + + foreach ($this->getCustomFields() as $cf) { + $choices[$this->translatableStringHelper->localize($cf->getName())] + = + $cf->getSlug(); + } + + return ['fields' => array_values($choices), 'address_date' => new DateTimeImmutable()]; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonDuplicate.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonDuplicate.php index e40ffccce..e7c105604 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonDuplicate.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonDuplicate.php @@ -74,9 +74,12 @@ class ListPersonDuplicate implements DirectExportInterface, ExportElementValidat { $builder->add('precision', NumberType::class, [ 'label' => 'Precision', - 'data' => self::PRECISION_DEFAULT_VALUE, ]); } + public function getFormDefaultData(): array + { + return ['precision' => self::PRECISION_DEFAULT_VALUE]; + } public function generate(array $acl, array $data = []): Response { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php index 7cb066e87..370046232 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php @@ -57,7 +57,6 @@ class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterfac { $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - // Add a checkbox to select fields $builder->add('fields', ChoiceType::class, [ 'multiple' => true, 'expanded' => true, @@ -80,17 +79,20 @@ class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterfac } }, ])], - 'data' => array_values($choices), ]); - // add a date field for addresses $builder->add('address_date', ChillDateType::class, [ 'label' => 'Data valid at this date', 'help' => 'Data regarding center, addresses, and so on will be computed at this date', - 'data' => new DateTimeImmutable(), 'input' => 'datetime_immutable', ]); } + public function getFormDefaultData(): array + { + $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); + + return ['fields' => array_values($choices), 'address_date' => new DateTimeImmutable()]; + } public function getAllowedFormattersTypes() { diff --git a/src/Bundle/ChillPersonBundle/Export/Export/StatAccompanyingCourseDuration.php b/src/Bundle/ChillPersonBundle/Export/Export/StatAccompanyingCourseDuration.php index 31ca41cbb..66b47131d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/StatAccompanyingCourseDuration.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/StatAccompanyingCourseDuration.php @@ -41,9 +41,12 @@ class StatAccompanyingCourseDuration implements ExportInterface, GroupedExportIn { $builder->add('closingdate', ChillDateType::class, [ 'label' => 'Closingdate to apply', - 'data' => new DateTime('now'), ]); } + public function getFormDefaultData(): array + { + return ['closingdate' => new DateTime('now')]; + } public function getAllowedFormattersTypes(): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOnDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOnDateFilter.php index 3fa8d711f..fcb4d67fb 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOnDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOnDateFilter.php @@ -67,9 +67,11 @@ class ActiveOnDateFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { $builder - ->add('on_date', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), - ]); + ->add('on_date', PickRollingDateType::class, []); + } + public function getFormDefaultData(): array + { + return ['on_date' => new RollingDate(RollingDate::T_TODAY)]; } public function describeAction($data, $format = 'string'): array diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOneDayBetweenDatesFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOneDayBetweenDatesFilter.php index f713e8a97..ae21dc724 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOneDayBetweenDatesFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOneDayBetweenDatesFilter.php @@ -56,12 +56,12 @@ class ActiveOneDayBetweenDatesFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { $builder - ->add('date_from', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), - ]) - ->add('date_to', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), - ]); + ->add('date_from', PickRollingDateType::class, []) + ->add('date_to', PickRollingDateType::class, []); + } + public function getFormDefaultData(): array + { + return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; } public function describeAction($data, $format = 'string'): array diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php index c74e309b2..aa1abb36e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php @@ -53,6 +53,10 @@ class AdministrativeLocationFilter implements FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php index e57370f30..153b2ecbd 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php @@ -64,6 +64,10 @@ class ClosingMotiveFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ConfidentialFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ConfidentialFilter.php index 8fcd874fe..8811b9930 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ConfidentialFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ConfidentialFilter.php @@ -22,11 +22,11 @@ use Symfony\Contracts\Translation\TranslatorInterface; class ConfidentialFilter implements FilterInterface { private const CHOICES = [ - 'is not confidential' => false, - 'is confidential' => true, + 'is not confidential' => 'false', + 'is confidential' => 'true', ]; - private const DEFAULT_CHOICE = false; + private const DEFAULT_CHOICE = 'false'; private TranslatorInterface $translator; @@ -67,9 +67,12 @@ class ConfidentialFilter implements FilterInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['accepted_confidentials' => self::DEFAULT_CHOICE]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorFilter.php index b13947e60..8a8a24013 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorFilter.php @@ -50,6 +50,10 @@ class CreatorFilter implements FilterInterface 'label' => false, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorJobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorJobFilter.php index f585cfafb..5faeb850f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorJobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorJobFilter.php @@ -69,6 +69,10 @@ class CreatorJobFilter implements FilterInterface 'label' => 'Job', ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EmergencyFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EmergencyFilter.php index afdb717f0..f9d45c9d7 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EmergencyFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EmergencyFilter.php @@ -22,11 +22,11 @@ use Symfony\Contracts\Translation\TranslatorInterface; class EmergencyFilter implements FilterInterface { private const CHOICES = [ - 'is emergency' => true, - 'is not emergency' => false, + 'is emergency' => 'true', + 'is not emergency' => 'false', ]; - private const DEFAULT_CHOICE = false; + private const DEFAULT_CHOICE = 'false'; private TranslatorInterface $translator; @@ -67,9 +67,12 @@ class EmergencyFilter implements FilterInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['accepted_emergency' => self::DEFAULT_CHOICE]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php index ab7f4257e..a6fb3583d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php @@ -75,6 +75,10 @@ class EvaluationFilter implements FilterInterface 'attr' => ['class' => 'select2'], ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php index d35565c80..a20cfc08f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php @@ -100,7 +100,6 @@ class GeographicalUnitStatFilter implements FilterInterface ->add('date_calc', PickRollingDateType::class, [ 'label' => 'Compute geographical location at date', 'required' => true, - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('units', ChoiceType::class, [ 'label' => 'Geographical unit', @@ -114,6 +113,10 @@ class GeographicalUnitStatFilter implements FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoActionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoActionFilter.php index 54c28d027..4820116c0 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoActionFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoActionFilter.php @@ -38,6 +38,10 @@ class HasNoActionFilter implements FilterInterface { // no form } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoReferrerFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoReferrerFilter.php index 4c682a56a..6d01ea7c2 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoReferrerFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoReferrerFilter.php @@ -65,9 +65,12 @@ class HasNoReferrerFilter implements FilterInterface $builder ->add('calc_date', PickRollingDateType::class, [ 'label' => 'Has no referrer on this date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['calc_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasTemporaryLocationFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasTemporaryLocationFilter.php index 615ace4c6..ec3b4e417 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasTemporaryLocationFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasTemporaryLocationFilter.php @@ -92,9 +92,12 @@ class HasTemporaryLocationFilter implements FilterInterface ]) ->add('calc_date', PickRollingDateType::class, [ 'label' => 'export.filter.course.having_temporarily.Calculation date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['calc_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HavingAnAccompanyingPeriodInfoWithinDatesFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HavingAnAccompanyingPeriodInfoWithinDatesFilter.php index 7069a1d80..d50622502 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HavingAnAccompanyingPeriodInfoWithinDatesFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HavingAnAccompanyingPeriodInfoWithinDatesFilter.php @@ -38,13 +38,14 @@ final readonly class HavingAnAccompanyingPeriodInfoWithinDatesFilter implements $builder ->add('start_date', PickRollingDateType::class, [ 'label' => 'export.filter.course.having_info_within_interval.start_date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('end_date', PickRollingDateType::class, [ 'label' => 'export.filter.course.having_info_within_interval.end_date', - 'data' => new RollingDate(RollingDate::T_TODAY), - ]) - ; + ]); + } + public function getFormDefaultData(): array + { + return ['start_date' => new RollingDate(RollingDate::T_TODAY), 'end_date' => new RollingDate(RollingDate::T_TODAY)]; } public function getTitle(): string diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/IntensityFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/IntensityFilter.php index b0c2205a0..1fd05d2b4 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/IntensityFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/IntensityFilter.php @@ -67,9 +67,12 @@ class IntensityFilter implements FilterInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['accepted_intensities' => self::DEFAULT_CHOICE]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php new file mode 100644 index 000000000..63a668b6d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php @@ -0,0 +1,130 @@ +userJobRepository->findAllActive(); + usort($jobs, fn (UserJob $a, UserJob $b) => $this->translatableStringHelper->localize($a->getLabel()) <=> $this->translatableStringHelper->localize($b->getLabel())); + + $builder + ->add('jobs', EntityType::class, [ + 'class' => UserJob::class, + 'choices' => $jobs, + 'choice_label' => fn (UserJob $userJob) => $this->translatableStringHelper->localize($userJob->getLabel()), + 'multiple' => true, + 'expanded' => true, + ]) + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_job_working.Job working after' + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_job_working.Job working before' + ]) + ; + } + + public function getFormDefaultData(): array + { + return [ + 'jobs' => [], + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function getTitle(): string + { + return 'export.filter.course.by_job_working.title'; + } + + public function describeAction($data, $format = 'string'): array + { + return [ + 'export.filter.course.by_job_working.Filtered by job working on course: only %jobs%, between %start_date% and %end_date%', [ + '%jobs%' => implode( + ', ', + array_map( + fn (UserJob $userJob) => $this->translatableStringHelper->localize($userJob->getLabel()), + $data['jobs'] + ) + ), + '%start_date%' => $this->rollingDateConverter->convert($data['start_date'])?->format('d-m-Y'), + '%end_date%' => $this->rollingDateConverter->convert($data['end_date'])?->format('d-m-Y'), + ], + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data): void + { + $ai_alias = 'jobs_working_on_course_filter_acc_info'; + $ai_user_alias = 'jobs_working_on_course_filter_user'; + $ai_jobs = 'jobs_working_on_course_filter_jobs'; + $start = 'acp_jobs_work_on_start'; + $end = 'acp_jobs_work_on_end'; + + $qb + ->andWhere( + $qb->expr()->exists( + "SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " {$ai_alias} JOIN {$ai_alias}.user {$ai_user_alias} " . + "WHERE IDENTITY({$ai_alias}.accompanyingPeriod) = acp.id + AND {$ai_user_alias}.userJob IN (:{$ai_jobs}) + AND {$ai_alias}.infoDate >= :{$start} and {$ai_alias}.infoDate < :{$end} + " + ) + ) + ->setParameter($ai_jobs, $data['jobs']) + ->setParameter($start, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($end, $this->rollingDateConverter->convert($data['end_date'])) + ; + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php index 07ca1de75..69fdd7bc0 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php @@ -38,7 +38,7 @@ class OpenBetweenDatesFilter implements FilterInterface { $clause = $qb->expr()->andX( $qb->expr()->gte('acp.openingDate', ':datefrom'), - $qb->expr()->lte('acp.openingDate', ':dateto') + $qb->expr()->lt('acp.openingDate', ':dateto') ); $qb->andWhere($clause); @@ -54,12 +54,12 @@ class OpenBetweenDatesFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { $builder - ->add('date_from', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_MONTH_PREVIOUS_START), - ]) - ->add('date_to', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), - ]); + ->add('date_from', PickRollingDateType::class, []) + ->add('date_to', PickRollingDateType::class, []); + } + public function getFormDefaultData(): array + { + return ['date_from' => new RollingDate(RollingDate::T_MONTH_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; } public function describeAction($data, $format = 'string'): array diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php index 00febc640..8395c79f8 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php @@ -64,6 +64,10 @@ class OriginFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php index 2a3e5d17e..f0bf65d49 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php @@ -77,11 +77,14 @@ class ReferrerFilter implements FilterInterface 'multiple' => true, ]) ->add('date_calc', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), 'label' => 'export.filter.course.by_referrer.Computation date for referrer', 'required' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/RequestorFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/RequestorFilter.php index 3295a5b57..614889c27 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/RequestorFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/RequestorFilter.php @@ -126,9 +126,12 @@ final class RequestorFilter implements FilterInterface 'multiple' => false, 'expanded' => true, 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]); } + public function getFormDefaultData(): array + { + return ['accepted_choices' => self::DEFAULT_CHOICE]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php new file mode 100644 index 000000000..b9787bf52 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php @@ -0,0 +1,132 @@ +scopeRepository->findAllActive(); + usort($scopes, fn (Scope $a, Scope $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName())); + + $builder + ->add('scopes', EntityType::class, [ + 'class' => Scope::class, + 'choices' => $scopes, + 'choice_label' => fn (Scope $scope) => $this->translatableStringHelper->localize($scope->getName()), + 'multiple' => true, + 'expanded' => true, + ]) + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_scope_working.Scope working after' + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_scope_working.Scope working before' + ]) + ; + } + + public function getFormDefaultData(): array + { + return [ + 'scopes' => [], + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function getTitle(): string + { + return 'export.filter.course.by_scope_working.title'; + } + + public function describeAction($data, $format = 'string'): array + { + return [ + 'export.filter.course.by_scope_working.Filtered by scope working on course: only %scopes%, between %start_date% and %end_date%', [ + '%scopes%' => implode( + ', ', + array_map( + fn (Scope $scope) => $this->translatableStringHelper->localize($scope->getName()), + $data['scopes'] + ) + ), + '%start_date%' => $this->rollingDateConverter->convert($data['start_date'])?->format('d-m-Y'), + '%end_date%' => $this->rollingDateConverter->convert($data['end_date'])?->format('d-m-Y'), + ], + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data): void + { + $ai_alias = 'scopes_working_on_course_filter_acc_info'; + $ai_user_alias = 'scopes_working_on_course_filter_user'; + $ai_scopes = 'scopes_working_on_course_filter_scopes'; + $start = 'acp_scopes_work_on_start'; + $end = 'acp_scopes_work_on_end'; + + $qb + ->andWhere( + $qb->expr()->exists( + "SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " {$ai_alias} JOIN {$ai_alias}.user {$ai_user_alias} " . + "WHERE IDENTITY({$ai_alias}.accompanyingPeriod) = acp.id + AND {$ai_user_alias}.mainScope IN (:{$ai_scopes}) + AND {$ai_alias}.infoDate >= :{$start} and {$ai_alias}.infoDate < :{$end} + " + ) + ) + ->setParameter($ai_scopes, $data['scopes']) + ->setParameter($start, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($end, $this->rollingDateConverter->convert($data['end_date'])) + ; + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php index bc1f368da..0c14ba73a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php @@ -70,6 +70,10 @@ class SocialActionFilter implements FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php index 917e129bf..a793bc548 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php @@ -69,6 +69,10 @@ class SocialIssueFilter implements FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilterBetweenDates.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilterBetweenDates.php new file mode 100644 index 000000000..9033c64b3 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilterBetweenDates.php @@ -0,0 +1,125 @@ + 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'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilterOnDate.php similarity index 78% rename from src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilter.php rename to src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilterOnDate.php index 8ee798c07..357ed6f30 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilterOnDate.php @@ -23,11 +23,15 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Contracts\Translation\TranslatorInterface; use function in_array; -class StepFilter implements FilterInterface +class StepFilterOnDate implements FilterInterface { 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'; @@ -79,7 +83,7 @@ class StepFilter implements FilterInterface $qb->expr()->in(self::A . '.step', ':acp_filter_by_step_steps') ) ->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() @@ -90,30 +94,36 @@ class StepFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { $builder - ->add('accepted_steps', ChoiceType::class, [ + ->add('accepted_steps_multi', ChoiceType::class, [ + 'label' => 'export.filter.course.by_step.steps', 'choices' => self::STEPS, - 'multiple' => false, + 'multiple' => true, 'expanded' => true, - 'empty_data' => self::DEFAULT_CHOICE, - 'data' => self::DEFAULT_CHOICE, ]) ->add('calc_date', PickRollingDateType::class, [ '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') { - $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%', [ - '%step%' => $this->translator->trans($step), + '%step%' => implode(', ', $steps), ]]; } public function getTitle() { - return 'Filter by step'; + return 'export.filter.course.by_step.Filter by step'; } } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php index 05d84d7b2..e7685fe4d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php @@ -96,11 +96,14 @@ class UserJobFilter implements FilterInterface 'label' => 'Job', ]) ->add('date_calc', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), 'label' => 'export.filter.course.by_user_scope.Computation date for referrer', 'required' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php index bbffa4bca..7c8d84499 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php @@ -100,11 +100,14 @@ class UserScopeFilter implements FilterInterface 'expanded' => true, ]) ->add('date_calc', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), 'label' => 'export.filter.course.by_user_scope.Computation date for referrer', 'required' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserWorkingOnCourseFilter.php index 586bb645d..1f9bfc61a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserWorkingOnCourseFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserWorkingOnCourseFilter.php @@ -13,7 +13,10 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Form\Type\PickUserDynamicType; +use Chill\MainBundle\Service\RollingDate\RollingDate; +use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Templating\Entity\UserRender; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Export\Declarations; @@ -27,11 +30,9 @@ use Symfony\Component\Form\FormBuilderInterface; */ readonly class UserWorkingOnCourseFilter implements FilterInterface { - private const AI_ALIAS = 'user_working_on_course_filter_acc_info'; - private const AI_USERS = 'user_working_on_course_filter_users'; - public function __construct( private UserRender $userRender, + private RollingDateConverterInterface $rollingDateConverter, ) { } @@ -40,7 +41,23 @@ readonly class UserWorkingOnCourseFilter implements FilterInterface $builder ->add('users', PickUserDynamicType::class, [ 'multiple' => true, - ]); + ]) + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_user_working.User working after' + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_user_working.User working before' + ]) + ; + } + + public function getFormDefaultData(): array + { + return [ + 'users' => [], + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; } public function getTitle(): string @@ -51,7 +68,7 @@ readonly class UserWorkingOnCourseFilter implements FilterInterface public function describeAction($data, $format = 'string'): array { return [ - 'export.filter.course.by_user_working.Filtered by user working on course: only %users%', [ + 'export.filter.course.by_user_working.Filtered by user working on course: only %users%, between %start_date% and %end_date%', [ '%users%' => implode( ', ', array_map( @@ -59,6 +76,8 @@ readonly class UserWorkingOnCourseFilter implements FilterInterface $data['users'] ) ), + '%start_date%' => $this->rollingDateConverter->convert($data['start_date'])?->format('d-m-Y'), + '%end_date%' => $this->rollingDateConverter->convert($data['end_date'])?->format('d-m-Y'), ], ]; } @@ -70,14 +89,21 @@ readonly class UserWorkingOnCourseFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data): void { + $ai_alias = 'user_working_on_course_filter_acc_info'; + $ai_users = 'user_working_on_course_filter_users'; + $start = 'acp_use_work_on_start'; + $end = 'acp_use_work_on_end'; + $qb ->andWhere( $qb->expr()->exists( - "SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " " . self::AI_ALIAS . " " . - "WHERE " . self::AI_ALIAS . ".user IN (:" . self::AI_USERS .") AND IDENTITY(" . self::AI_ALIAS . ".accompanyingPeriod) = acp.id" + "SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " {$ai_alias} " . + "WHERE {$ai_alias}.user IN (:{$ai_users}) AND IDENTITY({$ai_alias}.accompanyingPeriod) = acp.id AND {$ai_alias}.infoDate >= :{$start} and {$ai_alias}.infoDate < :{$end}" ) ) - ->setParameter(self::AI_USERS, $data['users']) + ->setParameter($ai_users, $data['users']) + ->setParameter($start, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($end, $this->rollingDateConverter->convert($data['end_date'])) ; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByEndDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByEndDateFilter.php index de86604e6..4c019cc73 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByEndDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByEndDateFilter.php @@ -57,13 +57,15 @@ class ByEndDateFilter implements FilterInterface $builder ->add('start_date', PickRollingDateType::class, [ 'label' => 'start period date', - 'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), ]) ->add('end_date', PickRollingDateType::class, [ 'label' => 'end period date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['start_date' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByStartDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByStartDateFilter.php index 27518599c..ea221e87b 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByStartDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/ByStartDateFilter.php @@ -57,13 +57,15 @@ class ByStartDateFilter implements FilterInterface $builder ->add('start_date', PickRollingDateType::class, [ 'label' => 'start period date', - 'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), ]) ->add('end_date', PickRollingDateType::class, [ 'label' => 'end period date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['start_date' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/CurrentEvaluationsFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/CurrentEvaluationsFilter.php index 140f6c3cb..8841c7b9e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/CurrentEvaluationsFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/CurrentEvaluationsFilter.php @@ -37,6 +37,10 @@ class CurrentEvaluationsFilter implements FilterInterface { //no form needed } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php index 6a0c71d55..77bae1b56 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php @@ -64,6 +64,10 @@ final class EvaluationTypeFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/MaxDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/MaxDateFilter.php index 4a65452a1..daa7d1954 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/MaxDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/MaxDateFilter.php @@ -61,6 +61,10 @@ class MaxDateFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/HouseholdFilters/CompositionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/HouseholdFilters/CompositionFilter.php index 22761c158..3a2cb5337 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/HouseholdFilters/CompositionFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/HouseholdFilters/CompositionFilter.php @@ -84,9 +84,11 @@ class CompositionFilter implements FilterInterface 'multiple' => true, 'expanded' => true, ]) - ->add('on_date', PickRollingDateType::class, [ - 'data' => new RollingDate(RollingDate::T_TODAY), - ]); + ->add('on_date', PickRollingDateType::class, []); + } + public function getFormDefaultData(): array + { + return ['on_date' => new RollingDate(RollingDate::T_TODAY)]; } public function describeAction($data, $format = 'string'): array diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php index 7580a37a3..5588d8c15 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php @@ -76,17 +76,19 @@ class AddressRefStatusFilter implements \Chill\MainBundle\Export\FilterInterface ->add('date_calc', PickRollingDateType::class, [ 'label' => 'Compute address at date', 'required' => true, - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('ref_statuses', ChoiceType::class, [ 'label' => 'export.filter.person.by_address_ref_status.Status', 'choices' => [Address::ADDR_REFERENCE_STATUS_TO_REVIEW, Address::ADDR_REFERENCE_STATUS_REVIEWED, Address::ADDR_REFERENCE_STATUS_MATCH], 'choice_label' => fn (string $item) => 'export.filter.person.by_address_ref_status.'.$item, 'multiple' => true, - 'expanded' => true, - 'data' => [Address::ADDR_REFERENCE_STATUS_TO_REVIEW] + 'expanded' => true ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY), 'ref_statuses' => [Address::ADDR_REFERENCE_STATUS_TO_REVIEW]]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php index c05f97ca8..f06e42c1f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php @@ -92,6 +92,15 @@ class AgeFilter implements ExportElementValidatedInterface, FilterInterface ]); } + public function getFormDefaultData(): array + { + return [ + 'min_age' => 0, + 'max_age' => 120, + 'date_calc' => new RollingDate(RollingDate::T_TODAY), + ]; + } + public function describeAction($data, $format = 'string') { return ['Filtered by person\'s age: ' diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/BirthdateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/BirthdateFilter.php index 7ae22ddd8..9a0478b6c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/BirthdateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/BirthdateFilter.php @@ -71,14 +71,16 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac { $builder->add('date_from', PickRollingDateType::class, [ 'label' => 'Born after this date', - 'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), ]); $builder->add('date_to', PickRollingDateType::class, [ 'label' => 'Born before this date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ByHouseholdCompositionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ByHouseholdCompositionFilter.php index 9b59549f1..749897a53 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ByHouseholdCompositionFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ByHouseholdCompositionFilter.php @@ -86,9 +86,12 @@ class ByHouseholdCompositionFilter implements FilterInterface ]) ->add('calc_date', PickRollingDateType::class, [ 'label' => 'export.filter.person.by_composition.Date calc', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['calc_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeadOrAliveFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeadOrAliveFilter.php index 384d6698a..8b2444ca2 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeadOrAliveFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeadOrAliveFilter.php @@ -102,9 +102,12 @@ class DeadOrAliveFilter implements FilterInterface $builder->add('date_calc', PickRollingDateType::class, [ 'label' => 'Filter in relation to this date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeathdateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeathdateFilter.php index 0ac4b3173..7805331a5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeathdateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeathdateFilter.php @@ -72,14 +72,16 @@ class DeathdateFilter implements ExportElementValidatedInterface, FilterInterfac { $builder->add('date_from', PickRollingDateType::class, [ 'label' => 'Death after this date', - 'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), ]); $builder->add('date_to', PickRollingDateType::class, [ 'label' => 'Deathdate before', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GenderFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GenderFilter.php index 739945b98..182006bbc 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GenderFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GenderFilter.php @@ -89,6 +89,10 @@ class GenderFilter implements 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php index 79f5cb2d4..ab9bb08ab 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php @@ -91,7 +91,6 @@ class GeographicalUnitFilter implements \Chill\MainBundle\Export\FilterInterface ->add('date_calc', PickRollingDateType::class, [ 'label' => 'Compute geographical location at date', 'required' => true, - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('units', ChoiceType::class, [ 'label' => 'Geographical unit', @@ -105,6 +104,10 @@ class GeographicalUnitFilter implements \Chill\MainBundle\Export\FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php index 47a75871c..021c22fc6 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php @@ -56,6 +56,10 @@ class MaritalStatusFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/NationalityFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/NationalityFilter.php index b1d77d60e..e5102128b 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/NationalityFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/NationalityFilter.php @@ -67,6 +67,10 @@ class NationalityFilter implements 'placeholder' => 'Choose countries', ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtThirdpartyFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtThirdpartyFilter.php index 21bb40947..8f22750a5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtThirdpartyFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtThirdpartyFilter.php @@ -107,9 +107,12 @@ class ResidentialAddressAtThirdpartyFilter implements FilterInterface $builder->add('date_calc', PickRollingDateType::class, [ 'label' => 'Date during which residential address was valid', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtUserFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtUserFilter.php index bd55c5b80..62b142420 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtUserFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtUserFilter.php @@ -82,9 +82,12 @@ class ResidentialAddressAtUserFilter implements FilterInterface { $builder->add('date_calc', PickRollingDateType::class, [ 'label' => 'Date during which residential address was valid', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutHouseholdComposition.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutHouseholdComposition.php index c4644fc11..9f5ed90b5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutHouseholdComposition.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutHouseholdComposition.php @@ -66,9 +66,12 @@ class WithoutHouseholdComposition implements FilterInterface $builder ->add('calc_date', PickRollingDateType::class, [ 'label' => 'export.filter.person.by_no_composition.Date calc', - 'data' => new RollingDate(RollingDate::T_TODAY), ]); } + public function getFormDefaultData(): array + { + return ['calc_date' => new RollingDate(RollingDate::T_TODAY)]; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php index b13c49e9a..e78b1d021 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php @@ -32,17 +32,18 @@ final readonly class AccompanyingPeriodWorkEndDateBetweenDateFilter implements F $builder ->add('start_date', PickRollingDateType::class, [ 'label' => 'export.filter.work.end_between_dates.start_date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('end_date', PickRollingDateType::class, [ 'label' => 'export.filter.work.end_between_dates.end_date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('keep_null', CheckboxType::class, [ 'label' => 'export.filter.work.end_between_dates.keep_null', 'help' => 'export.filter.work.end_between_dates.keep_null_help', - ]) - ; + ]); + } + public function getFormDefaultData(): array + { + return ['start_date' => new RollingDate(RollingDate::T_TODAY), 'end_date' => new RollingDate(RollingDate::T_TODAY)]; } public function getTitle(): string diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkStartDateBetweenDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkStartDateBetweenDateFilter.php index e9526a9a5..947e6c57c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkStartDateBetweenDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkStartDateBetweenDateFilter.php @@ -32,17 +32,18 @@ final readonly class AccompanyingPeriodWorkStartDateBetweenDateFilter implements $builder ->add('start_date', PickRollingDateType::class, [ 'label' => 'export.filter.work.start_between_dates.start_date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('end_date', PickRollingDateType::class, [ 'label' => 'export.filter.work.start_between_dates.end_date', - 'data' => new RollingDate(RollingDate::T_TODAY), ]) ->add('keep_null', CheckboxType::class, [ 'label' => 'export.filter.work.start_between_dates.keep_null', 'help' => 'export.filter.work.start_between_dates.keep_null_help', - ]) - ; + ]); + } + public function getFormDefaultData(): array + { + return ['start_date' => new RollingDate(RollingDate::T_TODAY), 'end_date' => new RollingDate(RollingDate::T_TODAY)]; } public function getTitle(): string diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CurrentActionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CurrentActionFilter.php index cbdb64d8d..09551a3c5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CurrentActionFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CurrentActionFilter.php @@ -37,14 +37,18 @@ class CurrentActionFilter implements FilterInterface { //no form } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { - return ['Filtered by current action']; + return ['Filtered actions without end date']; } public function getTitle(): string { - return 'Filter by current actions'; + return 'Filter actions without end date'; } } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php index 144bf3260..265c8e24d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php @@ -76,6 +76,10 @@ class JobFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php index 8d0da32fb..bddcfbf9b 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php @@ -57,6 +57,10 @@ class ReferrerFilter implements FilterInterface 'multiple' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string'): array { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php index 315025722..737759c3e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php @@ -76,6 +76,10 @@ class ScopeFilter implements FilterInterface 'expanded' => true, ]); } + public function getFormDefaultData(): array + { + return []; + } public function describeAction($data, $format = 'string') { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/SocialWorkTypeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/SocialWorkTypeFilter.php index 11fd0ed9d..e97834319 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/SocialWorkTypeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/SocialWorkTypeFilter.php @@ -111,6 +111,11 @@ class SocialWorkTypeFilter implements FilterInterface ); } + public function getFormDefaultData(): array + { + return ['action_type' => [], 'goal' => [], 'result' => []]; + } + public function describeAction($data, $format = 'string'): array { $actionTypes = []; diff --git a/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php b/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php index 074c27027..13291bdd1 100644 --- a/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php +++ b/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php @@ -12,7 +12,9 @@ declare(strict_types=1); namespace Chill\PersonBundle\Menu; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Knp\Menu\MenuItem; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; class HouseholdMenuBuilder implements LocalMenuBuilderInterface @@ -22,9 +24,12 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface */ protected $translator; - public function __construct(TranslatorInterface $translator) + private Security $security; + + public function __construct(TranslatorInterface $translator, Security $security) { $this->translator = $translator; + $this->security = $security; } public function buildMenu($menuId, MenuItem $menu, array $parameters): void @@ -53,12 +58,14 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface ], ]) ->setExtras(['order' => 17]); - $menu->addChild($this->translator->trans('household.Accompanying period'), [ - 'route' => 'chill_person_household_accompanying_period', - 'routeParameters' => [ - 'household_id' => $household->getId(), - ], ]) - ->setExtras(['order' => 20]); + if ($this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['household'])) { + $menu->addChild($this->translator->trans('household.Accompanying period'), [ + 'route' => 'chill_person_household_accompanying_period', + 'routeParameters' => [ + 'household_id' => $household->getId(), + ],]) + ->setExtras(['order' => 20]); + } $menu->addChild($this->translator->trans('household.Addresses'), [ 'route' => 'chill_person_household_addresses', diff --git a/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodNotificationHandler.php b/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodNotificationHandler.php index 486209453..2492e9a41 100644 --- a/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodNotificationHandler.php +++ b/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodNotificationHandler.php @@ -27,7 +27,7 @@ final class AccompanyingPeriodNotificationHandler implements NotificationHandler 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 diff --git a/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodWorkEvaluationDocumentNotificationHandler.php b/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodWorkEvaluationDocumentNotificationHandler.php new file mode 100644 index 000000000..4dcb03489 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodWorkEvaluationDocumentNotificationHandler.php @@ -0,0 +1,47 @@ +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; + } +} diff --git a/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodWorkNotificationHandler.php b/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodWorkNotificationHandler.php new file mode 100644 index 000000000..b111c131f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Notification/AccompanyingPeriodWorkNotificationHandler.php @@ -0,0 +1,46 @@ +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; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php index 0aaabd05f..79a7710ff 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php @@ -12,107 +12,93 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository; use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\PostalCode; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; -use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; +use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; +use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; -use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; -use DateTime; - -use DateTimeImmutable; -use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; +use Repository\AccompanyingPeriodACLAwareRepositoryTest; use Symfony\Component\Security\Core\Security; use function count; -final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface +/** + * @see AccompanyingPeriodACLAwareRepositoryTest + */ +final readonly class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface { private AccompanyingPeriodRepository $accompanyingPeriodRepository; - private AuthorizationHelper $authorizationHelper; + private AuthorizationHelperForCurrentUserInterface $authorizationHelper; - private CenterResolverDispatcherInterface $centerResolverDispatcher; + private CenterResolverManagerInterface $centerResolver; private Security $security; public function __construct( AccompanyingPeriodRepository $accompanyingPeriodRepository, Security $security, - AuthorizationHelper $authorizationHelper, - CenterResolverDispatcherInterface $centerResolverDispatcher + AuthorizationHelperForCurrentUserInterface $authorizationHelper, + CenterResolverManagerInterface $centerResolverDispatcher ) { $this->accompanyingPeriodRepository = $accompanyingPeriodRepository; $this->security = $security; $this->authorizationHelper = $authorizationHelper; - $this->centerResolverDispatcher = $centerResolverDispatcher; + $this->centerResolver = $centerResolverDispatcher; } - /** - * @param array|PostalCode[] - * - * @return QueryBuilder - */ - public function buildQueryOpenedAccompanyingCourseByUser(?User $user, array $postalCodes = []) + public function buildQueryOpenedAccompanyingCourseByUserAndPostalCodes(?User $user, array $postalCodes = []): QueryBuilder { $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); $qb->where($qb->expr()->eq('ap.user', ':user')) ->andWhere( $qb->expr()->neq('ap.step', ':draft'), - $qb->expr()->orX( - $qb->expr()->isNull('ap.closingDate'), - $qb->expr()->gt('ap.closingDate', ':now') - ) + $qb->expr()->neq('ap.step', ':closed'), ) ->setParameter('user', $user) - ->setParameter('now', new DateTime('now')) - ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT); + ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT) + ->setParameter('closed', AccompanyingPeriod::STEP_CLOSED); if ([] !== $postalCodes) { - $qb->join('ap.locationHistories', 'location_history') - ->leftJoin(PersonHouseholdAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)') + $qb->join('ap.locationHistories', 'location_history', Join::WITH, 'location_history.endDate IS NULL') + ->leftJoin(Person\PersonCurrentAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)') ->join( Address::class, 'address', Join::WITH, - 'COALESCE(IDENTITY(location_history.addressLocation), IDENTITY(person_address.address)) = address.id' + 'COALESCE(IDENTITY(person_address.address), IDENTITY(location_history.addressLocation)) = address.id' ) + ->join('address.postcode', 'postcode') ->andWhere( - $qb->expr()->orX( - $qb->expr()->isNull('person_address'), - $qb->expr()->andX( - $qb->expr()->lte('person_address.validFrom', ':now'), - $qb->expr()->orX( - $qb->expr()->isNull('person_address.validTo'), - $qb->expr()->lt('person_address.validTo', ':now') - ) - ) - ) + $qb->expr()->in('postcode.code', ':postal_codes') ) - ->andWhere( - $qb->expr()->isNull('location_history.endDate') - ) - ->andWhere( - $qb->expr()->in('address.postcode', ':postal_codes') - ) - ->setParameter('now', new DateTimeImmutable('now'), Types::DATE_IMMUTABLE) - ->setParameter('postal_codes', $postalCodes); + ->setParameter('postal_codes', array_map(fn (PostalCode $postalCode) => $postalCode->getCode(), $postalCodes)); } return $qb; } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int { - $qb = $this->addACLByUnDispatched($this->buildQueryUnDispatched($jobs, $services, $administrativeLocations)); + $qb = $this->addACLMultiCenterOnQuery( + $this->buildQueryUnDispatched($jobs, $services, $administrativeLocations), + $this->buildCenterOnScope() + ); $qb->select('COUNT(ap)'); @@ -125,22 +111,12 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC return 0; } - return $this->buildQueryOpenedAccompanyingCourseByUser($user, $postalCodes) - ->select('COUNT(ap)') - ->getQuery() - ->getSingleScalarResult(); - } + $qb = $this->buildQueryOpenedAccompanyingCourseByUserAndPostalCodes($user, $postalCodes); + $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); - public function countByUserOpenedAccompanyingPeriod(?User $user): int - { - if (null === $user) { - return 0; - } + $qb->select('COUNT(DISTINCT ap)'); - return $this->buildQueryOpenedAccompanyingCourseByUser($user) - ->select('COUNT(ap)') - ->getQuery() - ->getSingleScalarResult(); + return $qb->getQuery()->getSingleScalarResult(); } public function findByPerson( @@ -152,10 +128,14 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC ): array { $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); $scopes = $this->authorizationHelper - ->getReachableCircles( - $this->security->getUser(), + ->getReachableScopes( $role, - $this->centerResolverDispatcher->resolveCenter($person) + $this->centerResolver->resolveCenters($person) + ); + $scopesCanSeeConfidential = $this->authorizationHelper + ->getReachableScopes( + AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, + $this->centerResolver->resolveCenters($person) ); if (0 === count($scopes)) { @@ -165,12 +145,44 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC $qb ->join('ap.participations', 'participation') ->where($qb->expr()->eq('participation.person', ':person')) - ->andWhere( - $qb->expr()->orX( - 'ap.confidential = FALSE', - $qb->expr()->eq('ap.user', ':user') - ) - ) + ->setParameter('person', $person); + + $qb = $this->addACLClauses($qb, $scopes, $scopesCanSeeConfidential); + $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); + + return $qb->getQuery()->getResult(); + } + + public function addOrderLimitClauses(QueryBuilder $qb, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): QueryBuilder + { + if (null !== $orderBy) { + foreach ($orderBy as $field => $order) { + $qb->addOrderBy('ap.' . $field, $order); + } + } + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + return $qb; + } + + /** + * Add clause for scope on a query, based on no + * + * @param QueryBuilder $qb where the accompanying period have the `ap` alias + * @param array $scopesCanSee + * @param array $scopesCanSeeConfidential + * @return QueryBuilder + */ + public function addACLClauses(QueryBuilder $qb, array $scopesCanSee, array $scopesCanSeeConfidential): QueryBuilder + { + $qb ->andWhere( $qb->expr()->orX( $qb->expr()->neq('ap.step', ':draft'), @@ -181,40 +193,67 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC ) ) ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT) - ->setParameter('person', $person) ->setParameter('user', $this->security->getUser()) ->setParameter('creator', $this->security->getUser()); + // add join condition for scopes $orx = $qb->expr()->orX( + // even if the scope is not in one authorized, the user can see the course if it is in DRAFT state $qb->expr()->eq('ap.step', ':draft') ); - foreach ($scopes as $key => $scope) { - $orx->add($qb->expr()->orX( + foreach ($scopesCanSee as $key => $scope) { + // for each scope: + // - either the user is the referrer of the course + // - or the accompanying course is one of the reachable scopes + // - and the parcours is not confidential OR the user is the referrer OR the user can see the confidential course + + $orOnScope = $qb->expr()->orX( $qb->expr()->isMemberOf(':scope_' . $key, 'ap.scopes'), $qb->expr()->eq('ap.user', ':user') - )); + ); + + if (in_array($scope, $scopesCanSeeConfidential, true)) { + $orx->add($orOnScope); + } else { + // we must add a condition: the course is not confidential or the user is the referrer + $andXOnScope = $qb->expr()->andX( + $orOnScope, + $qb->expr()->orX( + 'ap.confidential = FALSE', + $qb->expr()->eq('ap.user', ':user') + ) + ); + $orx->add($andXOnScope); + } $qb->setParameter('scope_' . $key, $scope); - $qb->setParameter('user', $this->security->getUser()); } $qb->andWhere($orx); - return $qb->getQuery()->getResult(); + return $qb; } - public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array + public function buildCenterOnScope(): array { - $qb = $this->addACLByUnDispatched($this->buildQueryUnDispatched($jobs, $services, $administrativeLocations)); + $centerOnScopes = []; + foreach ($this->authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE) as $center) { + $centerOnScopes[] = [ + 'center' => $center, + 'scopeOnRole' => $this->authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center), + 'scopeCanSeeConfidential' => $this->authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center), + ]; + } + return $centerOnScopes; + } + + public function findByUnDispatched(array $jobs, array $services, array $administrativeAdministrativeLocations, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + $qb = $this->buildQueryUnDispatched($jobs, $services, $administrativeAdministrativeLocations); $qb->select('ap'); - if (null !== $limit) { - $qb->setMaxResults($limit); - } - - if (null !== $offset) { - $qb->setFirstResult($offset); - } + $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); + $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); return $qb->getQuery()->getResult(); } @@ -225,76 +264,80 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC return []; } - $qb = $this->buildQueryOpenedAccompanyingCourseByUser($user); - - $qb->setFirstResult($offset) - ->setMaxResults($limit); - - foreach ($orderBy as $field => $direction) { - $qb->addOrderBy('ap.' . $field, $direction); - } + $qb = $this->buildQueryOpenedAccompanyingCourseByUserAndPostalCodes($user, $postalCodes); + $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); + $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); return $qb->getQuery()->getResult(); } /** - * @return array|AccompanyingPeriod[] + * @param QueryBuilder $qb + * @param list, scopeCanSeeConfidential: list}> $centerScopes + * @param bool $allowNoCenter if true, will allow to see the periods linked to person which does not have any center. Very few edge case when some Person are not associated to a center. + * @return QueryBuilder */ - public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array + public function addACLMultiCenterOnQuery(QueryBuilder $qb, array $centerScopes, bool $allowNoCenter = false): QueryBuilder { - if (null === $user) { - return []; - } + $user = $this->security->getUser(); - $qb = $this->buildQueryOpenedAccompanyingCourseByUser($user); - - $qb->setFirstResult($offset) - ->setMaxResults($limit); - - foreach ($orderBy as $field => $direction) { - $qb->addOrderBy('ap.' . $field, $direction); - } - - return $qb->getQuery()->getResult(); - } - - private function addACLByUnDispatched(QueryBuilder $qb): QueryBuilder - { - $centers = $this->authorizationHelper->getReachableCenters( - $this->security->getUser(), - AccompanyingPeriodVoter::SEE - ); - - $orX = $qb->expr()->orX(); - - if (0 === count($centers)) { + if (0 === count($centerScopes) || !$user instanceof User) { return $qb->andWhere("'FALSE' = 'TRUE'"); } - foreach ($centers as $key => $center) { - $scopes = $this->authorizationHelper - ->getReachableCircles( - $this->security->getUser(), - AccompanyingPeriodVoter::SEE, - $center - ); + $orX = $qb->expr()->orX(); + $idx = 0; + foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) { $and = $qb->expr()->andX( - $qb->expr()->exists('SELECT part FROM ' . AccompanyingPeriodParticipation::class . ' part ' . - "JOIN part.person p WHERE part.accompanyingPeriod = ap.id AND p.center = :center_{$key}") + $qb->expr()->exists( + 'SELECT 1 FROM ' . AccompanyingPeriodParticipation::class . " part_{$idx} " . + "JOIN part_{$idx}.person p{$idx} LEFT JOIN p{$idx}.centerCurrent centerCurrent_{$idx} " . + "WHERE part_{$idx}.accompanyingPeriod = ap.id AND (centerCurrent_{$idx}.center = :center_{$idx}" + . ($allowNoCenter ? " OR centerCurrent_{$idx}.id IS NULL)" : ")") + ) ); - $qb->setParameter('center_' . $key, $center); - $orScope = $qb->expr()->orX(); + $qb->setParameter('center_' . $idx, $center); - foreach ($scopes as $skey => $scope) { - $orScope->add( - $qb->expr()->isMemberOf(':scope_' . $key . '_' . $skey, 'ap.scopes') + $orScopeInsideCenter = $qb->expr()->orX( + // even if the scope is not in one authorized, the user can see the course if it is in DRAFT state + $qb->expr()->eq('ap.step', ':draft') + ); + + $idx++; + foreach ($scopes as $scope) { + // for each scope: + // - either the user is the referrer of the course + // - or the accompanying course is one of the reachable scopes + // - and the parcours is not confidential OR the user is the referrer OR the user can see the confidential course + $orOnScope = $qb->expr()->orX( + $qb->expr()->isMemberOf(':scope_' . $idx, 'ap.scopes'), + $qb->expr()->eq('ap.user', ':user_executing') ); - $qb->setParameter('scope_' . $key . '_' . $skey, $scope); + $qb->setParameter('user_executing', $user); + + if (in_array($scope, $scopesCanSeeConfidential, true)) { + $orScopeInsideCenter->add($orOnScope); + } else { + // we must add a condition: the course is not confidential or the user is the referrer + $andXOnScope = $qb->expr()->andX( + $orOnScope, + $qb->expr()->orX( + 'ap.confidential = FALSE', + $qb->expr()->eq('ap.user', ':user_executing') + ) + ); + $orScopeInsideCenter->add($andXOnScope); + } + $qb->setParameter('scope_' . $idx, $scope); + + $idx++; } - $and->add($orScope); + $and->add($orScopeInsideCenter); $orX->add($and); + + $idx++; } return $qb->andWhere($orX); @@ -305,7 +348,7 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC * @param array|Scope[] $services * @param array|Location[] $locations */ - private function buildQueryUnDispatched(array $jobs, array $services, array $locations): QueryBuilder + public function buildQueryUnDispatched(array $jobs, array $services, array $locations): QueryBuilder { $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); @@ -333,8 +376,8 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC $or = $qb->expr()->orX(); foreach ($services as $key => $service) { - $or->add($qb->expr()->isMemberOf(':scope_' . $key, 'ap.scopes')); - $qb->setParameter('scope_' . $key, $service); + $or->add($qb->expr()->isMemberOf(':scopef_' . $key, 'ap.scopes')); + $qb->setParameter('scopef_' . $key, $service); } $qb->andWhere($or); } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php index 0cca1a5f4..7b31887b9 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php @@ -31,28 +31,28 @@ interface AccompanyingPeriodACLAwareRepositoryInterface */ public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int; - public function countByUserOpenedAccompanyingPeriod(?User $user): int; - + /** + * @return array + */ public function findByPerson( Person $person, string $role, ?array $orderBy = [], - ?int $limit = null, - ?int $offset = null + ?int $limit = null, + ?int $offset = null ): array; /** * @param array|UserJob[] $jobs if empty, does not take this argument into account * @param array|Scope[] $services if empty, does not take this argument into account * - * @return array|AccompanyingPeriod[] + * @return list */ - public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array; + public function findByUnDispatched(array $jobs, array $services, array $administrativeAdministrativeLocations, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; /** * @param array|PostalCode[] $postalCodes + * @return list */ public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array; - - public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array; } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/accompanying_period_work.scss b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/accompanying_period_work.scss index a12f1554c..e82029a1f 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/accompanying_period_work.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/chill/scss/accompanying_period_work.scss @@ -1,3 +1,25 @@ +.badge-accompanying-work-type { + display: inline-block; + background-color: #f3f3f3; + + .title_label { + @include chill_badge(#e2793d); + } + + .title_action { + padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x); + margin-right: 1rem; + + font-size: var(--bs-badge-font-size); + font-weight: var(--bs-badge-font-weight); + line-height: 1; + color: var(--bs-badge-color); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + } +} + /// AccompanyingCourse Work Pages div.accompanying-course-work { diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue index 755e4455c..ad386992e 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue @@ -122,7 +122,8 @@ + v-bind:evaluation="e" + v-bind:docAnchorId="this.docAnchorId"> @@ -296,7 +297,21 @@ @go-to-generate-workflow="goToGenerateWorkflow" > - + +
      • + + +
      • +
      - +
      • @@ -85,7 +85,7 @@ export default { Modal, ListWorkflowModal, }, - props: ['evaluation'], + props: ['evaluation', 'docAnchorId'], i18n, data() { return { diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue index 7dcb595a3..4ea00f73a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue @@ -79,8 +79,8 @@
        {{ $t('Documents') }} :
        -
        -
        +
        +
        @@ -92,7 +92,7 @@ :data-key="i" @input="onInputDocumentTitle"/>
        -
        +
        @@ -102,27 +102,32 @@
        -
          -
        • - -
        • +
            +
          • + +
          • - - + +
          • +
          • + + +
          • @@ -187,6 +201,7 @@ import AddAsyncUpload from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUpload import AddAsyncUploadDownloader from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUploadDownloader.vue'; import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue'; import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js'; +import {buildLinkCreate as buildLinkCreateNotification} from 'ChillMainAssets/lib/entity-notification/api'; import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue"; const i18n = { @@ -214,14 +229,17 @@ const i18n = { template_title: "Nom du template", browse: "Ajouter un document", 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 { name: "FormEvaluation", - props: ['evaluation'], + props: ['evaluation', 'docAnchorId'], components: { ckeditor: CKEditor.component, PickTemplate, @@ -260,8 +278,14 @@ export default { }, computed: { ...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() { return this.$store.getters.getTemplatesAvailablesForEvaluation(this.evaluation.evaluation); }, @@ -390,6 +414,20 @@ export default { return this.$store.dispatch('submit', callback) .catch(e => { console.log(e); throw e; }); }, + goToGenerateDocumentNotification(document, tos) { + const callback = (data) => { + let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key); + let updatedDocument = evaluation.documents.find(d => d.key === document.key); + window.location.assign(buildLinkCreateNotification( + 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', + updatedDocument.id, + tos === true ? this.$store.state.work.accompanyingPeriod.user.id : null, + window.location.pathname + window.location.search + window.location.hash + )); + }; + return this.$store.dispatch('submit', callback) + .catch(e => {console.log(e); throw e}); + } }, } @@ -402,4 +440,19 @@ export default { ul.document-upload { 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;} + } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js index 47e4b2d3f..9a46b741a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js @@ -35,6 +35,7 @@ const store = createStore({ referrers: window.accompanyingCourseWork.referrers, isPosting: false, errors: [], + me: null }, getters: { socialAction(state) { @@ -130,6 +131,9 @@ const store = createStore({ } }, mutations: { + setWhoAmiI(state, me) { + state.me = me; + }, setEvaluationsPicked(state, evaluations) { state.evaluationsPicked = evaluations.map((e, index) => { var k = Object.assign(e, { @@ -385,6 +389,19 @@ const store = createStore({ }, }, 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) { commit('updateThirdParty', payload); }, @@ -514,6 +531,7 @@ store.commit('setEvaluationsPicked', window.accompanyingCourseWork.accompanyingP store.dispatch('getReachablesResultsForAction'); store.dispatch('getReachablesGoalsForAction'); store.dispatch('getReachablesEvaluationsForAction'); +store.dispatch('getWhoAmI'); store.state.evaluationsPicked.forEach(evaluation => { store.dispatch('fetchTemplatesAvailablesForEvaluation', evaluation.evaluation) diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue index 0ce554d65..ca9385990 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue @@ -178,7 +178,7 @@