Compare commits

...

279 Commits

Author SHA1 Message Date
b5c9e65986 Merge branch 'refs/heads/master' into 321-text-editor
# Conflicts:
#	src/Bundle/ChillMainBundle/Resources/public/module/ckeditor5/editor_config.ts
#	src/Bundle/ChillMainBundle/Resources/public/module/ckeditor5/index.ts
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Comment.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/WriteComment.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/PersonComment.vue
#	yarn.lock
2025-05-21 20:03:14 +02:00
8b2af35e97 Fix typo 2025-05-21 17:57:35 +02:00
dc44c46667 Fix SocialActionCSVExporterTest.php 2025-05-21 09:29:25 +02:00
ba571c1a69 Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-05-20 10:04:53 +02:00
6a364705f2 Git add desactivation date for social issue csv export 2025-05-20 09:57:07 +02:00
b6d454691a Git add desactivation date for social action csv export 2025-05-20 09:38:39 +02:00
6d7a6932a9 Merge branch '365-correct-activities-works-counters' into 'master'
#365 correct works counter in acc course summary

Closes #365

See merge request Chill-Projet/chill-bundles!826
2025-05-16 14:40:20 +00:00
juminet
2faf194b15 #365 correct works counter in acc course summary 2025-05-16 14:40:19 +00:00
f207599d86 Merge branch '352-remove-wopi-link-module' into 'master'
Resolve "Code mort: module wopi-link semble inutilisé"

Closes #352

See merge request Chill-Projet/chill-bundles!824
2025-05-16 08:33:57 +00:00
b0959f8cc5 Resolve "Code mort: module wopi-link semble inutilisé" 2025-05-16 08:33:56 +00:00
4c5dee5f0a Fix pipeline 2025-05-14 17:31:48 +02:00
f6c98aa0d5 Add missing translation for user_group.no_user_groups 2025-05-14 14:53:50 +02:00
6d13d184d5 Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-05-14 13:57:22 +02:00
af36eccfaf Allow more characters for maritalstatus id 2025-05-14 13:56:36 +02:00
483a20a43f Merge branch '374-task-default-filters' into 'master'
Resolve "Module tâche: enlever les filtres par défaut"

Closes #374

See merge request Chill-Projet/chill-bundles!819
2025-05-13 08:59:01 +00:00
6d8e2ad825 Resolve "Module tâche: enlever les filtres par défaut" 2025-05-13 08:59:01 +00:00
86388a63a8 Merge branch '381-uncoherent-display-persons-acc-course-works' into 'master'
381 display previous person participation in acc course work

Closes #381

See merge request Chill-Projet/chill-bundles!823
2025-05-13 08:45:38 +00:00
juminet
5ea55ebfe5 381 display previous person participation in acc course work 2025-05-13 08:45:38 +00:00
f97dc8f931 Merge branch '377-document-file-name' into 'master'
377 - add the document filename to document title when uploading a new document

Closes #377

See merge request Chill-Projet/chill-bundles!821
2025-05-13 08:30:31 +00:00
juminet
a9c3aab528 377 - add the document filename to document title when uploading a new document 2025-05-13 08:30:30 +00:00
1181377bd6 Merge branch '376-typeerror-doc-history' into 'master'
#376 prevent typerror in doc-history + improve display of document history

Closes #376

See merge request Chill-Projet/chill-bundles!820
2025-05-13 07:50:53 +00:00
juminet
2275b7c560 #376 prevent typerror in doc-history + improve display of document history 2025-05-13 07:50:52 +00:00
4a8d298ae5 Fix typing error for the display of text in calendar events 2025-05-05 10:42:04 +02:00
3e7f03d331 Add possibility to cancel a workflow if there is a pending signature [ci-skip] 2025-04-17 14:23:32 +02:00
b830952b9e Release v3.11.0 2025-04-17 14:22:35 +02:00
ad17313c61 Merge branch '380-remove-not-null-constraint-household-composition' into 'master'
Remove "not null" validation on HouseholdComposition properties

Closes #380

See merge request Chill-Projet/chill-bundles!818
2025-04-17 09:03:15 +00:00
620515ad15 Remove "not null" validation on HouseholdComposition properties
This change removes the "not null" constraint on specific properties in the HouseholdComposition entity to allow null values. The adjustment addresses Issue #380 and ensures better flexibility without impacting the schema.
2025-04-17 10:56:45 +02:00
50c377ee22 Merge branch 'fix/fix-stored-object-version-not-delete-when-not-exists-on-disk' into 'master'
Fix error when cleaning non-existent stored object versions

See merge request Chill-Projet/chill-bundles!816
2025-04-17 08:36:36 +00:00
cc7e7a90ee Merge branch 'fix/cancel-stale-workflow-handle-fails-silently' into 'master'
Add consistent LOG_PREFIX and key to CancelStaleWorkflowHandler logs

See merge request Chill-Projet/chill-bundles!817
2025-04-16 19:10:37 +00:00
1d4ef19051 Add key to log messages context and add log prefix to enhance CancelStaleWorkflowHandler messages
Introduced a consistent LOG_PREFIX to all log messages in CancelStaleWorkflowHandler. This ensures clearer contextual identification in logs and improves traceability when debugging or monitoring workflow actions.
2025-04-16 21:03:29 +02:00
8337a724d1 Fix error when cleaning non-existent stored object versions
Prevent the `RemoveOldVersionMessageHandler` from throwing errors when the stored object version is missing on disk. Introduced a check to log a notice instead of attempting deletion in such cases and added corresponding test coverage.
2025-04-16 16:26:02 +02:00
8ca377d5d4 Merge branch 'button-signature-zone' into 'master'
Add button unique signature zone

See merge request Chill-Projet/chill-bundles!812
2025-04-15 13:09:54 +00:00
224e0bae43 Add button unique signature zone 2025-04-15 13:09:54 +00:00
3aa4fac80d Merge branch '364-tel2-third-party' into 'master'
Adding a second phone number to thirdparty entity

Closes #364

See merge request Chill-Projet/chill-bundles!810
2025-04-15 12:59:58 +00:00
juminet
a7517eb647 Adding a second phone number to thirdparty entity 2025-04-15 12:59:57 +00:00
e278e636e0 Merge branch '365-add-activities-works-counter' into 'master'
#365 Add works and activities counter

Closes #365

See merge request Chill-Projet/chill-bundles!809
2025-04-14 09:34:02 +00:00
juminet
40e373a9c7 #365 Add works and activities counter 2025-04-14 09:34:02 +00:00
1c1f418b18 Merge branch 'fix/acc-period-step-change-shorten-elapsed' into 'master'
Adjust cronjob interval to ensure daily execution

See merge request Chill-Projet/chill-bundles!814
2025-04-10 13:49:11 +00:00
bf0e14b43a Merge branch '102-liste-des-document-titre-long' into 'master'
Fix graphical bug in document list with title overflowing the frame, and add new classes to display title and aside in flex-table (DX + Fix)

Closes #102 and #22

See merge request Chill-Projet/chill-bundles!815
2025-04-10 13:47:40 +00:00
203a098054 Refactor document row layouts to use CSS grid
Replaced the old 'item-col' structure with a 'item-two-col-grid' layout across multiple templates, improving consistency and responsiveness. Introduced CSS grid styles ensuring proper alignment and wrapping of titles and aside elements in different viewport sizes. This enhances the overall readability and maintainability of the views.
2025-04-10 15:41:06 +02:00
d58acff541 Add css layout for badges for accompanying period work, activity and calendar 2025-04-10 15:41:06 +02:00
5858e05a42 Replace grid in person_list document to fit the whole width of the page 2025-04-10 15:31:06 +02:00
b9b4fafe14 Adjust cronjob interval to ensure daily execution
The interval for `AccompanyingPeriodStepChangeCronjob` was reduced from 24 hours to 23 hours and 45 minutes. This change guarantees at least one execution per day, addressing potential issues with timing overlaps.
2025-04-09 21:30:41 +02:00
fe6949ea26 Update chill bundles to v3.10.3 2025-03-18 13:47:31 +01:00
8a444a12f4 Turn off eslint error ban-ts-comment 2025-03-18 10:57:24 +01:00
8b7b5ceed7 Release v3.10.2 2025-03-17 22:30:17 +01:00
b0e826d05a Revert "Remove unnecessary ts-expect-error"
This reverts commit 7f101ba616.
2025-03-17 22:22:37 +01:00
6fb9c3af3f Release v3.10.1 2025-03-17 21:59:32 +01:00
7f101ba616 Remove unnecessary ts-expect-error 2025-03-17 21:59:16 +01:00
cea82fac10 Merge branch '333-removing-node-deps-to-symfony-ux-translator' into 'master'
Copy Symfony UX Translator module into to chill-bundles

Closes #333

See merge request Chill-Projet/chill-bundles!808
2025-03-17 20:45:48 +00:00
1344fc33e1 Copy Symfony UX Translator module into to chill-bundles 2025-03-17 20:45:48 +00:00
c8e09a28e6 Eslint fixes 2025-03-17 17:32:28 +01:00
c52d4b2a0e Release v3.10.0 2025-03-17 16:41:07 +01:00
7f326d5441 Fix typing of argument in FullCAlendar slot 2025-03-17 16:29:59 +01:00
2840c06476 Merge branch '368-fix-user-search-engine' into 'master'
Fix search query for user-groups

Closes #368

See merge request Chill-Projet/chill-bundles!806
2025-03-14 14:10:53 +00:00
7ddf84ea5a Fix ts errors upon prod compilation 2025-03-14 15:07:23 +01:00
f202625ea8 Fix LIKE clause logic in UserGroupRepository query
Adjusted the parameter order and ensured consistent use of LOWER and UNACCENT functions in the LIKE clause. This resolves potential mismatches and improves query reliability for pattern matching.
2025-03-14 15:04:22 +01:00
7a9168fcdb Refactor variable declarations in pick-entity module.
Consolidated variable declarations into a single statement using const. This improves code readability and aligns with modern JavaScript best practices.
2025-03-14 15:02:09 +01:00
40eb71f95a Merge branch '362-bug-manager-registry-fix2' into 'master'
Resolve loading of manager registry

Closes #362

See merge request Chill-Projet/chill-bundles!803
2025-03-05 16:45:08 +00:00
84b7cc8145 Php cs fixes 2025-03-05 10:46:36 +01:00
08af530726 Replace deprecated Statement::create() method use constructor directly 2025-03-05 10:26:22 +01:00
03b2496817 Fix dependency injection issues in AbstractCRUDController
Replaced incorrect service definitions in AbstractCRUDController to ensure proper dependency injection. Specifically, fixed retrieval of the ManagerRegistry and Validator services to resolve CalendarRange save errors (Issue #362). No schema changes were introduced.
2025-03-04 23:02:19 +01:00
f638ce71fd Merge branch '363-display_actions_by_issue' into 'master'
Resolve "Change display of social issues and social actions"

Closes #363

See merge request Chill-Projet/chill-bundles!802
2025-03-04 14:40:48 +00:00
b39997f00a Resolve "Change display of social issues and social actions" 2025-03-04 14:40:48 +00:00
38b21a2159 Update chill to version v3.9.2 2025-02-27 16:20:03 +01:00
17db571244 Add changie and linter fixes 2025-02-27 16:19:21 +01:00
8c8c16c1a1 use fetchResult instead of makeFetch for acpw creation 2025-02-27 16:16:54 +01:00
046d3ec9f1 use fetchResult instead of makeFetch for SocialIssuesAcc.vue component 2025-02-27 16:12:00 +01:00
596833f1a5 use fetchResult instead of makeFetch 2025-02-27 15:58:53 +01:00
66e4bab558 Fix pipeline phpunit and lint 2025-02-27 13:50:35 +01:00
d1c9926bb1 Update bundles to version v3.9.1 2025-02-27 12:21:26 +01:00
a71d066765 Fix post/path requests with missing 'type' property for gender 2025-02-27 12:19:10 +01:00
217ac7b9e7 Linter fixes 2025-02-27 11:54:48 +01:00
be901822bc Update chill version to v3.9.0 2025-02-27 11:26:25 +01:00
2dcce7b826 Remove dumps 2025-02-25 15:29:45 +01:00
7ed10efcd1 Fix declaration of variables for dynamic picker 2025-02-24 13:49:33 +01:00
350661a4fa Merge branch '341-export-csv-thirdparties' into 'master'
Resolve "Extraction de la liste des tiers au format CSV"

Closes #341

See merge request Chill-Projet/chill-bundles!789
2025-02-19 11:37:46 +00:00
08207b656a Resolve "Extraction de la liste des tiers au format CSV" 2025-02-19 11:37:46 +00:00
8de63de6d6 Merge branch '343-social_issue_action_export' into 'master'
Resolve "Permettre de télécharger la liste des problématiques et la liste des actions en CSV"

Closes #343

See merge request Chill-Projet/chill-bundles!785
2025-02-19 11:18:05 +00:00
51804b10c0 Resolve "Permettre de télécharger la liste des problématiques et la liste des actions en CSV" 2025-02-19 11:18:04 +00:00
02f555efae Merge branch '333-working-with-translations' into 'master'
Setup alias for use in standalone chill-bundles project and replace relative paths

Closes #333

See merge request Chill-Projet/chill-bundles!766
2025-02-19 10:57:59 +00:00
d2fcb6945b Setup alias for use in standalone chill-bundles project and replace relative paths 2025-02-19 10:57:59 +00:00
dcd1777a70 Pass content of the textEditor to the symfony form 2025-02-17 16:25:17 +01:00
a6eb28175a Revert backend to original state and load commentEditor vue app anytime CommentType form field is used 2025-02-17 14:13:07 +01:00
c89e3785ef Merge branch '360-restore-document-on-workflow-cancel' into 'master'
Add event subscriber for document restoration on cancel

Closes #360

See merge request Chill-Projet/chill-bundles!798
2025-02-17 12:22:57 +00:00
9f17ec4841 Merge branch '353-doc_require_to_populate_languages' into 'master'
doc: add languages population command

Closes #353

See merge request Chill-Projet/chill-bundles!792
2025-02-17 12:22:25 +00:00
b277a7749a Merge branch '357-doc_address-ref-requires-language' into 'master'
doc: chill:main:address-ref-from-best-addresse requires a language code

Closes #357

See merge request Chill-Projet/chill-bundles!793
2025-02-17 12:21:48 +00:00
c8b6b6e33a Merge branch '349-suggest-agents-traitants' into 'master'
Resolve "Proposer en plus du référent de parcours les agents traitants saisis dans toutes les cations d'accompagnement du parcours"

Closes #349

See merge request Chill-Projet/chill-bundles!787
2025-02-17 12:21:10 +00:00
10eaebf610 Resolve "Proposer en plus du référent de parcours les agents traitants saisis dans toutes les cations d'accompagnement du parcours" 2025-02-17 12:21:10 +00:00
7d78512823 Change symfony form back to original 2025-02-17 13:11:03 +01:00
0a34f9086f Add event subscriber for document restoration on cancel
Implement an event subscriber to restore documents to their last kept version when a workflow transition ends in a non-positive final state. Includes corresponding unit tests and an unreleased feature change log entry.
2025-02-14 15:08:42 +01:00
739e0b1692 Merge branch 'fix/docgen-accompanying-period-context-with-list-aworks' into 'master'
Refactor username mapping in activity context filter.

See merge request Chill-Projet/chill-bundles!797
2025-02-14 12:20:07 +00:00
8db8f5fdf5 Refactor username mapping in activity context filter.
Replaced array_map with a foreach loop for clarity and maintainability when extracting usernames from work referrers. This ensures better readability and aligns with coding standards.
2025-02-14 12:11:28 +01:00
d0cd4792d6 Fix dynamicEntityPicker: declare variables 2025-02-13 15:02:06 +01:00
6d196ead94 Merge branch '321-text-editor' of https://gitlab.com/Chill-Projet/chill-bundles into 321-text-editor 2025-02-13 14:49:51 +01:00
4047d5fd5b WIP Allow for comment content to be submitted to backend 2025-02-13 14:49:33 +01:00
9aac80d834 Change logic to allow as many comment vue apps to be charged on same page as needed 2025-02-13 14:49:33 +01:00
7560dc57c6 Remove comment editor component from Activity vue app 2025-02-13 14:49:33 +01:00
10314845f6 Turn component into a small vue app and add public private comment logic 2025-02-13 14:49:33 +01:00
9b84bc4d69 Use update ckeditor5 2025-02-13 14:49:33 +01:00
a2fcf039be Refactor CKEditor integration across application
Replaced `ClassicEditor` with a consistent `classicEditor` and added centralized editor configuration using `editorConfig`. Simplified webpack configuration by removing CKEditor-specific setups and dependencies, improving maintainability.
2025-02-13 14:49:33 +01:00
b4d887a372 upgrade ckeditor 2025-02-13 14:49:32 +01:00
0aaa7122da upgrade ckeditor/vue 2025-02-13 14:49:32 +01:00
1bc7f85874 Implement localStorage and toggling of simple/rich text editor 2025-02-13 14:49:32 +01:00
1d2fd000aa Setup new component for text editor 2025-02-13 14:49:32 +01:00
fc32f9eca9 Merge branch 'add-dependents' into 'master'
add household properties :  number of dependents and number of dependents with disabilities

See merge request Chill-Projet/chill-bundles!778
2025-02-13 12:41:29 +00:00
ab35e8c034 Release v3.8.2 2025-02-10 14:52:22 +01:00
2aded2974f Merge branch '358-remove-filter-button' into 'master'
Remove "filter" button from attachment modal in workflows

Closes #358

See merge request Chill-Projet/chill-bundles!794
2025-02-10 13:49:09 +00:00
f84c1632b2 Remove "filter" button from attachment modal in workflows
The "filter" button was unnecessary in the document list within the "add attachment" modal and has been removed for simplicity. This change does not involve any schema modifications and resolves issue #358.
2025-02-10 14:41:53 +01:00
Christophe Siraut
03717a1a87 ChillPersonBundle: move numberOfDependents configuration to a new household node; extend AdministrativeStatusRepository and EmploymentStatusRepository from ServiceEntityRepository 2025-02-07 17:45:33 +01:00
Christophe Siraut
02c524dd79 doc: chill:main:address-ref-from-best-addresse requires a language code 2025-02-07 10:53:53 +01:00
506df432b0 WIP Allow for comment content to be submitted to backend 2025-02-06 16:07:40 +01:00
c32c18b0e2 Change logic to allow as many comment vue apps to be charged on same page as needed 2025-02-06 15:10:49 +01:00
321d569ee9 Remove comment editor component from Activity vue app 2025-02-06 14:32:22 +01:00
cd40eb3932 Turn component into a small vue app and add public private comment logic 2025-02-06 12:46:19 +01:00
f0f2531fa3 Use update ckeditor5 2025-02-06 10:33:32 +01:00
183a220e7b Refactor CKEditor integration across application
Replaced `ClassicEditor` with a consistent `classicEditor` and added centralized editor configuration using `editorConfig`. Simplified webpack configuration by removing CKEditor-specific setups and dependencies, improving maintainability.
2025-02-06 10:33:32 +01:00
9df127a82c upgrade ckeditor 2025-02-06 10:33:32 +01:00
04a1412562 upgrade ckeditor/vue 2025-02-06 10:33:32 +01:00
3aef0a185e Implement localStorage and toggling of simple/rich text editor 2025-02-06 10:33:32 +01:00
578bce31b9 Setup new component for text editor 2025-02-06 10:33:32 +01:00
99e4824137 Release bundles v.3.8.1 2025-02-05 18:14:22 +01:00
dacaaea235 Fix household link in parcours banner 2025-02-05 18:09:34 +01:00
096466e79e Add missing lines for v3.8.0 2025-02-03 22:22:11 +01:00
7285e5c2b0 Merge branch '331-manage-attachments-to-workflow' into 'master'
Add attachments to workflow

Closes #331

See merge request Chill-Projet/chill-bundles!764
2025-02-03 21:15:00 +00:00
37227a3aeb Add attachments to workflow 2025-02-03 21:15:00 +00:00
7569667189 Release v3.8.0 2025-02-03 22:11:42 +01:00
b0993f4062 Merge branch '334-current-user-activity-suggestion' into 'master'
Resolve "Quand on crée un échange, l'utilisateur courant doit toujours être suggéré"

Closes #334

See merge request Chill-Projet/chill-bundles!786
2025-02-03 21:08:49 +00:00
7c79b65f48 Merge branch '350-validation_person_duplicate' into 'master'
Resolve "Erreur 500 lorsqu'un utilisateur fusionne deux dossiers d'usagers, et que les deux dossiers usagers sélectionnés sont les mêmes"

Closes #350

See merge request Chill-Projet/chill-bundles!788
2025-02-03 21:04:28 +00:00
b8f25bcd45 Resolve "Erreur 500 lorsqu'un utilisateur fusionne deux dossiers d'usagers, et que les deux dossiers usagers sélectionnés sont les mêmes" 2025-02-03 21:04:27 +00:00
f4efb0e975 Merge branch '351-prettier_span_fix' into 'master'
Resolve "UX bug caused by 'Prettier' linter"

Closes #351

See merge request Chill-Projet/chill-bundles!790
2025-02-03 21:02:46 +00:00
c641baec78 Resolve "UX bug caused by 'Prettier' linter" 2025-02-03 21:02:44 +00:00
cc150e32f0 Merge branch '354-fix_creation_document_category' into 'master'
Resolve "chill_doc: cannot create a document category"

Closes #354

See merge request Chill-Projet/chill-bundles!791
2025-02-03 21:02:03 +00:00
Christophe Siraut
bc7f0907ab doc: add languages population command 2025-02-03 14:05:08 +01:00
26cf6459b4 Add changie 2025-02-03 11:23:12 +01:00
d0fa6dd512 Apply fix to query idInsideBundle for documentCategory creation 2025-02-03 11:21:00 +01:00
Christophe Siraut
fbdc0d32f0 ChillPersonBundle: Add numberOfDependents and numberOfDependentsWithDisabilities 2025-01-31 10:13:47 +01:00
Christophe Siraut
5f31473c90 ChillMainBundle: optionnaly mask aggegators in ExportType 2025-01-31 09:37:46 +01:00
Christophe Siraut
98cf167040 ChillPersonBundle: add aggregators for administrative status and employment status 2025-01-31 09:37:46 +01:00
Christophe Siraut
6c37d798bf ChillPersonBundle: add administrativeStatus property to Person 2025-01-31 09:06:45 +01:00
03748a7e84 Add changie 2025-01-30 12:00:11 +01:00
9e3431f397 Merge branch 'improve_news_item_admin_form' into 'master'
Improve ux news item admin form

See merge request Chill-Projet/chill-bundles!779
2025-01-27 10:59:51 +00:00
912861dbff Remove assert attribute on startdate 2025-01-27 11:03:46 +01:00
35f25daf7c Merge branch '320-display-persons-in-banner' into 'master'
Display first associated persons in the banner of an accompanying course

Closes #320

See merge request Chill-Projet/chill-bundles!769
2025-01-23 20:19:31 +00:00
21274155b5 Display first associated persons in the banner of an accompanying course 2025-01-23 20:19:31 +00:00
3f7c136d6b Remove yarn.lock from tracking 2025-01-23 12:40:39 +01:00
5d9c573853 Merge branch '319-display-info-in-notification' into 'master'
Resolve "Afficher les noms des usagers et l'entité concerné par l'entité notifiée dans la liste des notifications"

Closes #319

See merge request Chill-Projet/chill-bundles!767
2025-01-23 11:34:16 +00:00
9a5fd67842 Resolve "Afficher les noms des usagers et l'entité concerné par l'entité notifiée dans la liste des notifications" 2025-01-23 11:34:16 +00:00
2755bc12c4 Add current user to suggested users when creating activity 2025-01-22 15:41:25 +01:00
9e191f1b5b Restrict eslint on src directory 2025-01-22 14:19:30 +00:00
ab684a20ad Release version v3.7.1 2025-01-21 23:06:30 +01:00
bc92b52498 Merge branch 'fix-notifier-config' into 'master'
Fix notifier  legacy configuration and some improvements

See merge request Chill-Projet/chill-bundles!784
2025-01-21 22:05:27 +00:00
be5655e537 Refine Notifier component configuration guidance
Added detailed instructions to configure the Symfony Notifier component correctly, emphasizing proper setup for SMS providers like OVHcloud. Clarified removal of legacy configurations and included examples for both utilizing and replacing the added configuration.
2025-01-21 22:59:08 +01:00
ceb0bd982e Prevent default texter configuration if already defined
Check existing framework configurations for `texter_transports` before applying default settings. This avoids overwriting or conflicting with pre-defined notifier configurations.
2025-01-21 22:59:08 +01:00
47c0af3623 Handle missing message IDs in SentMessageEventSubscriber
Added a condition to log an info message when the sent SMS lacks a message ID. Ensures clearer distinction between successful and incomplete SMS message logging.
2025-01-21 22:39:37 +01:00
f6f2efee2c Release v3.7.0 with new feature and updated dependencies
Introduce the Symfony Notifier component for SMS messaging, supporting more providers. Fix referrer's scope and job aggregation issue (#348). Update numerous dependencies, including Babel and ESLint, for improved stability and functionality.
2025-01-21 16:07:00 +01:00
59fd9fc63f Add a line in the documentation about the role of prettier within eslint configuration 2025-01-21 10:36:45 +01:00
ec2c08681e Add --fix to eslint yarn command 2025-01-21 09:52:29 +01:00
fedcbb9a70 Add missing changie for MR !782 2025-01-20 15:47:01 +01:00
3f1a4fe353 Merge branch 'short-message-use-notifier-component' into 'master'
Send short messages using Notifier integration

See merge request Chill-Projet/chill-bundles!782
2025-01-20 14:17:01 +00:00
fc27c73dab Merge branch '332-changie-schema-prompt' into 'master'
Add a prompt to signal a schema change in the db

Closes #332

See merge request Chill-Projet/chill-bundles!772
2025-01-20 14:13:33 +00:00
20bfd5b717 Add Rector command to composer scripts
Included the Rector command in composer.json to streamline running Rector tasks. This addition ensures consistency and improves developer efficiency by integrating the tool directly into the project workflow.
2025-01-20 15:10:44 +01:00
5e3a1eb2ab Remove legacy ShortMessage components and integrate Notifier
Replaced outdated ShortMessage functionalities with Symfony's Notifier component for handling SMS messages. Deprecated legacy `ShortMessage` components and introduced a transition layer for existing OVH configurations. Updated dependencies and environment setup to support the new implementation.
2025-01-20 15:10:44 +01:00
b02820407c Add log SMS when a message is sent
Introduced a new event subscriber to log SMS sent events with details such as recipient and message IDs. This enhances monitoring and debugging of SMS delivery.
2025-01-20 15:10:43 +01:00
594ed4a5b4 Replace custom ShortMessage usage with Symfony’s SmsMessage.
Switched the entire short message notification system to leverage Symfony's Notifier component and its TexterInterface with SmsMessage. This update simplifies the implementation, removes custom short message handling, and aligns with Symfony's standardized approach.
2025-01-20 15:10:43 +01:00
88fbf7bc1c Add Symfony Notifier integration
Integrated Symfony Notifier into the project by adding it to `composer.json` and creating a configuration file `notifier.yaml`. Updated `symfony.lock` to include the recipe configuration for Notifier. Minor documentation formatting issues were also fixed in `index.rst`.
2025-01-20 14:28:49 +01:00
aa26e67f6f Update PHP-CS-Fixer cache path and add PHPStan command
Changed the cache path in `.php-cs-fixer.dist.php` to improve organization by moving it to the `var` directory. Added a new Composer script for running PHPStan to streamline static analysis workflows.
2025-01-20 14:12:24 +01:00
21ac3eaab4 Merge branch '348-erreur-dans-le-regroupement-par-service-du-referent-de-parcours' into 'master'
Resolve "Erreur dans le regroupement par service du référent de parcours"

Closes #348

See merge request Chill-Projet/chill-bundles!783
2025-01-20 12:10:25 +00:00
2ff500b00e Resolve "Erreur dans le regroupement par service du référent de parcours" 2025-01-20 12:10:25 +00:00
19fa308c06 Update chill bundles to version 3.6.0 2025-01-16 18:00:48 +01:00
1b831bc424 Fix activity between dates filter: condition added for alias 2025-01-16 15:19:38 +01:00
573118e514 Undo change migration 2025-01-16 13:05:18 +01:00
0cabf5654a Add changie for fix 2025-01-16 12:25:42 +01:00
cfb547d55f Increase length of varchar for id chill_person_marital_status 2025-01-16 12:23:59 +01:00
a915c35026 Rector changes 2025-01-16 10:25:02 +01:00
018f8aef5c Add export button to social actions template 2025-01-15 16:45:05 +01:00
de6385ba21 Refactor SocialIssuesExportController to include method for exporting social actions 2025-01-15 16:44:45 +01:00
edb51dd3cd Add export button to template 2025-01-15 16:35:09 +01:00
c379bccad4 Create social issue export controller 2025-01-15 16:33:57 +01:00
bd9ad8a569 remove prettier command from yarn 2025-01-15 13:08:26 +01:00
0cdd9184a3 Add condition to rendering of schema change 2025-01-14 16:10:58 +01:00
cb5fd2b69d Merge branch 'address-importer-ban' into 'master'
Add service and command to import French addresses from BAN

See merge request Chill-Projet/chill-bundles!781
2025-01-13 16:09:17 +00:00
feebcf6662 Add changie [ci-skip] 2025-01-13 17:08:45 +01:00
2a61197999 Merge branch '332-changie-schema-prompt' of https://gitlab.com/Chill-Projet/chill-bundles into 332-changie-schema-prompt 2025-01-13 16:22:14 +01:00
0a53a9a9d1 Add a prompt to signal a schema change in the db 2025-01-13 16:22:04 +01:00
eea1e40663 Merge branch 'improve_news_item_admin_form' of https://gitlab.com/Chill-Projet/chill-bundles into improve_news_item_admin_form 2025-01-13 16:20:08 +01:00
1b0771eb07 Add changie 2025-01-13 16:19:52 +01:00
3a74c48104 Indicate user to set startDate equal to or greater than today + set empty_data in form content field 2025-01-13 16:19:52 +01:00
6de4861b98 Fix email attachment handling in address import reports
Updated attachment logic to use in-memory file contents and apply a `.gz` suffix to filenames. This ensures better file handling and resolves potential issues with attaching files directly from a path.
2025-01-10 23:00:40 +01:00
b4a1e824ac Add service and command to import French addresses from BAN
Introduce a service to handle the import of French addresses from the Base Adresse Nationale (BAN) dataset. Add a new console command `chill:main:address-ref-from-ban` to trigger the import by department numbers, with an option to send a report email for unmatched addresses.
2025-01-10 22:52:08 +01:00
d87cf925e2 Add changie file for storing document on disk feature 2025-01-09 16:28:36 +01:00
ce3cce7b95 Merge branch '346-store-docs-on-disk' into 'master'
Resolve "Permettre de stocker les documents sur disque, localement."

Closes #346

See merge request Chill-Projet/chill-bundles!774
2025-01-09 15:16:45 +00:00
6c97654e5e Add documentation for document storage configuration
Introduce a new guide detailing document storage options, including on-disk storage and cloud-based OpenStack integration. This document explains configuration steps, benefits, and limitations for both methods, and is now linked in the production installation index.
2025-01-09 16:05:58 +01:00
0787e61c22 Set default configuration file for chill_doc_store 2025-01-09 15:25:44 +01:00
73bcfb82b7 Add configuration option to select storage driver
Introduces a new `use_driver` configuration option to specify the desired storage driver (`local_storage` or `openstack`). Ensures proper validation to handle multiple drivers and throws appropriate errors when configurations are inconsistent or missing. Refactors related logic to improve clarity and maintainability.
2025-01-09 15:25:43 +01:00
812e4047d0 Adjust key size in KeyGenerator to 32 bytes.
Changed the key size from 128 bytes to 32 bytes in the KeyGenerator service. This aligns with the expected algorithm requirements and ensures proper cryptographic behavior.
2025-01-09 15:25:43 +01:00
999ac3af2b Add TempUrl signature validation to local storage
Implemented local storage-based file handling with TempUrl signature validation for upload and retrieval. Added validation checks for parameters like max file size/count, expiration, and signature integrity. Included unit tests for TempUrl signature validation and adjusted configuration for local storage.
2025-01-09 15:25:42 +01:00
0c628c39db store encrypted content 2025-01-09 15:25:42 +01:00
c65f1d495d Refactor ConfigureOpenstackObjectStorageCommand
- change namespace for more obvious handling;
- remove command of local storage is configured
2025-01-09 15:25:41 +01:00
83f7086bb0 Configure DI for providing kernel secret for TempUrlLocalStorageGenerator 2025-01-09 15:25:41 +01:00
c1e449f48e Implements StoredObjectManager for local storage 2025-01-09 15:25:41 +01:00
1f6de3cb11 Implement TempUrlLocalStorageGenerator and its tests
Added the full implementation for TempUrlLocalStorageGenerator, including methods to generate signed URLs and POST requests with expiration and signature logic. Introduced corresponding unit tests to validate functionality using mocked dependencies.
2025-01-09 15:25:40 +01:00
3a2548ed89 Select storage depending on configuration 2025-01-09 15:25:39 +01:00
d7652658f2 Define new configuration for local storage 2025-01-09 15:25:39 +01:00
67b5bc6dba Implements required interface to store documents on disk 2025-01-09 15:25:38 +01:00
e25c1e1816 Refactor object storage to separate local storage and openstack storage 2025-01-09 15:25:38 +01:00
282b7f7fbb Merge branch 'import-addresses-handle-no-postcode' into 'master'
Allow addresses without postal code to be imported without failure, and add email reporting for unimported addresses in import commands

See merge request Chill-Projet/chill-bundles!780
2025-01-09 11:52:21 +00:00
ab311eaecb Add email reporting for unimported addresses in import commands
Enhanced address import commands to optionally send a recap of unimported addresses via email. Updated import logic to handle cases where postal codes are missing, log issues, and generate compressed CSV reports with failed entries.
2025-01-09 12:21:10 +01:00
b37d7fb907 Add changie 2025-01-08 16:21:36 +01:00
57b8dacba0 Indicate user to set startDate equal to or greater than today + set empty_data in form content field 2025-01-08 16:18:13 +01:00
edcc01149b Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles 2025-01-07 16:59:32 +01:00
27b2d77fdb Fix EntityToJsonTransformer for saved exports 2025-01-07 16:59:16 +01:00
96bb98f854 upgrade yarn deps 2025-01-07 10:03:40 +01:00
68ed2db51e Refactor exception type to InvalidConfigurationException.
Replaced InvalidArgumentException with InvalidConfigurationException for widget service alias conflicts. This ensures the exception better reflects the configuration-related nature of the error.
2025-01-07 10:03:24 +01:00
184bb095d8 Update chill-bundles version to 3.5.2 2024-12-19 10:45:07 +01:00
78c8e94765 Merge branch '345-export-activity-bug' into 'master'
Fix the filtering of users that have been associated to an activity between certain dates

Closes #345

See merge request Chill-Projet/chill-bundles!773
2024-12-19 09:41:21 +00:00
c8d1a91953 Fix the filtering of users that have been associated to an activity between certain dates 2024-12-19 09:41:21 +00:00
3e8e2b0fa8 Merge branch 'add-employment-status' into 'master'
add an employmentStatus property to the person entity

See merge request Chill-Projet/chill-bundles!763
2024-12-17 15:15:57 +00:00
Christophe Siraut
2c9c700ca7 composer.json: specify doctrine/data-fixtures version 2024-12-16 17:35:22 +01:00
Christophe Siraut
110db30748 ChillPersonBundle: add employmentStatus property to Person 2024-12-16 17:35:19 +01:00
Christophe Siraut
1f96f76f87 ChillPersonBundle: add a visibility parameter to addFieldNode() 2024-12-16 16:54:18 +01:00
Christophe Siraut
65902ea231 ChillCUstomFieldsBundle: Symfony\Component\Frm\FormFactory::createNamedBuilder(): Argument #1 () must be of type string 2024-12-16 16:54:18 +01:00
Christophe Siraut
88c0b1570d composer: require-dev php-cs-fixer 2024-12-16 16:54:18 +01:00
4933d2251c fix tests about PdfSignedMessageHandler.php 2024-12-16 16:50:07 +01:00
c939ff4a4e Release v3.5.1 2024-12-16 15:45:29 +01:00
82fb98348b Wrap PdfSignedMessage handling in a transaction
Ensure atomicity when writing stored objects and marking signatures as signed by wrapping these operations in a database transaction. This reduces the risk of partial updates and improves data consistency.
2024-12-16 15:44:40 +01:00
d56d00421a Add convetion on database table and add translation file in english for the conventions 2024-12-12 11:37:54 +01:00
de11fa86c6 Add database table naming convention 2024-12-12 10:52:39 +01:00
4cc254a3e5 Add script to package.json for eslint + take ts files into account with new baseline 2024-12-11 14:24:53 +01:00
22ce16ef49 Fix display of gender label in the filiation 2024-12-11 14:12:54 +01:00
1e52a93cbe ignore certain folders for eslint 2024-12-11 12:26:41 +01:00
16fe07cce7 More automatic eslint fixes, update baseline and eslint docs 2024-12-11 11:32:29 +01:00
e8962782ed Merge branch 'implement_eslint' into 'master'
Implement ESLint

See merge request Chill-Projet/chill-bundles!752
2024-12-11 10:26:35 +00:00
64a853fb6d Renew baseline after applying prettier 2024-12-11 10:50:01 +01:00
ebfd48e41f Apply prettier to files 2024-12-11 10:49:11 +01:00
a6aa2a81c2 Resolve merge with master 2024-12-11 10:46:06 +01:00
98d29c2134 Add baseline to eslint 2024-12-11 10:38:55 +01:00
60386ae9ac Add a prompt to signal a schema change in the db 2024-12-10 18:30:44 +01:00
559901e528 Update chill bundles version to v3.5.0 2024-12-09 11:35:16 +01:00
05bc69fd33 Fix display of gender label
Label already localized in serialization so not to be accessed as label.fr within vue component
2024-12-09 11:32:28 +01:00
49da62d364 Merge branch 'signature-state-change-wrapped-into-transaction' into 'master'
Refactor transaction handling for signature state changes, to wrap them into transactions

See merge request Chill-Projet/chill-bundles!771
2024-12-06 11:43:08 +00:00
3af7824d01 Refactor transaction handling for signature state changes, to wrap them into transactions
Wrap signature state changes in transactions to prevent race conditions and ensure data integrity. Update controller and test class names to reflect broader state change capabilities. Enhance documentation with comments to clarify transaction requirements and procedure details for signature operations.
2024-12-06 12:37:17 +01:00
033053c437 Restrict the version of amqp-messenger 2024-12-06 12:37:16 +01:00
633bb00154 fix typing of Datetime 2024-12-06 12:37:16 +01:00
f5c1b5cf8a Merge branch '318-signature-vue-app-show-full-doc' into 'master'
Resolve "[App de signature] Pouvoir voir le document en continu (toutes les pages ensemble)"

Closes #318

See merge request Chill-Projet/chill-bundles!761
2024-12-05 16:31:04 +00:00
ccd71da4e4 Add changie 2024-12-05 17:25:08 +01:00
1eadb3bbdb add missing configuration for chill_workflow_signature_documents.yaml 2024-12-05 17:16:06 +01:00
nobohan
0bb5a79cae FEATURE signature: show full pages - show next signature button even if one signature 2024-12-05 17:16:06 +01:00
nobohan
bd3198e42b FEATURE signature: show full pages - UI and turn pages 2024-12-05 17:16:05 +01:00
nobohan
96dfddc55f FEATURE signature: show full pages - can add zone + fix hitSignature is canvas-aware 2024-12-05 17:16:05 +01:00
nobohan
da37a3db5f FEATURE signature: show full pages - can select zone 2024-12-05 17:16:04 +01:00
nobohan
c2882b1079 FEATURE signature: show full pages - WIP 2024-12-05 17:16:01 +01:00
b9e515f4e6 Merge branch '317-add-zone-bug-with-zoom' into 'master'
Resolve "Lorsqu'on place manuellement une zone de signature avec le zoom actif, la zone n'est pas placée correctement sur la page"

Closes #317

See merge request Chill-Projet/chill-bundles!757
2024-12-05 15:49:38 +00:00
nobohan
df2ea7e1ba FIXED adding zone in signature vue app was not placed correctly when zooming 2024-12-05 16:41:51 +01:00
nobohan
d59cda9cc4 DX format signature vue app 2024-12-05 16:41:51 +01:00
54e10cacd3 git use apk instead of apt-get to install jq 2024-12-05 16:25:40 +01:00
3ca126804b Implement baseline comparison for eslint in the CI 2024-12-05 16:13:07 +01:00
40d733c290 Add a baseline for eslint 2024-12-05 16:08:42 +01:00
7a98bb5a06 release v3.4.3 2024-12-05 15:44:51 +01:00
2ce8f540fe release v3.4.3 2024-12-05 15:34:28 +01:00
6c4d8990cc Merge branch '342-error-while-deploying-chill-version-v3-4-2-migrations-are-blocked' into 'master'
Resolve "Error while deploying chill version v3.4.2: migrations are blocked"

Closes #342

See merge request Chill-Projet/chill-bundles!770
2024-12-05 14:33:34 +00:00
351e9c3fcc Remove custom join table configuration for documents
The custom join table setup for the 'documents' relation has been removed. This change relies on the default naming and configuration provided by Doctrine, simplifying the code and reducing potential configuration errors.
2024-12-05 15:32:35 +01:00
1b65cac1df Make person phone number nullable
Removed the "not null" constraint from the person phone number field to allow for better flexibility in data storage, such as storing notes. This change rectifies issues in certain instances where the migration had incorrectly set the field to "not null". Adjustments include updating the database schema and modifying the entity definition to reflect this change.
2024-12-05 15:30:25 +01:00
aa0785fc71 Apply prettier rules 2024-11-14 18:47:38 +01:00
610227815a Add prettier to eslint to format vue, js and ts code + add script to run it locally 2024-11-14 18:46:22 +01:00
9d9f062417 add eslint to ci gitlab 2024-11-14 18:37:37 +01:00
0454e5d758 eslint corrections in mainbundle 2024-11-14 16:46:07 +01:00
2d6d2a1f58 eslint corrections in personbundle 2024-11-14 16:45:50 +01:00
c971e34675 JS correction in ChillWopiBundle required by ESLint 2024-11-12 15:39:52 +01:00
be2a119163 JS correction in ChillThirdpartyBundle required by ESLint 2024-11-12 15:39:31 +01:00
22ecb11227 JS corrections in ChillPersonBundle required by ESLint 2024-11-12 15:38:46 +01:00
dd854ea339 Switch back to using .mjs extension otherwise webpack doesn't compile well with type:module specified in package.json file 2024-11-05 15:48:48 +01:00
f05c25853c Fix of errors: automatic and some manual 2024-11-04 19:56:03 +01:00
90798b12e5 Add ts parser and adjust config 2024-11-04 19:55:27 +01:00
f91f5ce27e Add eslint documentation to developers manual 2024-11-04 17:57:13 +01:00
96f73b419d Eslint correction always return something in computed property 2024-10-31 19:11:49 +01:00
273f91fd00 Add config for eslint 2024-10-31 19:11:16 +01:00
27ecae4486 first eslint corrections 2024-10-31 17:28:02 +01:00
f90f1c7ef8 Install eslint and eslint vue plugin + config file 2024-10-31 17:27:45 +01:00
657 changed files with 40089 additions and 32447 deletions

View File

@@ -0,0 +1,6 @@
kind: DX
body: Remove dead code for wopi-link module
time: 2025-04-30T14:45:50.406111606+02:00
custom:
Issue: "352"
SchemaChange: No schema change

View File

@@ -0,0 +1,7 @@
kind: Feature
body: Add the document file name to the document title when a user upload a document,
unless there is already a document title.
time: 2025-04-24T14:22:11.800975422+02:00
custom:
Issue: "377"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Add desactivation date for social action and issue csv export
time: 2025-05-20T09:56:28.108941934+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -0,0 +1,7 @@
kind: Fixed
body: trying to prevent bug of typeerror in doc-history + improved display of document
history
time: 2025-04-24T13:39:43.878468232+02:00
custom:
Issue: "376"
SchemaChange: No schema change

View File

@@ -0,0 +1,7 @@
kind: Fixed
body: Display previous participation in acc course work even if the person has left
the acc course
time: 2025-04-24T16:37:46.970203594+02:00
custom:
Issue: "381"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: Fix display of text in calendar events
time: 2025-05-05T10:27:15.461493066+02:00
custom:
Issue: "372"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: Add missing translation for user_group.no_user_groups
time: 2025-05-14T14:53:39.53927329+02:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: UX
body: Remove default filter in_progress for the page 'my tasks'; Allows for new tasks to be displayed upon opening of the page
time: 2025-04-23T17:26:24.45777387+02:00
custom:
Issue: "374"
SchemaChange: No schema change

6
.changes/v3.10.0.md Normal file
View File

@@ -0,0 +1,6 @@
## v3.10.0 - 2025-03-17
### Feature
* ([#363](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/363)) Display social actions grouped per social issue within activity form
### Fixed
* ([#362](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/362)) Fix Dependency Injection, which prevented to save the CalendarRange
* ([#368](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/368)) fix search query for user groups

3
.changes/v3.10.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.10.1 - 2025-03-17
### DX
* Remove yarn dependency to symfony/ux-translator, to ease the build process

3
.changes/v3.10.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.10.2 - 2025-03-17
### Fixed
* Replace a ts-expect-error with a ts-ignore

3
.changes/v3.10.3.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes

19
.changes/v3.11.0.md Normal file
View File

@@ -0,0 +1,19 @@
## v3.11.0 - 2025-04-17
### Feature
* ([#365](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/365)) Add counters of actions and activities, with 2 boxes to (1) show the number of active actions on total actions and (2) show the number of activities in a accompanying period, and pills in menus for showing the number of active actions and the number of activities.
* ([#364](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/364)) Added a second phone number "telephone2" to the thirdParty entity. Adapted twig templates and vuejs apps to handle this phone number
**Schema Change**: Add columns or tables
* Signature: add a button to go directly to the signature zone, even if there is only one
### Fixed
* Fixed wrong translations in the on-the-fly for creation of thirdParty
* Fixed update of phone number in on-the-fly edition of thirdParty
* Fixed closing of modal when editing thirdParty in accompanying course works
* Shorten the delay between two execution of AccompanyingPeriodStepChangeCronjob, to ensure at least one execution in a day
* ([#102](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/102)) Fix display of title in document list
* When cleaning the old stored object versions, do not throw an error if the stored object is not found on disk
* Add consistent log prefix and key to logs when stale workflows are automatically canceled
* ([#380](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/380)) Remove the "not null" validation constraint on recently added properties on HouseholdComposition
### DX
* Add new chill-col style for displaying title and aside in a flex table

4
.changes/v3.4.3.md Normal file
View File

@@ -0,0 +1,4 @@
## v3.4.3 - 2024-12-05
### Fixed
* Remove the "not null" constraint on person supplementary phones
* Remove doctrine annotation that prevent from adding documents to activities

6
.changes/v3.5.0.md Normal file
View File

@@ -0,0 +1,6 @@
## v3.5.0 - 2024-12-09
### Feature
* ([#318](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/318)) Show all the pages of the documents in the signature app
### Fixed
* Wrap the signature's change state into a transaction, to avoid race conditions
* Fix display of gender label

4
.changes/v3.5.1.md Normal file
View File

@@ -0,0 +1,4 @@
## v3.5.1 - 2024-12-16
### Fixed
* Filiation: fix the display of the gender label in the graph
* Wrap handling of PdfSignedMessage into transactions

3
.changes/v3.5.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.5.2 - 2024-12-19
### Fixed
* ([#345](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/345)) Export: activity filtering of users that were associated to an activity between certain dates. Results contained activities that were not within the specified date range"

3
.changes/v3.5.3.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.5.3 - 2025-01-07
### Fixed
* Fix the EntityToJsonTransformer to return an empty array if the value is ""

9
.changes/v3.6.0.md Normal file
View File

@@ -0,0 +1,9 @@
## v3.6.0 - 2025-01-16
### Feature
* Importer for addresses does not fails when the postal code is not found with some addresses, and compute a recap list of all addresses that could not be imported. This recap list can be send by email.
* ([#346](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/346)) Create a driver for storing documents on disk (instead of openstack object store)
* Add address importer from french Base d'Adresse Nationale (BAN)
* ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add csv export for social issues and social actions
### Fixed
* Export: fix missing alias in activity between certain dates filter. Condition added for alias.

62
.changes/v3.7.0.md Normal file
View File

@@ -0,0 +1,62 @@
## v3.7.0 - 2025-01-21
### Feature
* Use the Notifier component from Symfony to sens short messages (SMS). This allow to use more provider.
### Fixed
* ([#348](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/348)) [export] Fix aggregation of referrer's scope and job: fix the date range comparison
### Warning on configuration of Notifier component
If installed in an symfony app where the recipes are activated, this configuration should be added automatically:
```yaml
framework:
notifier:
chatter_transports:
texter_transports:
ovhcloud: '%env(OVHCLOUD_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }
```
Actually, you should either:
- remove the configuration of ovhcloud added by the recipe
- or remove the previous configuration of chill, to avoid keeping legacy configuration
#### Remove the added configuration and keep the legacy configuration
To remove the configuration:
```diff
framework:
notifier:
chatter_transports:
texter_transports:
- ovhcloud: '%env(OVHCLOUD_DSN)%'
```
In that case, the previous configuration, which was stored under the `chill_main.short_messages.dsn` will be reconfigured into the Notifier component's configuration.
#### Properly configure SMS
You can also properly configure it, as [described in the OVH cloud provider repository](https://github.com/symfony/ovh-cloud-notifier/tree/5.4?tab=readme-ov-file#dsn-example) (where the scheme is `ovhcloud`):
**NOTE**: You have access to all notifier available with the [Notifier component](https://symfony.com/doc/current/notifier.html#notifier-sms-channel). You are not restricted to use OVH as a provider.
```diff
framework:
notifier:
chatter_transports:
texter_transports:
+ ovhcloud: '%env(OVHCLOUD_DSN)%' # this value should be located in a variable, and have `ovhcloud://` as a scheme
chill_main:
- short_messages:
- dsn: '%env(string:SHORT_MESSAGE_DSN)%'
```

3
.changes/v3.7.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.7.1 - 2025-01-21
### Fixed
* Fix legacy configuration processor for notifier component

11
.changes/v3.8.0.md Normal file
View File

@@ -0,0 +1,11 @@
## v3.8.0 - 2025-02-03
### Feature
* Improve the UX of the news item admin form to prevent wrong usage
* ([#319](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/319)) Notification list: display the concerned person's badges in the list
* ([#320](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/320)) Show the first 3 persons directly in the accompanying period's banner
* ([#334](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/334)) Suggest current user when creating an activity
* ([#331](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/331)) Add attachments to workflows
### Fixed
* ([#350](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/350)) Add validation error to manual selection of person in PersonDuplicateController
* ([#354](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/354)) Fix document category creation
* ([#351](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/351)) Add definitive whitespace between span elements in vue PersonText component

3
.changes/v3.8.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.8.1 - 2025-02-05
### Fixed
* Fix household link in the parcours banner

3
.changes/v3.8.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.8.2 - 2025-02-10
### Fixed
* ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal

10
.changes/v3.9.0.md Normal file
View File

@@ -0,0 +1,10 @@
## v3.9.0 - 2025-02-27
### Feature
* ([#349](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/349)) Suggest all referrers within actions of the accompanying period when creating an activity
* ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add possibility to export a csv with all social issues and social actions
* ([#360](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/360)) Restore document to previous kept version when a workflow is canceled
* ([#341](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/341)) Add a list of third parties from within the admin (csv download)
### Fixed
* fix generation of document with accompanying period context, and list of activities and works
### DX
* ([#333](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/333)) Create an unique source of trust for translations

3
.changes/v3.9.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.9.1 - 2025-02-27
### Fixed
* Fix post/patch request with missing 'type' property for gender

3
.changes/v3.9.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.9.2 - 2025-02-27
### Fixed
* Use fetchResults method to fetch all social issues instead of only the first page

View File

@@ -7,15 +7,29 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: '### {{.Kind}}' kindFormat: '### {{.Kind}}'
# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description. # Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description.
changeFormat: >- changeFormat: >-
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}} {{ if and (.Custom.Long) (not (eq .Custom.Long "")) }} * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }}
**Schema Change**: {{ .Custom.SchemaChange }}
{{- end -}}
{{ if and (.Custom.Long) (not (eq .Custom.Long "")) }}{{ .Custom.Long }}{{ end }}
{{ .Custom.Long }}{{ end }}
custom: custom:
- key: SchemaChange
label: Is a schema change required?
optional: false
type: enum
enumOptions:
- "No schema change"
- "Add columns or tables"
- "Drop or rename table or columns, or enforce new constraint that must be manually fixed"
- key: Issue - key: Issue
label: Issue number (on chill-bundles repository) (optional) label: Issue number (on chill-bundles repository) (optional)
optional: true optional: true
type: int type: int
minInt: 1 minInt: 1
body: body:
# allow multiline messages # allow multiline messages
block: true block: true

4
.env
View File

@@ -88,3 +88,7 @@ REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT} REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
###< chill-project/chill-bundles ### ###< chill-project/chill-bundles ###
###> symfony/ovh-cloud-notifier ###
# OVHCLOUD_DSN=ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME
###< symfony/ovh-cloud-notifier ###

2386
.eslint-baseline.json Normal file

File diff suppressed because it is too large Load Diff

8
.gitignore vendored
View File

@@ -5,12 +5,15 @@ composer.lock
docs/build/ docs/build/
.php_cs.cache .php_cs.cache
.cache/* .cache/*
yarn.lock
docker/db/data docker/db/data
docker/rabbitmq/data docker/rabbitmq/data
# in this development bundle, we want to ignore directories related to a real app # in this development bundle, we want to ignore directories related to a real app
assets/* assets/*
!assets/translator.ts
!assets/ux-translator
migrations/* migrations/*
templates/* templates/*
translations/* translations/*
@@ -51,3 +54,8 @@ phpstan.neon
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
###< symfony/webpack-encore-bundle ### ###< symfony/webpack-encore-bundle ###
###> friendsofphp/php-cs-fixer ###
/.php-cs-fixer.php
/.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ###

View File

@@ -5,6 +5,7 @@ cache:
paths: paths:
- /vendor/ - /vendor/
- .cache - .cache
- node_modules/
# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service # Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
# See http://docs.gitlab.com/ee/ci/services/README.html for examples. # See http://docs.gitlab.com/ee/ci/services/README.html for examples.
@@ -103,6 +104,32 @@ rector_tests:
paths: paths:
- vendor/ - vendor/
lint:
stage: Tests
image: node:20-alpine
before_script:
- apk add --no-cache python3 make g++ py3-setuptools
- export PYTHON="$(which python3)"
- export PATH="./node_modules/.bin:$PATH"
script:
- yarn install --ignore-optional
- npx eslint-baseline "src/**/*.{js,ts,vue}"
cache:
paths:
- node_modules/
# psalm_tests:
# stage: Tests
# image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
# script:
# - bin/psalm
# allow_failure: true
# artifacts:
# expire_in: 30 min
# paths:
# - bin
# - tests/app/vendor/
unit_tests: unit_tests:
stage: Tests stage: Tests
image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82 image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82

View File

@@ -25,7 +25,7 @@ $config = new PhpCsFixer\Config();
$config $config
->setFinder($finder) ->setFinder($finder)
->setRiskyAllowed(true) ->setRiskyAllowed(true)
->setCacheFile('.cache/php-cs-fixer.cache') ->setCacheFile('var/php-cs-fixer.cache')
->setUsingCache(true) ->setUsingCache(true)
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
; ;

View File

@@ -6,6 +6,166 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes
## v3.10.2 - 2025-03-17
### Fixed
* Replace a ts-expect-error with a ts-ignore
## v3.10.1 - 2025-03-17
### DX
* Remove yarn dependency to symfony/ux-translator, to ease the build process
## v3.10.0 - 2025-03-17
### Feature
* ([#363](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/363)) Display social actions grouped per social issue within activity form
### Fixed
* ([#362](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/362)) Fix Dependency Injection, which prevented to save the CalendarRange
* ([#368](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/368)) fix search query for user groups
## v3.9.2 - 2025-02-27
### Fixed
* Use fetchResults method to fetch all social issues instead of only the first page
## v3.9.1 - 2025-02-27
### Fixed
* Fix post/patch request with missing 'type' property for gender
## v3.9.0 - 2025-02-27
### Feature
* ([#349](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/349)) Suggest all referrers within actions of the accompanying period when creating an activity
* ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add possibility to export a csv with all social issues and social actions
* ([#360](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/360)) Restore document to previous kept version when a workflow is canceled
* ([#341](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/341)) Add a list of third parties from within the admin (csv download)
### Fixed
* fix generation of document with accompanying period context, and list of activities and works
### DX
* ([#333](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/333)) Create an unique source of trust for translations
## v3.8.2 - 2025-02-10
### Fixed
* ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal
## v3.8.1 - 2025-02-05
### Fixed
* Fix household link in the parcours banner
## v3.8.0 - 2025-02-03
### Feature
* Improve the UX of the news item admin form to prevent wrong usage
* ([#319](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/319)) Notification list: display the concerned person's badges in the list
* ([#320](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/320)) Show the first 3 persons directly in the accompanying period's banner
* ([#334](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/334)) Suggest current user when creating an activity
* ([#331](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/331)) Add attachments to workflows
### Fixed
* ([#350](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/350)) Add validation error to manual selection of person in PersonDuplicateController
* ([#354](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/354)) Fix document category creation
* ([#351](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/351)) Add definitive whitespace between span elements in vue PersonText component
## v3.7.1 - 2025-01-21
### Fixed
* Fix legacy configuration processor for notifier component
## v3.7.0 - 2025-01-21
### Feature
* Use the Notifier component from Symfony to sens short messages (SMS). This allow to use more provider.
### Fixed
* ([#348](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/348)) [export] Fix aggregation of referrer's scope and job: fix the date range comparison
### Warning on configuration of Notifier component
If installed in an symfony app where the recipes are activated, this configuration should be added automatically:
```yaml
framework:
notifier:
chatter_transports:
texter_transports:
ovhcloud: '%env(OVHCLOUD_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }
```
Actually, you should either:
- remove the configuration of ovhcloud added by the recipe
- or remove the previous configuration of chill, to avoid keeping legacy configuration
#### Remove the added configuration and keep the legacy configuration
To remove the configuration:
```diff
framework:
notifier:
chatter_transports:
texter_transports:
- ovhcloud: '%env(OVHCLOUD_DSN)%'
```
In that case, the previous configuration, which was stored under the `chill_main.short_messages.dsn` will be reconfigured into the Notifier component's configuration.
#### Properly configure SMS
You can also properly configure it, as [described in the OVH cloud provider repository](https://github.com/symfony/ovh-cloud-notifier/tree/5.4?tab=readme-ov-file#dsn-example) (where the scheme is `ovhcloud`):
**NOTE**: You have access to all notifier available with the [Notifier component](https://symfony.com/doc/current/notifier.html#notifier-sms-channel). You are not restricted to use OVH as a provider.
```diff
framework:
notifier:
chatter_transports:
texter_transports:
+ ovhcloud: '%env(OVHCLOUD_DSN)%' # this value should be located in a variable, and have `ovhcloud://` as a scheme
chill_main:
- short_messages:
- dsn: '%env(string:SHORT_MESSAGE_DSN)%'
```
## v3.6.0 - 2025-01-16
### Feature
* Importer for addresses does not fails when the postal code is not found with some addresses, and compute a recap list of all addresses that could not be imported. This recap list can be send by email.
* ([#346](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/346)) Create a driver for storing documents on disk (instead of openstack object store)
* Add address importer from french Base d'Adresse Nationale (BAN)
* ([#343](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/343)) Add csv export for social issues and social actions
### Fixed
* Export: fix missing alias in activity between certain dates filter. Condition added for alias.
## v3.5.3 - 2025-01-07
### Fixed
* Fix the EntityToJsonTransformer to return an empty array if the value is ""
## v3.5.2 - 2024-12-19
### Fixed
* ([#345](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/345)) Export: activity filtering of users that were associated to an activity between certain dates. Results contained activities that were not within the specified date range"
## v3.5.1 - 2024-12-16
### Fixed
* Filiation: fix the display of the gender label in the graph
* Wrap handling of PdfSignedMessage into transactions
## v3.5.0 - 2024-12-09
### Feature
* ([#318](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/318)) Show all the pages of the documents in the signature app
### Fixed
* Wrap the signature's change state into a transaction, to avoid race conditions
* Fix display of gender label
## v3.4.3 - 2024-12-05
### Fixed
* Remove the "not null" constraint on person supplementary phones
* Remove doctrine annotation that prevent from adding documents to activities
## v3.4.2 - 2024-12-05 ## v3.4.2 - 2024-12-05
### Fixed ### Fixed
* ([#329](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/329)) Fix the serialization of gender for the generation of documents * ([#329](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/329)) Fix the serialization of gender for the generation of documents

386
CONVENTIONS-en.md Normal file
View File

@@ -0,0 +1,386 @@
# Chill Conventions
In Progress
## Translations
Per bundle, all translations for Twig pages are located in a single file: translations/messages.fr.yaml.
## File Locations
Controllers, form types, and Twig templates are placed in the root folders Controller, Form, and Resources/views, respectively.
Admin pages are no longer placed in subfolders under Admin.
## Assets: Entrypoint Naming
There are three types of entry points:
* Vue application-specific (often for a single page): Prefixed with vue_.
* Reusable JavaScript/CSS code:
Examples include:
* ckeditor
* async_upload (used for forms)
* bootstrap
* chill.js
* ...
=> We prefix with `mod_`
* Page-specific CSS/JS:
* Often reuses functionalities like ShowHide.
=> We prefix with `page_`.
## Folder Structure
````
# Under Resources/public:
- chill/ => Contains the theme (Chill).
- chillmain.scss: Compiled into the chill entrypoint.
- lib/ => Libraries never used as entrypoints but reused elsewhere.
- Examples: ShowHide, Collection, Select2.
- module/: Ends up in reusable entrypoints (mod_).
- bootstrap
- custom.scss
- custom/
- variable.scss
- ...
- AsyncUpload.
- vue/ => Vue applications only (vue_).
- Examples: _components, app.
- page/ => Page-specific assets (page_).
- login
- person
- personvendee
- household_edit_metadata
- index.js
````
# Stylesheet Organization
1. The mod_bootstrap (module bootstrap) entry point is the first level. All parts (modules) of bootstrap are included in the bootstrap.js file located in ChillMainBundle/Resources/public/module/bootstrap.
* At the beginning, this file imports the variables.scss file, which determines most of the bootstrap settings as customized. This file overrides the original, and many variables are adjusted for Chill.
* Care must be taken to ensure this file can always be compared to the original bootstrap file. In the event of a bootstrap update, a diff must be generated and this diff applied to the variable file of the new version.
* At the end, it imports the custom.scss file, which includes bootstrap adaptations to prepare it for our Chill theme.
* This `custom.scss` file can be split into smaller files using `@import 'custom/...'`.
* The idea is that this first bootstrap layer sets an important part of the applications styles, particularly those related to layout positioning, responsive breakpoints, and the default margins and spacing applied to elements being manipulated.
2. The chill entry point is the second level. It contains the Chill theme, which is recognizable in the application.
* Each bundle has a `Resources/public/chill` folder containing a main sass file, which can optionally be split using `@imports`. All these files are compiled into a single Chill entry point, which serves as the applications theme and overrides bootstrap.
* The chillmain.scss file should contain the most general style cascades, those applied to many areas of the application.
* The chillperson.scss file also includes styles specific to different contexts related to people: person, household, and accompanying course.
* Some smaller bundles contain only styles specific to their functionality.
3. The vue_ entry points are used for Vue components. Vue files can contain an scss style block. These styles are specific to the component and its inheritance, with the scoped tag precisely defining their scope (see the documentation).
4. The page_ entry points are used to add assets specific to certain pages, most often scripts and styles.
## HTML Tagging and Style Cascades
The following example shows how to tag a code element without overdoing it. Note that:
* It is not necessary to tag all inner classes.
* The parent class should not be repeated in all child classes. Sass cascading allows for flexible HTML structuring without overloading the tag hierarchy.
* Often, the first class will have variations created with additional classes that start in the same way: bloc-dark simply adds the dark version of bloc. We do not use bloc dark because we dont want the dark class of bloc to interact with the same dark class of table. As a result, we will have an element bloc bloc-dark and another element table table-dark.
```html
<div class="bloc bloc-dark my-bloc">
<h3>My Title</h3>
<ul class="record_actions">
<li>
<a class="btn btn-edit"></a>
</li>
</ul>
</div>
```
Finally, it is important to define what a block, an action zone, and a button are. These three elements exist independently and are the only ones we tag.
For example, to style the title, we simply specify h3 within the block cascade.
```scss
div.bloc {
// un bloc générique, utilisé à plusieurs endroits
&.bloc-dark {
// la version sombre du bloc
}
h3 {}
ul {
// une liste standard dans bloc
li {
// des items de liste standard dans bloc
}
}
}
div.mon-bloc {
// des exceptions spécifiques à mon-bloc,
// qui sont des adaptations de bloc
}
ul.record_actions {
// va uniformiser tous les record_actions de l'application
li {
//...
}
}
.btn {
// les boutons de bootstrap
.btn-edit {
// chill étends les boutons bootstrap pour ses propres besoins
}
}
</style>
```
## Render box
## URL
## Route Naming Conventions
:::warning
These rules have not always been followed in the past. They are desired for the future.
:::
Routes follow this structure:
`chill_(api|crud)_bundle_(api)_entity_action`
1. First, chill_ (for all Chill modules).
2. Then, crud or api, optional, automatically added if the route is generated by the configuration.
3. Then, a string indicating the bundle (`main`, `person`, `activity`, ...).
4. Then, api, if the route is an API route.
5. Then, a string indicating the entity the route targets, and possibly the sub-entities.
6. Then, an action (`list`, `view`, `edit`, `new`, ...).
Indicating `api` in the fourth position allows distinguishing API routes generated by the configuration (which are all prefixed with `chill_api`) from those generated manually. (For example: `chill_api_household__index` and `chill_person_api_household_members_move`).
If points 4 and 5 are missing, they are replaced by other elements to ensure the uniqueness of the route and its clear understanding.
#### HTML pages
:::warning
These rules have not always been followed in the past. They are desired for the future.
:::
Syntaxe:
```
/{_locale}/bundle/entity/{id}/action
/{_locale}/bundle/entity/sub-entity/{id}/action
```
The following elements should be included in the list:
1. The locale;
2. An identifier for the bundle;
3. The entity it relates to;
4. Any sub-entities that the URL refers to;
5. The action.
```
# list of activities for a person
/fr/activity/person/25/activity/list
# new activity
/fr/activity/activity/new?person_id=25
```
#### Pour les API
:::info
Automatically generated routes are prefixed with `chill_api`
:::
Syntaxe:
```
/api/1.0/bundle/entity/{id}/action
/api/1.0/bundle/entity/sub-entity/{id}/action
```
The following elements should be included in the list:
1. The string `/api/` followed by the version (e.g., 1.0);
2. An identifier for the bundle;
3. The entity it relates to;
4. Any sub-entities that the URL refers to;
5. The action.
These elements may be interspersed with the entity identifier. In this case, the identifier should be placed immediately after the entity it relates to.
#### URLS for admin pages
Same conventions as for other html pages, **but `admin` is added in second position**. Soit:
`/{_locale}/admin/bundle/entity/{id}/action`
## Database table naming convention
When creating a new entity and the corresponding database table, we follow the following naming convention for the database table:
`chill_{bundle_identifier}_{entity_name}`.
Eg. chill_person_spoken_languages
## UI Rules
### Page Titles
#### Every page must have a title (in the head tag and page header).
Each page contains a title in the <head> tag. This title is typically the same as the header title on the page.
Tip: It is possible to use the block function in Twig for this.
Example:
```htmlmixed=
{% block title "Titre de la page" %}
{% block content %}
<h1>
{{ block('title')}}
</h1>
{% endblock %}
```
### entity_render usage
#### in Twig
Always use chill_entity_render_box for rendering entities like:
* User
* Person
* SocialAction
* SocialIssue
* Address
* Thirdparty
* ...
Example:
```
address|chill_entity_render_box
```
Justification:
1. Customization by installation: Some elements are sometimes customized during installation (for example, the name of each user will be followed by the name of the service).
2. To streamline and make displays consistent: Ensures uniformity in the way information is displayed across different pages or sections.
3. To simplify Twig code: By using blocks and centralizing title logic, it reduces repetition and makes the Twig code easier to maintain.
* Three rendering modes:
* inline
* block
* item.
#### In Vue
There is always a renderbox equivalent in vue.
#### HTML Links to Sections
Always include links/icons for accessing records, such as person or household details, if the user has access.
### Form Guidelines
#### Vocabulary:
* `Create` in a `bt bt-create` for links to the form to create an entity (to access the form).
* `Save` in a `bt bt-save` for "Save" buttons (in either an edit or create form).
* `Save and new`
* `Save and view`
* `Edit` in a `bt bt-edit` for links to the edit form.
* `Duplicate` (specify where it can be seen).
* `Cancel` for leaving an edit page with a link to the list, or the `returnPath`.
#### After Saving:
Redirect to the returnPath if available; otherwise, redirect to the view page.
### Sticky Form Buttons:
Buttons like "Cancel" and "Save" must be within a "sticky-form" bar at the bottom of the form.
If relevant:
* The banner contains a "Cancel" button that returns to the previous page. It is mandatory for forms, but optional for lists or "summary" pages.
* This "Cancel" button is always on the left.
```
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_entity_return_path('route_name' { 'route': 'option' } )}}">{{ return_path_label }}</a>
</li>
<li>
<!-- action 1 -->
</li>
</ul>
```
### Flash Messages
#### Display a flash message upon entity creation:
Everytime an entity has been created
> "The entity has been created."
The name of the element can be replaced with something more relevant:
> * The activity has been created
> * The appointment has been created
> * ...
#### On Saving an Entity
Each time an entity is saved, a flash message should appear:
> The data has been modified
#### Form Error (Validation Error)
At the top of the form, a flash message should indicate that validations have failed:
> This form contains errors
Errors should appear attached to the field they concern. However, it is acceptable to display errors at the root of the form if it is technically difficult to attach errors.
### Return Links
Each time a link is provided, check whether the function chill_return_path, chill_forward_return_path, or chill_return_path_or should be used.
* From the list page, to the opening of an element, or the creation button => use chill_path_add_return_path
* In these edit pages:
* use chill_return_path_or in the "Cancel" button;
* for the "Save and view" and "Save and close" buttons => ?
### Assets for Suggestion Lists
Create a list of suggestions to add (the entire item is clickable)
```html
<ul class="list-suggest add-items">
<li>
<span>item</span>
</li>
</ul>
```
Create a list of suggestions to remove (with a clickable red cross, the anchor <a> is empty)
```html
<ul class="list-suggest remove-items">
<li>
<span>
item
</span>
</li>
</ul>
```
Create a removable title (with a clickable red cross, the anchor <a> is empty)
```html
<div class="item-title">
<span>title</span>
</div>
```
The classes `cols` or `inline` can be added alongside `list-suggest` to modify the layout of the list. In the last example, add a `removable` class to the `<span>` if you want to make the item removable.

View File

@@ -4,7 +4,6 @@ en cours de rédaction
## Translations ## Translations
Par bundle, toutes les traductions des pages twig se trouvent dans un seul fichier `translations/messages.fr.yaml`. Par bundle, toutes les traductions des pages twig se trouvent dans un seul fichier `translations/messages.fr.yaml`.
## Emplacement des fichiers ## Emplacement des fichiers
@@ -142,7 +141,6 @@ ul.record_actions {
## Render box ## Render box
## URL ## URL
### Nommage des routes ### Nommage des routes
@@ -234,6 +232,13 @@ Même conventions que dans les autres pages html de l'application, **mais `admin
`/{_locale}/admin/bundle/entity/{id}/action` `/{_locale}/admin/bundle/entity/{id}/action`
### Nommage des tables de base de donnée
Lors de la création d'une nouvelle entité et de la table de base de données correspondante, nous suivons la convention d'appellation suivante pour la table de base de données :
`chill_{bundle_identifier}_{nom_de_l'entité}`.
Par exemple : chill_person_spoken_languages
## Règles UI chill ## Règles UI chill
@@ -293,8 +298,6 @@ A prevoir:
> quand on passe loption render: bloc, on peut placer le render_box dans une boucle for plus large qui fonctionne avec la classe flex-table ou la classe flex-bloc, ce qui donnera un affichage en rangée (table) ou en blocs. [name=Mathieu] > quand on passe loption render: bloc, on peut placer le render_box dans une boucle for plus large qui fonctionne avec la classe flex-table ou la classe flex-bloc, ce qui donnera un affichage en rangée (table) ou en blocs. [name=Mathieu]
#### En vue #### En vue
Il existe systématiquement une "box" équivalente en vue. Il existe systématiquement une "box" équivalente en vue.

7
assets/translator.ts Normal file
View File

@@ -0,0 +1,7 @@
import { trans, setLocale, setLocaleFallbacks } from "./ux-translator";
setLocaleFallbacks({"en": "fr", "nl": "fr", "fr": "en"});
setLocale('fr');
export { trans };
export * from '../var/translations';

View File

@@ -0,0 +1,3 @@
This directory import the symfony ux-translator files directly into chill-bundles.
This remove the yarn dependencies from the real package, which breaks our installation.

View File

@@ -0,0 +1 @@
export declare function format(id: string, parameters: Record<string, string | number>, locale: string): string;

View File

@@ -0,0 +1 @@
export declare function formatIntl(id: string, parameters: Record<string, string | number>, locale: string): string;

View File

@@ -0,0 +1,27 @@
export type DomainType = string;
export type LocaleType = string;
export type TranslationsType = Record<DomainType, {
parameters: ParametersType;
}>;
export type NoParametersType = Record<string, never>;
export type ParametersType = Record<string, string | number | Date> | NoParametersType;
export type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T;
export type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never;
export type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never;
export type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType> ? Translations[D] extends {
parameters: infer Parameters;
} ? Parameters : never : never;
export interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
id: string;
translations: {
[domain in DomainType]: {
[locale in Locale]: string;
};
};
}
export declare function setLocale(locale: LocaleType | null): void;
export declare function getLocale(): LocaleType;
export declare function throwWhenNotFound(enabled: boolean): void;
export declare function setLocaleFallbacks(localeFallbacks: Record<LocaleType, LocaleType>): void;
export declare function getLocaleFallbacks(): Record<LocaleType, LocaleType>;
export declare function trans<M extends Message<TranslationsType, LocaleType>, D extends DomainsOf<M>, P extends ParametersOf<M, D>>(...args: P extends NoParametersType ? [message: M, parameters?: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>] : [message: M, parameters: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>]): string;

View File

@@ -0,0 +1 @@
export * from './translator';

View File

@@ -0,0 +1,283 @@
import { IntlMessageFormat } from 'intl-messageformat';
function strtr(string, replacePairs) {
const regex = Object.entries(replacePairs).map(([from]) => {
return from.replace(/([-[\]{}()*+?.\\^$|#,])/g, '\\$1');
});
if (regex.length === 0) {
return string;
}
return string.replace(new RegExp(regex.join('|'), 'g'), (matched) => replacePairs[matched].toString());
}
function format(id, parameters, locale) {
if (null === id || '' === id) {
return '';
}
if (typeof parameters['%count%'] === 'undefined' || Number.isNaN(parameters['%count%'])) {
return strtr(id, parameters);
}
const number = Number(parameters['%count%']);
let parts = [];
if (/^\|+$/.test(id)) {
parts = id.split('|');
}
else {
parts = id.match(/(?:\|\||[^|])+/g) || [];
}
const intervalRegex = /^(?<interval>({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?<left_delimiter>[[\]])\s*(?<left>-Inf|-?\d+(\.\d+)?)\s*,\s*(?<right>\+?Inf|-?\d+(\.\d+)?)\s*(?<right_delimiter>[[\]]))\s*(?<message>.*?)$/s;
const standardRules = [];
for (let part of parts) {
part = part.trim().replace(/\|\|/g, '|');
const matches = part.match(intervalRegex);
if (matches) {
const matchGroups = matches.groups || {};
if (matches[2]) {
for (const n of matches[3].split(',')) {
if (number === Number(n)) {
return strtr(matchGroups.message, parameters);
}
}
}
else {
const leftNumber = '-Inf' === matchGroups.left ? Number.NEGATIVE_INFINITY : Number(matchGroups.left);
const rightNumber = ['Inf', '+Inf'].includes(matchGroups.right)
? Number.POSITIVE_INFINITY
: Number(matchGroups.right);
if (('[' === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber) &&
(']' === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber)) {
return strtr(matchGroups.message, parameters);
}
}
}
else {
const ruleMatch = part.match(/^\w+:\s*(.*?)$/);
standardRules.push(ruleMatch ? ruleMatch[1] : part);
}
}
const position = getPluralizationRule(number, locale);
if (typeof standardRules[position] === 'undefined') {
if (1 === parts.length && typeof standardRules[0] !== 'undefined') {
return strtr(standardRules[0], parameters);
}
throw new Error(`Unable to choose a translation for "${id}" with locale "${locale}" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").`);
}
return strtr(standardRules[position], parameters);
}
function getPluralizationRule(number, locale) {
number = Math.abs(number);
let _locale = locale;
if (locale === 'pt_BR' || locale === 'en_US_POSIX') {
return 0;
}
_locale = _locale.length > 3 ? _locale.substring(0, _locale.indexOf('_')) : _locale;
switch (_locale) {
case 'af':
case 'bn':
case 'bg':
case 'ca':
case 'da':
case 'de':
case 'el':
case 'en':
case 'en_US_POSIX':
case 'eo':
case 'es':
case 'et':
case 'eu':
case 'fa':
case 'fi':
case 'fo':
case 'fur':
case 'fy':
case 'gl':
case 'gu':
case 'ha':
case 'he':
case 'hu':
case 'is':
case 'it':
case 'ku':
case 'lb':
case 'ml':
case 'mn':
case 'mr':
case 'nah':
case 'nb':
case 'ne':
case 'nl':
case 'nn':
case 'no':
case 'oc':
case 'om':
case 'or':
case 'pa':
case 'pap':
case 'ps':
case 'pt':
case 'so':
case 'sq':
case 'sv':
case 'sw':
case 'ta':
case 'te':
case 'tk':
case 'ur':
case 'zu':
return 1 === number ? 0 : 1;
case 'am':
case 'bh':
case 'fil':
case 'fr':
case 'gun':
case 'hi':
case 'hy':
case 'ln':
case 'mg':
case 'nso':
case 'pt_BR':
case 'ti':
case 'wa':
return number < 2 ? 0 : 1;
case 'be':
case 'bs':
case 'hr':
case 'ru':
case 'sh':
case 'sr':
case 'uk':
return 1 === number % 10 && 11 !== number % 100
? 0
: number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20)
? 1
: 2;
case 'cs':
case 'sk':
return 1 === number ? 0 : number >= 2 && number <= 4 ? 1 : 2;
case 'ga':
return 1 === number ? 0 : 2 === number ? 1 : 2;
case 'lt':
return 1 === number % 10 && 11 !== number % 100
? 0
: number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20)
? 1
: 2;
case 'sl':
return 1 === number % 100 ? 0 : 2 === number % 100 ? 1 : 3 === number % 100 || 4 === number % 100 ? 2 : 3;
case 'mk':
return 1 === number % 10 ? 0 : 1;
case 'mt':
return 1 === number
? 0
: 0 === number || (number % 100 > 1 && number % 100 < 11)
? 1
: number % 100 > 10 && number % 100 < 20
? 2
: 3;
case 'lv':
return 0 === number ? 0 : 1 === number % 10 && 11 !== number % 100 ? 1 : 2;
case 'pl':
return 1 === number
? 0
: number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14)
? 1
: 2;
case 'cy':
return 1 === number ? 0 : 2 === number ? 1 : 8 === number || 11 === number ? 2 : 3;
case 'ro':
return 1 === number ? 0 : 0 === number || (number % 100 > 0 && number % 100 < 20) ? 1 : 2;
case 'ar':
return 0 === number
? 0
: 1 === number
? 1
: 2 === number
? 2
: number % 100 >= 3 && number % 100 <= 10
? 3
: number % 100 >= 11 && number % 100 <= 99
? 4
: 5;
default:
return 0;
}
}
function formatIntl(id, parameters, locale) {
if (id === '') {
return '';
}
const intlMessage = new IntlMessageFormat(id, [locale.replace('_', '-')], undefined, { ignoreTag: true });
parameters = { ...parameters };
Object.entries(parameters).forEach(([key, value]) => {
if (key.includes('%') || key.includes('{')) {
delete parameters[key];
parameters[key.replace(/[%{} ]/g, '').trim()] = value;
}
});
return intlMessage.format(parameters);
}
let _locale = null;
let _localeFallbacks = {};
let _throwWhenNotFound = false;
function setLocale(locale) {
_locale = locale;
}
function getLocale() {
return (_locale ||
document.documentElement.getAttribute('data-symfony-ux-translator-locale') ||
(document.documentElement.lang ? document.documentElement.lang.replace('-', '_') : null) ||
'en');
}
function throwWhenNotFound(enabled) {
_throwWhenNotFound = enabled;
}
function setLocaleFallbacks(localeFallbacks) {
_localeFallbacks = localeFallbacks;
}
function getLocaleFallbacks() {
return _localeFallbacks;
}
function trans(message, parameters = {}, domain = 'messages', locale = null) {
if (typeof domain === 'undefined') {
domain = 'messages';
}
if (typeof locale === 'undefined' || null === locale) {
locale = getLocale();
}
if (typeof message.translations === 'undefined') {
return message.id;
}
const localesFallbacks = getLocaleFallbacks();
const translationsIntl = message.translations[`${domain}+intl-icu`];
if (typeof translationsIntl !== 'undefined') {
while (typeof translationsIntl[locale] === 'undefined') {
locale = localesFallbacks[locale];
if (!locale) {
break;
}
}
if (locale) {
return formatIntl(translationsIntl[locale], parameters, locale);
}
}
const translations = message.translations[domain];
if (typeof translations !== 'undefined') {
while (typeof translations[locale] === 'undefined') {
locale = localesFallbacks[locale];
if (!locale) {
break;
}
}
if (locale) {
return format(translations[locale], parameters, locale);
}
}
if (_throwWhenNotFound) {
throw new Error(`No translation message found with id "${message.id}".`);
}
return message.id;
}
export { getLocale, getLocaleFallbacks, setLocale, setLocaleFallbacks, throwWhenNotFound, trans };

1
assets/ux-translator/dist/utils.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export declare function strtr(string: string, replacePairs: Record<string, string | number>): string;

View File

@@ -0,0 +1,34 @@
{
"name": "@symfony/ux-translator",
"description": "Symfony Translator for JavaScript",
"license": "MIT",
"version": "1.0.0",
"main": "dist/translator_controller.js",
"types": "dist/translator_controller.d.ts",
"scripts": {
"build": "node ../../../bin/build_package.js .",
"watch": "node ../../../bin/build_package.js . --watch",
"test": "../../../bin/test_package.sh .",
"check": "biome check",
"ci": "biome ci"
},
"symfony": {
"importmap": {
"intl-messageformat": "^10.5.11",
"@symfony/ux-translator": "path:%PACKAGE%/dist/translator_controller.js",
"@app/translations": "path:var/translations/index.js",
"@app/translations/configuration": "path:var/translations/configuration.js"
}
},
"peerDependencies": {
"intl-messageformat": "^10.5.11"
},
"peerDependenciesMeta": {
"intl-messageformat": {
"optional": false
}
},
"devDependencies": {
"intl-messageformat": "^10.5.11"
}
}

View File

@@ -13,8 +13,10 @@
"ext-json": "*", "ext-json": "*",
"ext-openssl": "*", "ext-openssl": "*",
"ext-redis": "*", "ext-redis": "*",
"ext-zlib": "*",
"champs-libres/wopi-bundle": "dev-master@dev", "champs-libres/wopi-bundle": "dev-master@dev",
"champs-libres/wopi-lib": "dev-master@dev", "champs-libres/wopi-lib": "dev-master@dev",
"doctrine/data-fixtures": "^1.8",
"doctrine/doctrine-bundle": "^2.1", "doctrine/doctrine-bundle": "^2.1",
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.13.0", "doctrine/orm": "^2.13.0",
@@ -56,7 +58,9 @@
"symfony/messenger": "^5.4", "symfony/messenger": "^5.4",
"symfony/mime": "^5.4", "symfony/mime": "^5.4",
"symfony/monolog-bundle": "^3.5", "symfony/monolog-bundle": "^3.5",
"symfony/notifier": "^5.4",
"symfony/options-resolver": "^5.4", "symfony/options-resolver": "^5.4",
"symfony/ovh-cloud-notifier": "^5.4",
"symfony/process": "^5.4", "symfony/process": "^5.4",
"symfony/property-access": "^5.4", "symfony/property-access": "^5.4",
"symfony/property-info": "^5.4", "symfony/property-info": "^5.4",
@@ -71,6 +75,7 @@
"symfony/templating": "^5.4", "symfony/templating": "^5.4",
"symfony/translation": "^5.4", "symfony/translation": "^5.4",
"symfony/twig-bundle": "^5.4", "symfony/twig-bundle": "^5.4",
"symfony/ux-translator": "^2.22",
"symfony/validator": "^5.4", "symfony/validator": "^5.4",
"symfony/webpack-encore-bundle": "^1.11", "symfony/webpack-encore-bundle": "^1.11",
"symfony/workflow": "^5.4", "symfony/workflow": "^5.4",
@@ -85,6 +90,7 @@
"require-dev": { "require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", "doctrine/doctrine-fixtures-bundle": "^3.3",
"fakerphp/faker": "^1.13", "fakerphp/faker": "^1.13",
"friendsofphp/php-cs-fixer": "3.65.0",
"jangregor/phpstan-prophecy": "^1.0", "jangregor/phpstan-prophecy": "^1.0",
"nelmio/alice": "^3.8", "nelmio/alice": "^3.8",
"nikic/php-parser": "^4.15", "nikic/php-parser": "^4.15",
@@ -95,6 +101,7 @@
"phpstan/phpstan-strict-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^10.5.24", "phpunit/phpunit": "^10.5.24",
"rector/rector": "^1.1.0", "rector/rector": "^1.1.0",
"symfony/amqp-messenger": "^5.4.45",
"symfony/debug-bundle": "^5.4", "symfony/debug-bundle": "^5.4",
"symfony/dotenv": "^5.4", "symfony/dotenv": "^5.4",
"symfony/flex": "^2.4", "symfony/flex": "^2.4",
@@ -156,7 +163,9 @@
"cache:clear": "symfony-cmd", "cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd" "assets:install %PUBLIC_DIR%": "symfony-cmd"
}, },
"php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none" "php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none",
"phpstan": "phpstan --no-progress",
"rector": "rector --no-progress-bar"
}, },
"extra": { "extra": {
"symfony": { "symfony": {

View File

@@ -36,4 +36,5 @@ return [
Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true], Chill\BudgetBundle\ChillBudgetBundle::class => ['all' => true],
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true], Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
]; ];

View File

@@ -1,4 +1,7 @@
chill_doc_store: chill_doc_store:
use_driver: openstack
local_storage:
storage_path: '%kernel.project_dir%/var/storage'
openstack: openstack:
temp_url: temp_url:
temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required

View File

@@ -0,0 +1,11 @@
chill_main:
workflow_signature:
base_signer:
document_kinds:
- { key: id_card, labels: [ { lang: fr, label: "Carte d'identité" } ] }
- { key: passport, labels: [ { lang: fr, label: "Passeport" } ] }
- { key: drivers_license, labels: [ { lang: fr, label: "Permis de conduire" } ] }
- { key: visa_long_stay, labels: [ { lang: fr, label: "Visa de long séjour" } ] }
- { key: resident_permit, labels: [ { lang: fr, label: "Carte de séjour" } ] }
- { key: residency_card, labels: [ { lang: fr, label: "Carte de résident" } ] }
- { key: provisionary_residency_permit, labels: [ { lang: fr, label: "Autorisation provisoire de séjour" } ] }

View File

@@ -0,0 +1,13 @@
framework:
notifier:
texter_transports:
#ovhcloud: '%env(OVHCLOUD_DSN)%'
#ovhcloud: '%env(SHORT_MESSAGE_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View File

@@ -0,0 +1,3 @@
ux_translator:
# The directory where the JavaScript translations are dumped
dump_directory: '%kernel.project_dir%/var/translations'

View File

@@ -220,6 +220,7 @@ framework:
- attenteModification - attenteModification
- attenteMiseEnForme - attenteMiseEnForme
- attenteValidationMiseEnForme - attenteValidationMiseEnForme
- attenteSignature
- attenteVisa - attenteVisa
- postSignature - postSignature
- attenteTraitement - attenteTraitement

View File

@@ -0,0 +1,19 @@
when@dev:
sass_assets:
path: /_dev/assets
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/dev.assets.html.twig'
sass_assets_test1:
path: /_dev/assets_test1
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/dev.assets.test1.html.twig'
sass_assets_test2:
path: /_dev/assets_test2
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/dev.assets.test2.html.twig'

View File

@@ -0,0 +1,12 @@
when@dev:
swagger_ui:
path: /_dev/swagger
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: '@ChillMain/Dev/swagger-ui/index.html.twig'
swagger_specs:
path: /_dev/specs.yaml
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: api/specs.yaml

View File

@@ -0,0 +1,71 @@
ESLint
======
To improve the quality of our JS and VueJS code, ESLint and eslint-plugin-vue are implemented within the chill-bundles project.
Commands
--------
To run ESLint, you can simply use the ``eslint`` command within the chill-bundles directory.
This runs eslint **not** taking the baseline into account, thus showing all existing errors in the project.
A script was also added to package.json allowing you to execute ``yarn run eslint``.
This will run eslint, but **taking the baseline into account**, thus only alerting to newly created errors.
The eslint command is configured to also run ``prettier`` which will simply format the code to look more uniform (takes care indentation for example).
Interesting options that can be used in combination with eslint are:
- ``--quiet`` to only get errors and silence the warnings
- ``--fix`` to have ESLint fix what it can, automatically. This will not fix everything.
Baseline
--------
To allow us the time to fix linting errors/warnings a baseline has been created using the following command
- ``npx eslint-baseline "**/*.{js,vue}"``
The baseline has been commited and the gitlab CI setup to only fail upon new errors/warnings being created.
When fixing errors/warnings manually, please update the baseline.
1. Delete the current baseline file
2. Run the above command locally, this will automatically create a new baseline that should be commited
Rules
-----
We use Vue 3, so the rules can be configured as follows within the ``eslint.config.mjs`` file:
- ``.configs["flat/base"]`` ... Settings and rules to enable correct ESLint parsing.
Configurations for using Vue.js 3.x:
- ``.configs["flat/essential"]`` : Base rules plus rules to prevent errors or unintended behavior.
- ``.configs["flat/strongly-recommended"]`` ... Above, plus rules to considerably improve code readability and/or dev experience.
- ``.configs["flat/recommended"]`` ... Above, plus rules to enforce subjective community defaults to ensure consistency.
Detailed information about which rules each set includes can be found here:
`https://eslint.vuejs.org/rules/ <https://eslint.vuejs.org/rules/>`_
Manual Rule Configuration
-------------------------
We can also manually configure certain rules or override rules that are part of the ruleset specified above.
For example, if we want to turn off a certain rule, we can do so as follows:
.. code-block:: javascript
rules: {
'vue/multi-word-component': 'off'
}
We could also change the severity of a certain rule from 'error' to 'warning', for example.
Within specific ``.js`` or ``.vue`` files, we can also override a certain rule only for that specific file by adding a comment:
.. code-block:: javascript
/* eslint multi-word-component: "off", no-child-content: "error"
--------
Here's a description about why this configuration is necessary. */

View File

@@ -31,6 +31,7 @@ As Chill relies on the `symfony <http://symfony.com>`_ framework, reading the fr
Exports <exports.rst> Exports <exports.rst>
Embeddable comments <embeddable-comments.rst> Embeddable comments <embeddable-comments.rst>
Run tests <run-tests.rst> Run tests <run-tests.rst>
ESLint <es-lint.rst>
Useful snippets <useful-snippets.rst> Useful snippets <useful-snippets.rst>
manual/index.rst manual/index.rst
Assets <assets.rst> Assets <assets.rst>

View File

@@ -1,39 +1,37 @@
import { ShowHide } from 'ShowHide/show_hide.js'; import { ShowHide } from "ShowHide/show_hide.js";
var
div_accompagnement = document.getElementById("form_accompagnement"),
div_accompagnement_comment = document.getElementById("form_accompagnement_comment"),
div_caf_id = document.getElementById("cafId"),
div_caf_inscription_date = document.getElementById("cafInscriptionDate"),
;
var div_accompagnement = document.getElementById("form_accompagnement"),
div_accompagnement_comment = document.getElementById(
"form_accompagnement_comment",
),
div_caf_id = document.getElementById("cafId"),
div_caf_inscription_date = document.getElementById("cafInscriptionDate");
// let show/hide the div_accompagnement_comment if the input with value `'autre'` is checked // let show/hide the div_accompagnement_comment if the input with value `'autre'` is checked
new ShowHide({ new ShowHide({
"froms": [div_accompagnement], froms: [div_accompagnement],
"test": function(froms, event) { test: function (froms, event) {
for (let el of froms.values()) { for (let el of froms.values()) {
for (let input of el.querySelectorAll('input').values()) { for (let input of el.querySelectorAll("input").values()) {
if (input.value === 'autre') { if (input.value === "autre") {
return input.checked; return input.checked;
}
}
} }
}
return false; }
},
"container": [div_accompagnement_comment] return false;
},
container: [div_accompagnement_comment],
}); });
// let show the date input only if the the id is filled // let show the date input only if the the id is filled
new ShowHide({ new ShowHide({
froms: [ div_caf_id ], froms: [div_caf_id],
test: function(froms, event) { test: function (froms, event) {
for (let el of froms.values()) { for (let el of froms.values()) {
return el.querySelector("input").value !== ""; return el.querySelector("input").value !== "";
} }
}, },
container: [ div_caf_inscription_date ], container: [div_caf_inscription_date],
// using this option, we use the event `input` instead of `change` // using this option, we use the event `input` instead of `change`
event_name: 'input' event_name: "input",
}); });

View File

@@ -16,7 +16,7 @@ Welcome to Chill documentation!
Chill is a free software for social workers. Chill is a free software for social workers.
Chill rely on the php framework `Symfony <http://symfony.com>`_. Chill rely on the php framework `Symfony <http://symfony.com>`_.
Contents of this documentation: Contents of this documentation:
@@ -42,7 +42,7 @@ Contribute
User manual User manual
=========== ===========
An user manual exists in French and currently focuses on describing the main concept of the software. An user manual exists in French and currently focuses on describing the main concept of the software.
`Read (and contribute) to the manual <https://fr.wikibooks.org/wiki/Chill>`_ `Read (and contribute) to the manual <https://fr.wikibooks.org/wiki/Chill>`_
@@ -55,12 +55,11 @@ Available bundles
* Chill Person, to deal with persons, * Chill Person, to deal with persons,
* chill custom fields, to add custom fields to some entities, * chill custom fields, to add custom fields to some entities,
* chill activity: to add activities to people, * chill activity: to add activities to people,
* chill report: to add report to people, * chill report: to add report to people,
* chill event: to gather people into events, * chill event: to gather people into events,
* chill docs store: to store documents to people, but also entities, * chill docs store: to store documents to people, but also entities,
* chill task: to register task with people, * chill task: to register task with people,
* chill third party: to register third parties, * chill third party: to register third parties,
* chill family members: to register family members
You will also found the following projects : You will also found the following projects :

View File

@@ -0,0 +1,84 @@
Document storage
################
You can store document on two different ways:
- on disk
- in the cloud, using object storage: currently only `openstack swift <https://docs.openstack.org/api-ref/object-store/index.html>`_ is supported.
Comparison
==========
Storing documents within the cloud is particularily suitable for "portable" deployments, like in kubernetes, or within container
without having to manage volumes to store documents. But you'll have to subscribe on a commercial offer.
Storing documents on disk is more easy to configure, but more difficult to manage: if you use container, you will have to
manager volumes to attach documents on disk. You'll have to do some backup of the directory. If chill is load-balanced (and
multiple instances of chill are run), you will have to find a way to share the directories in read-write mode for every instance.
On Disk
=======
Configure Chill like this:
.. code-block:: yaml
# file config/packages/chill_doc_store.yaml
chill_doc_store:
use_driver: local_storage
local_storage:
storage_path: '%kernel.project_dir%/var/storage'
In this configuration, documents will be stored in :code:`var/storage` within your app directory. But this path can be
elsewhere on the disk. Be aware that the directory must be writable by the user executing the chill app (php-fpm or www-data).
Documents will be stored in subpathes within that directory. The files will be encrypted, the key is stored in the database.
In the cloud, using openstack object store
##########################################
You must subscribe to a commercial offer for object store.
Chill use some features to allow documents to be stored in the cloud without being uploaded first to the chill server:
- `Form POST Middelware <https://docs.openstack.org/swift/latest/api/form_post_middleware.html>`_;
- `Temporary URL Middelware <https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html>`_.
A secret key must be generated and configured, and CORS must be configured depending on the domain you will use to serve Chill.
At first, create a container and get the base path to the container. For instance, on OVH, if you create a container named "mychill",
you will be able to retrieve the base path of the container within the OVH interface, like this:
- base_path: :code:`https://storage.gra.cloud.ovh.net/v1/AUTH_123456789/mychill/` => will be variable :code:`ASYNC_UPLOAD_TEMP_URL_BASE_PATH`
- container: :code:`mychill` => will be variable :code:`ASYNC_UPLOAD_TEMP_URL_CONTAINER`
You can also generate a key, which should have at least 20 characters. This key will go in the variable :code:`ASYNC_UPLOAD_TEMP_URL_KEY`.
.. note::
See the `documentation of symfony <https://symfony.com/doc/current/configuration.html#config-env-vars>`_ on how to store variables, and how to encrypt them if needed.
Configure the storage like this:
.. code-block:: yaml
# file config/packages/chill_doc_store.yaml
chill_doc_store:
use_driver: openstack
openstack:
temp_url:
temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
container: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' # Required
temp_url_base_path: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required
Chill is able to configure the container in order to store document. Grab an Openstack Token (for instance, using :code:`openstack token issue` or
the web interface of your openstack provider), and run this command:
.. code-block:: bash
symfony console async-upload:configure --os_token=OPENSTACK_TOKEN -d https://mychill.mydomain.example
# or, without symfony-cli
bin/console async-upload:configure --os_token=OPENSTACK_TOKEN -d https://mychill.mydomain.example

View File

@@ -29,8 +29,7 @@ We strongly encourage you to initialize a git repository at this step, to track
# add the flex endpoints required for custom recipes # add the flex endpoints required for custom recipes
cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json
# install chill and some dependencies # install chill and some dependencies
# TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released symfony composer require chill-project/chill-bundles ^3.7.1 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev symfony/amqp-messenger
symfony composer require chill-project/chill-bundles v3.0.0-RC3 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev
We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database. We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database.
You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually). You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually).
@@ -48,7 +47,7 @@ You must also accept to configure recipes from the contrib repository, unless yo
If you encounter this error during assets compilation (:code:`yarn run encore production`) (repeated multiple times): If you encounter this error during assets compilation (:code:`yarn run encore production`) (repeated multiple times):
.. code-block:: txt .. code-block::
[tsl] ERROR in /tmp/chill/v1/public/bundles/chillcalendar/types.ts(2,65) [tsl] ERROR in /tmp/chill/v1/public/bundles/chillcalendar/types.ts(2,65)
TS2307: Cannot find module '../../../ChillMainBundle/Resources/public/types' or its corresponding type declarations. TS2307: Cannot find module '../../../ChillMainBundle/Resources/public/types' or its corresponding type declarations.
@@ -74,14 +73,22 @@ or in the :code:`.env.local` file, which should not be committed to the git repo
You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically
by the symfony server, from the docker compose services. by the symfony server, from the docker compose services.
The only required variable is the :code:`ADMIN_PASSWORD`. You can generate a hashed and salted admin password using the command The required variables are:
:code:`symfony console security:hash-password <your password> 'Symfony\Component\Security\Core\User\User'`. Then,
- the :code:`ADMIN_PASSWORD`;
- the :code:`OVHCLOUD_DSN` variable;
:code:`ADMIN_PASSWORD`
^^^^^^^^^^^^^^^^^^^^^^
You can generate a hashed and salted admin password using the command
:code:`symfony console security:hash-password <your password> 'Symfony\Component\Security\Core\User\User'`.Then,
you can either: you can either:
- add this password to the :code:`.env.local` file, you must escape the character :code:`$`: if the generated password - add this password to the :code:`.env.local` file, you must escape the character :code:`$`: if the generated password
is :code:`$2y$13$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm`, your :code:`.env.local` file will be: is :code:`$2y$13$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm`, your :code:`.env.local` file will be:
.. code-block:: env .. code-block:: bash
ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm
# note: if you copy-paste the line above, the password will be "admin". # note: if you copy-paste the line above, the password will be "admin".
@@ -89,12 +96,24 @@ you can either:
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env, - add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
not the password in clear text). not the password in clear text).
- set up the jwt authentication bundle :code:`OVHCLOUD_DSN` and sending SMS messages
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is a temporary dependency, for ensuring compatibility for previous behaviour.
You can set it to :code:`null://null` if you do not plan to use sending SMS.
.. code-block:: bash
OVHCLOUD_DSN=null://null
If you plan to do it, you can configure the notifier component `as described in the symfony documentation <https://symfony.com/doc/current/notifier.html#notifier-sms-channel>`_.
Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. Some environment variables are available for the JWT authentication bundle in the :code:`.env` file.
Prepare migrations and other tools Prepare database, messenger queue, and other configuration
---------------------------------- ----------------------------------------------------------
To continue the installation process, you will have to run migrations: To continue the installation process, you will have to run migrations:
@@ -109,17 +128,22 @@ To continue the installation process, you will have to run migrations:
symfony console messenger:setup-transports symfony console messenger:setup-transports
# prepare some views # prepare some views
symfony console chill:db:sync-views symfony console chill:db:sync-views
# load languages data
symfony console chill:main:languages:populate
# generate jwt token, required for some api features (webdav access, ...) # generate jwt token, required for some api features (webdav access, ...)
symfony console lexik:jwt:generate-keypair symfony console lexik:jwt:generate-keypair
.. warning:: .. note::
If you encounter an error while running :code:`symfony console messenger:setup-transports`, you can set up the messenger If you encounter this error:
transport to redis, by adding this in the :code:`.env.local` or :code:`.env` file:
.. code-block::
No transport supports the given Messenger DSN.
Please check that you installed the package `symfony/amqp-messenger`.
.. code-block:: env
MESSENGER_TRANSPORT_DSN=redis://${REDIS_HOST}:${REDIS_PORT}/messages
Start your web server locally Start your web server locally
----------------------------- -----------------------------
@@ -323,6 +347,7 @@ Going further
:maxdepth: 2 :maxdepth: 2
prod.rst prod.rst
document-storage.rst
load-addresses.rst load-addresses.rst
prod-calendar-sms-sending.rst prod-calendar-sms-sending.rst
msgraph-configure.rst msgraph-configure.rst

View File

@@ -41,16 +41,18 @@ Postal code are loaded from this database. There is no need to load postal codes
The data are prepared for Chill (`See this repository <https://gitea.champs-libres.be/Chill-project/belgian-bestaddresses-transform/releases>`_). The data are prepared for Chill (`See this repository <https://gitea.champs-libres.be/Chill-project/belgian-bestaddresses-transform/releases>`_).
One can select postal code by his first number (:code:`1xxx` for postal codes from 1000 to 1999), or a limited list for development purpose. One can select postal code by his first number (:code:`1xxx` for postal codes from 1000 to 1999), or a limited list for development purpose.
The command expects a language code as first argument.
.. code-block:: bash .. code-block:: bash
# load postal code from 1000 to 3999: # load postal code from 1000 to 3999:
bin/console chill:main:address-ref-from-best-addresse 1xxx 2xxx 3xxx bin/console chill:main:address-ref-from-best-addresse fr 1xxx 2xxx 3xxx
# load only an extract (for dev purposes) # load only an extract (for dev purposes)
bin/console chill:main:address-ref-from-best-addresse extract bin/console chill:main:address-ref-from-best-addresse fr extract
# load full addresses (discouraged) # load full addresses (discouraged)
bin/console chill:main:address-ref-from-best-addresse full bin/console chill:main:address-ref-from-best-addresse fr full
.. note:: .. note::

40
eslint.config.mjs Normal file
View File

@@ -0,0 +1,40 @@
import eslintPluginVue from "eslint-plugin-vue";
import ts from "typescript-eslint";
import eslintPluginPrettier from "eslint-plugin-prettier";
export default ts.config(
...ts.configs.recommended,
...ts.configs.stylistic,
...eslintPluginVue.configs["flat/essential"],
{
files: ["**/*.vue"],
languageOptions: {
parserOptions: {
parser: "@typescript-eslint/parser",
},
},
},
{
ignores: [
"**/vendor/*",
"**/import-png.d.ts",
"**/chill.webpack.config.js",
"**/var/*",
"**/docker/*",
"**/node_modules/*",
"**/public/build/*"
],
},
{
plugins: {
prettier: eslintPluginPrettier,
},
rules: {
"prettier/prettier": "error",
// override/add rules settings here, such as:
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/ban-ts-comment": "off"
},
},
);

View File

@@ -6,33 +6,41 @@
"@apidevtools/swagger-cli": "^4.0.4", "@apidevtools/swagger-cli": "^4.0.4",
"@babel/core": "^7.20.5", "@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.20.2",
"@ckeditor/ckeditor5-build-classic": "^41.4.2", "@ckeditor/ckeditor5-vue": "^7.3.0",
"@ckeditor/ckeditor5-dev-translations": "^40.2.0", "@eslint/js": "^9.14.0",
"@ckeditor/ckeditor5-dev-utils": "^40.2.0", "@hotwired/stimulus": "^3.0.0",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13", "@luminateone/eslint-baseline": "^1.0.9",
"@ckeditor/ckeditor5-markdown-gfm": "^41.4.2", "@symfony/stimulus-bridge": "^3.2.0",
"@ckeditor/ckeditor5-theme-lark": "^41.4.2", "@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@ckeditor/ckeditor5-vue": "^5.1.0",
"@symfony/webpack-encore": "^4.1.0", "@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node14": "^1.0.1", "@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/eslint__js": "^8.42.3",
"@typescript-eslint/parser": "^8.12.2",
"bindings": "^1.5.0", "bindings": "^1.5.0",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"ckeditor5": "^44.1.0",
"dompurify": "^3.1.0", "dompurify": "^3.1.0",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.30.0",
"fork-awesome": "^1.1.7", "fork-awesome": "^1.1.7",
"intl-messageformat": "^10.5.11",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"marked": "^12.0.1",
"node-sass": "^8.0.0", "node-sass": "^8.0.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss-loader": "^7.0.2", "postcss-loader": "^7.0.2",
"prettier": "^3.3.3",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"sass-loader": "^14.0.0", "sass-loader": "^14.0.0",
"select2": "^4.0.13", "select2": "^4.0.13",
"select2-bootstrap-theme": "0.1.0-beta.10", "select2-bootstrap-theme": "0.1.0-beta.10",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"ts-loader": "^9.3.1", "ts-loader": "^9.3.1",
"typescript": "^5.4.5", "typescript": "^5.6.3",
"typescript-eslint": "^8.13.0",
"vue-loader": "^17.0.0", "vue-loader": "^17.0.0",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
@@ -45,11 +53,13 @@
"@fullcalendar/timegrid": "^6.1.4", "@fullcalendar/timegrid": "^6.1.4",
"@fullcalendar/vue3": "^6.1.4", "@fullcalendar/vue3": "^6.1.4",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"dropzone": "^5.7.6", "dropzone": "^5.7.6",
"es6-promise": "^4.2.8", "es6-promise": "^4.2.8",
"intl-messageformat": "^10.5.11",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"marked": "^12.0.2", "marked": "^12.0.2",
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
@@ -63,13 +73,19 @@
"vuex": "^4.0.0" "vuex": "^4.0.0"
}, },
"browserslist": [ "browserslist": [
"Firefox ESR" "defaults and fully supports es6-module and not dead"
], ],
"scripts": { "scripts": {
"dev-server": "encore dev-server", "dev-server": "encore dev-server",
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress" "build": "encore production --progress",
"specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml",
"specs-validate": "swagger-cli validate templates/api/specs.yaml",
"specs-create-dir": "mkdir -p templates/api",
"specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate",
"version": "node --version",
"eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\""
}, },
"private": true "private": true
} }

View File

@@ -20,6 +20,10 @@ return static function (RectorConfig $rectorConfig): void {
__DIR__ . '/src', __DIR__ . '/src',
]); ]);
$rectorConfig->skip([
\Rector\Php55\Rector\String_\StringClassNameToClassConstantRector::class => __DIR__ . 'src/Bundle/ChillMainBundle/Service/Notifier/LegacyOvhCloudFactory.php'
]);
$rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/test/App_KernelTestDebugContainer.xml '); $rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/test/App_KernelTestDebugContainer.xml ');
$rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/symfony-container.php'); $rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/symfony-container.php');

View File

@@ -84,11 +84,6 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
*/ */
#[Assert\Valid(traverse: true)] #[Assert\Valid(traverse: true)]
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])] #[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])]
#[ORM\JoinTable(
name: 'activity_storedobject',
joinColumns: new ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', nullable: false),
inverseJoinColumns: new ORM\InverseJoinColumn(name: 'storedobject_id', referencedColumnName: 'id', unique: true, nullable: false)
)]
private Collection $documents; private Collection $documents;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TIME_MUTABLE, nullable: true)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TIME_MUTABLE, nullable: true)]

View File

@@ -55,6 +55,10 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
.' AND ' .' AND '
.'(person_person_having_activity.id = person.id OR person MEMBER OF activity_person_having_activity.persons)'); .'(person_person_having_activity.id = person.id OR person MEMBER OF activity_person_having_activity.persons)');
if (\in_array('activity', $qb->getAllAliases(), true)) {
$sqb->andWhere('activity_person_having_activity.id = activity.id');
}
if (isset($data['reasons']) && [] !== $data['reasons']) { if (isset($data['reasons']) && [] !== $data['reasons']) {
// add clause activity reason // add clause activity reason
$sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity'); $sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Menu; namespace Chill\ActivityBundle\Menu;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -23,22 +24,30 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/ */
class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
{ {
public function __construct(protected Security $security, protected TranslatorInterface $translator) {} public function __construct(
protected Security $security,
protected TranslatorInterface $translator,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {}
public function buildMenu($menuId, MenuItem $menu, array $parameters) public function buildMenu($menuId, MenuItem $menu, array $parameters)
{ {
$period = $parameters['accompanyingCourse']; $period = $parameters['accompanyingCourse'];
$activities = $this->managerRegistry->getManager()->getRepository(Activity::class)->findBy(
['accompanyingPeriod' => $period]
);
if ( if (
AccompanyingPeriod::STEP_DRAFT !== $period->getStep() AccompanyingPeriod::STEP_DRAFT !== $period->getStep()
&& $this->security->isGranted(ActivityVoter::SEE, $period) && $this->security->isGranted(ActivityVoter::SEE, $period)
) { ) {
$menu->addChild($this->translator->trans('Activity'), [ $menu->addChild($this->translator->trans('Activities'), [
'route' => 'chill_activity_activity_list', 'route' => 'chill_activity_activity_list',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
], ]) ], ])
->setExtras(['order' => 40]); ->setExtras(['order' => 40, 'counter' => count($activities) > 0 ? count($activities) : null]);
} }
} }

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Menu; namespace Chill\ActivityBundle\Menu;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
@@ -23,13 +24,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/ */
final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface
{ {
public function __construct(private AuthorizationCheckerInterface $authorizationChecker, private TranslatorInterface $translator) {} public function __construct(
private readonly ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
private AuthorizationCheckerInterface $authorizationChecker,
private TranslatorInterface $translator,
) {}
public function buildMenu($menuId, MenuItem $menu, array $parameters) public function buildMenu($menuId, MenuItem $menu, array $parameters)
{ {
/** @var Person $person */ /** @var Person $person */
$person = $parameters['person']; $person = $parameters['person'];
$count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE);
if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) { if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) {
$menu->addChild( $menu->addChild(
$this->translator->trans('Activities'), $this->translator->trans('Activities'),
@@ -38,7 +46,7 @@ final readonly class PersonMenuBuilder implements LocalMenuBuilderInterface
'routeParameters' => ['person_id' => $person->getId()], 'routeParameters' => ['person_id' => $person->getId()],
] ]
) )
->setExtra('order', 201); ->setExtras(['order' => 201, 'counter' => $count > 0 ? $count : null]);
} }
} }

View File

@@ -15,10 +15,13 @@ use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\NotificationHandlerInterface; use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
final readonly class ActivityNotificationHandler implements NotificationHandlerInterface final readonly class ActivityNotificationHandler implements NotificationHandlerInterface
{ {
public function __construct(private ActivityRepository $activityRepository) {} public function __construct(private ActivityRepository $activityRepository, private TranslatableStringHelperInterface $translatableStringHelper) {}
public function getTemplate(Notification $notification, array $options = []): string public function getTemplate(Notification $notification, array $options = []): string
{ {
@@ -37,4 +40,30 @@ final readonly class ActivityNotificationHandler implements NotificationHandlerI
{ {
return Activity::class === $notification->getRelatedEntityClass(); return Activity::class === $notification->getRelatedEntityClass();
} }
public function getTitle(Notification $notification, array $options = []): TranslatableInterface
{
if (null === $activity = $this->getRelatedEntity($notification)) {
return new TranslatableMessage('activity.deleted');
}
return new TranslatableMessage('activity.title', [
'date' => $activity->getDate(),
'type' => $this->translatableStringHelper->localize($activity->getActivityType()->getName()),
]);
}
public function getAssociatedPersons(Notification $notification, array $options = []): array
{
if (null === $activity = $this->getRelatedEntity($notification)) {
return [];
}
return $activity->getPersonsAssociated();
}
public function getRelatedEntity(Notification $notification): ?Activity
{
return $this->activityRepository->find($notification->getRelatedEntityId());
}
} }

View File

@@ -120,3 +120,34 @@ li.document-list-item {
vertical-align: baseline; vertical-align: baseline;
} }
} }
.badge-activity-type-simple {
@extend .badge;
display: inline-block;
margin: 0.2rem 0;
padding-left: 0;
padding-right: 0.5rem;
border-left: 20px groove #9acd32;
border-radius: $badge-border-radius;
color: black;
font-weight: normal;
font-size: unset;
max-width: 100%;
background-color: $gray-100;
overflow: hidden;
text-overflow: ellipsis;
text-indent: 5px hanging;
text-align: left;
&::before {
margin-right: 3px;
position: relative;
left: -0.5px;
font-family: ForkAwesome;
content: '\f04b';
color: #9acd32;
}
}

View File

@@ -1 +1 @@
require('./chillactivity.scss'); require("./chillactivity.scss");

View File

@@ -1,21 +1,21 @@
<template> <template>
<concerned-groups v-if="hasPerson"></concerned-groups> <concerned-groups v-if="hasPerson" />
<social-issues-acc v-if="hasSocialIssues"></social-issues-acc> <social-issues-acc v-if="hasSocialIssues" />
<location v-if="hasLocation"></location> <location v-if="hasLocation" />
</template> </template>
<script> <script>
import ConcernedGroups from './components/ConcernedGroups.vue'; import ConcernedGroups from "./components/ConcernedGroups.vue";
import SocialIssuesAcc from './components/SocialIssuesAcc.vue'; import SocialIssuesAcc from "./components/SocialIssuesAcc.vue";
import Location from './components/Location.vue'; import Location from "./components/Location.vue";
export default { export default {
name: "App", name: "App",
props: ['hasSocialIssues', 'hasLocation', 'hasPerson'], props: ["hasSocialIssues", "hasLocation", "hasPerson", "isSimpleEditor"],
components: { components: {
ConcernedGroups, ConcernedGroups,
SocialIssuesAcc, SocialIssuesAcc,
Location Location,
} },
} };
</script> </script>

View File

@@ -1,38 +1,39 @@
import { getSocialIssues } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api.js'; import { getSocialIssues } from "ChillPersonAssets/vuejs/AccompanyingCourse/api.js";
import { fetchResults } from 'ChillMainAssets/lib/api/apiMethods'; import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
/* /*
* Load socialActions by socialIssue (id) * Load socialActions by socialIssue (id)
*/ */
const getSocialActionByIssue = (id) => { const getSocialActionByIssue = (id) => {
const url = `/api/1.0/person/social/social-action/by-social-issue/${id}.json`; const url = `/api/1.0/person/social/social-action/by-social-issue/${id}.json`;
return fetch(url) return fetch(url).then((response) => {
.then(response => { if (response.ok) {
if (response.ok) { return response.json(); } return response.json();
throw Error('Error with request resource response'); }
}); throw Error("Error with request resource response");
});
}; };
const getLocations = () => fetchResults('/api/1.0/main/location.json'); const getLocations = () => fetchResults("/api/1.0/main/location.json");
const getLocationTypes = () => fetchResults('/api/1.0/main/location-type.json'); const getLocationTypes = () => fetchResults("/api/1.0/main/location-type.json");
const getUserCurrentLocation =
() => fetch('/api/1.0/main/user-current-location.json')
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
const getUserCurrentLocation = () =>
fetch("/api/1.0/main/user-current-location.json").then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
/* /*
* Load Location Type by defaultFor * Load Location Type by defaultFor
* @param {string} entity - can be "person" or "thirdparty" * @param {string} entity - can be "person" or "thirdparty"
*/ */
const getLocationTypeByDefaultFor = (entity) => { const getLocationTypeByDefaultFor = (entity) => {
return getLocationTypes().then(results => return getLocationTypes().then(
results.filter(t => t.defaultFor === entity)[0] (results) => results.filter((t) => t.defaultFor === entity)[0],
); );
}; };
/** /**
@@ -43,26 +44,27 @@ const getLocationTypeByDefaultFor = (entity) => {
* @returns {Promise<T>} * @returns {Promise<T>}
*/ */
const postLocation = (body) => { const postLocation = (body) => {
const url = `/api/1.0/main/location.json`; const url = `/api/1.0/main/location.json`;
return fetch(url, { return fetch(url, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json;charset=utf-8' "Content-Type": "application/json;charset=utf-8",
}, },
body: JSON.stringify(body) body: JSON.stringify(body),
}) }).then((response) => {
.then(response => { if (response.ok) {
if (response.ok) { return response.json(); } return response.json();
throw Error('Error with request resource response'); }
}); throw Error("Error with request resource response");
});
}; };
export { export {
getSocialIssues, getSocialIssues,
getSocialActionByIssue, getSocialActionByIssue,
getLocations, getLocations,
getLocationTypes, getLocationTypes,
getLocationTypeByDefaultFor, getLocationTypeByDefaultFor,
postLocation, postLocation,
getUserCurrentLocation getUserCurrentLocation,
}; };

View File

@@ -1,219 +1,257 @@
<template> <template>
<teleport to="#add-persons" v-if="isComponentVisible"> <teleport to="#add-persons" v-if="isComponentVisible">
<div class="flex-bloc concerned-groups" :class="getContext">
<persons-bloc
v-for="bloc in contextPersonsBlocs"
:key="bloc.key"
:bloc="bloc"
:bloc-width="getBlocWidth"
:set-persons-in-bloc="setPersonsInBloc"
/>
</div>
<div
v-if="
getContext === 'accompanyingCourse' &&
suggestedEntities.length > 0
"
>
<ul class="list-suggest add-items inline">
<li
v-for="(p, i) in suggestedEntities"
@click="addSuggestedEntity(p)"
:key="`suggestedEntities-${i}`"
>
<person-text v-if="p.type === 'person'" :person="p" />
<span v-else>{{ p.text }}</span>
</li>
</ul>
</div>
<div class="flex-bloc concerned-groups" :class="getContext"> <ul class="record_actions">
<persons-bloc <li class="add-persons">
v-for="bloc in contextPersonsBlocs" <add-persons
v-bind:key="bloc.key" :buttonTitle="trans(ACTIVITY_ADD_PERSONS)"
v-bind:bloc="bloc" :modalTitle="trans(ACTIVITY_ADD_PERSONS)"
v-bind:blocWidth="getBlocWidth" v-bind:key="addPersons.key"
v-bind:setPersonsInBloc="setPersonsInBloc"> v-bind:options="addPersonsOptions"
</persons-bloc> @addNewPersons="addNewPersons"
</div> ref="addPersons"
<div v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0"> >
<ul class="list-suggest add-items inline"> </add-persons>
<li v-for="(p, i) in suggestedEntities" @click="addSuggestedEntity(p)" :key="`suggestedEntities-${i}`"> </li>
<person-text v-if="p.type === 'person'" :person="p"></person-text> </ul>
<span v-else>{{ p.text }}</span> </teleport>
</li>
</ul>
</div>
<ul class="record_actions">
<li class="add-persons">
<add-persons
buttonTitle="activity.add_persons"
modalTitle="activity.add_persons"
v-bind:key="addPersons.key"
v-bind:options="addPersonsOptions"
@addNewPersons="addNewPersons"
ref="addPersons">
</add-persons>
</li>
</ul>
</teleport>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from "vuex";
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'; import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
import PersonsBloc from './ConcernedGroups/PersonsBloc.vue'; import PersonsBloc from "./ConcernedGroups/PersonsBloc.vue";
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue'; import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
import {
ACTIVITY_BLOC_PERSONS,
ACTIVITY_BLOC_PERSONS_ASSOCIATED,
ACTIVITY_BLOC_THIRDPARTY,
ACTIVITY_BLOC_USERS,
ACTIVITY_ADD_PERSONS,
trans,
} from "translator";
export default { export default {
name: "ConcernedGroups", name: "ConcernedGroups",
components: { components: {
AddPersons, AddPersons,
PersonsBloc, PersonsBloc,
PersonText PersonText,
}, },
data() { setup() {
return { return {
personsBlocs: [ trans,
{ key: 'persons', ACTIVITY_ADD_PERSONS,
title: 'activity.bloc_persons', };
persons: [], },
included: false data() {
return {
personsBlocs: [
{
key: "persons",
title: trans(ACTIVITY_BLOC_PERSONS),
persons: [],
included: false,
},
{
key: "personsAssociated",
title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED),
persons: [],
included: window.activity
? window.activity.activityType.personsVisible !== 0
: true,
},
{
key: "personsNotAssociated",
title: "activity.bloc_persons_not_associated",
persons: [],
included: window.activity
? window.activity.activityType.personsVisible !== 0
: true,
},
{
key: "thirdparty",
title: trans(ACTIVITY_BLOC_THIRDPARTY),
persons: [],
included: window.activity
? window.activity.activityType.thirdPartiesVisible !== 0
: true,
},
{
key: "users",
title: trans(ACTIVITY_BLOC_USERS),
persons: [],
included: window.activity
? window.activity.activityType.usersVisible !== 0
: true,
},
],
addPersons: {
key: "activity",
}, },
{ key: 'personsAssociated', };
title: 'activity.bloc_persons_associated', },
persons: [], computed: {
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true isComponentVisible() {
}, return window.activity
{ key: 'personsNotAssociated', ? window.activity.activityType.personsVisible !== 0 ||
title: 'activity.bloc_persons_not_associated', window.activity.activityType.thirdPartiesVisible !== 0 ||
persons: [], window.activity.activityType.usersVisible !== 0
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true : true;
}, },
{ key: 'thirdparty', ...mapState({
title: 'activity.bloc_thirdparty', persons: (state) => state.activity.persons,
persons: [], thirdParties: (state) => state.activity.thirdParties,
included: window.activity ? window.activity.activityType.thirdPartiesVisible !== 0 : true users: (state) => state.activity.users,
}, accompanyingCourse: (state) => state.activity.accompanyingPeriod,
{ key: 'users', }),
title: 'activity.bloc_users', ...mapGetters(["suggestedEntities"]),
persons: [], getContext() {
included: window.activity ? window.activity.activityType.usersVisible !== 0 : true return this.accompanyingCourse ? "accompanyingCourse" : "person";
}, },
], contextPersonsBlocs() {
addPersons: { return this.personsBlocs.filter((bloc) => bloc.included !== false);
key: 'activity' },
} addPersonsOptions() {
} let optionsType = [];
}, if (window.activity) {
computed: { if (window.activity.activityType.personsVisible !== 0) {
isComponentVisible() { optionsType.push("person");
return window.activity }
? (window.activity.activityType.personsVisible !== 0 || window.activity.activityType.thirdPartiesVisible !== 0 || window.activity.activityType.usersVisible !== 0) if (window.activity.activityType.thirdPartiesVisible !== 0) {
: true optionsType.push("thirdparty");
}, }
...mapState({ if (window.activity.activityType.usersVisible !== 0) {
persons: state => state.activity.persons, optionsType.push("user");
thirdParties: state => state.activity.thirdParties, }
users: state => state.activity.users,
accompanyingCourse: state => state.activity.accompanyingPeriod
}),
...mapGetters([
'suggestedEntities'
]),
getContext() {
return (this.accompanyingCourse) ? "accompanyingCourse" : "person";
},
contextPersonsBlocs() {
return this.personsBlocs.filter(bloc => bloc.included !== false);
},
addPersonsOptions() {
let optionsType = [];
if (window.activity) {
if (window.activity.activityType.personsVisible !== 0) {
optionsType.push('person')
}
if (window.activity.activityType.thirdPartiesVisible !== 0) {
optionsType.push('thirdparty')
}
if (window.activity.activityType.usersVisible !== 0) {
optionsType.push('user')
}
} else {
optionsType = ['person', 'thirdparty', 'user'];
}
return {
type: optionsType,
priority: null,
uniq: false,
button: {
size: 'btn-sm'
}
}
},
getBlocWidth() {
return Math.round(100/(this.contextPersonsBlocs.length)) + '%';
}
},
mounted() {
this.setPersonsInBloc();
},
methods: {
setPersonsInBloc() {
let groups;
if (this.accompanyingCourse) {
groups = this.splitPersonsInGroups();
}
this.personsBlocs.forEach(bloc => {
if (this.accompanyingCourse) {
switch (bloc.key) {
case 'personsAssociated':
bloc.persons = groups.personsAssociated;
bloc.included = true;
break;
case 'personsNotAssociated':
bloc.persons = groups.personsNotAssociated;
bloc.included = true;
break;
}
} else { } else {
switch (bloc.key) { optionsType = ["person", "thirdparty", "user"];
case 'persons':
bloc.persons = this.persons;
bloc.included = true;
break;
}
} }
switch (bloc.key) { return {
case 'thirdparty': type: optionsType,
bloc.persons = this.thirdParties; priority: null,
break; uniq: false,
case 'users': button: {
bloc.persons = this.users; size: "btn-sm",
break; },
} };
}, groups); },
}, getBlocWidth() {
splitPersonsInGroups() { return Math.round(100 / this.contextPersonsBlocs.length) + "%";
let personsAssociated = []; },
let personsNotAssociated = this.persons; },
let participations = this.getCourseParticipations(); mounted() {
this.persons.forEach(person => {
participations.forEach(participation => {
if (person.id === participation.id) {
//console.log(person.id);
personsAssociated.push(person);
personsNotAssociated = personsNotAssociated.filter(p => p !== person);
}
});
});
return {
'personsAssociated': personsAssociated,
'personsNotAssociated': personsNotAssociated
};
},
getCourseParticipations() {
let participations = [];
this.accompanyingCourse.participations.forEach(participation => {
if (!participation.endDate) {
participations.push(participation.person);
}
});
return participations;
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach((item) => {
this.$store.dispatch('addPersonsInvolved', item);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
this.setPersonsInBloc();
},
addSuggestedEntity(person) {
this.$store.dispatch('addPersonsInvolved', { result: person, type: 'person' });
this.setPersonsInBloc(); this.setPersonsInBloc();
}, },
} methods: {
} setPersonsInBloc() {
let groups;
if (this.accompanyingCourse) {
groups = this.splitPersonsInGroups();
}
this.personsBlocs.forEach((bloc) => {
if (this.accompanyingCourse) {
switch (bloc.key) {
case "personsAssociated":
bloc.persons = groups.personsAssociated;
bloc.included = true;
break;
case "personsNotAssociated":
bloc.persons = groups.personsNotAssociated;
bloc.included = true;
break;
}
} else {
switch (bloc.key) {
case "persons":
bloc.persons = this.persons;
bloc.included = true;
break;
}
}
switch (bloc.key) {
case "thirdparty":
bloc.persons = this.thirdParties;
break;
case "users":
bloc.persons = this.users;
break;
}
}, groups);
},
splitPersonsInGroups() {
let personsAssociated = [];
let personsNotAssociated = this.persons;
let participations = this.getCourseParticipations();
this.persons.forEach((person) => {
participations.forEach((participation) => {
if (person.id === participation.id) {
//console.log(person.id);
personsAssociated.push(person);
personsNotAssociated = personsNotAssociated.filter(
(p) => p !== person,
);
}
});
});
return {
personsAssociated: personsAssociated,
personsNotAssociated: personsNotAssociated,
};
},
getCourseParticipations() {
let participations = [];
this.accompanyingCourse.participations.forEach((participation) => {
if (!participation.endDate) {
participations.push(participation.person);
}
});
return participations;
},
addNewPersons({ selected, modal }) {
console.log("@@@ CLICK button addNewPersons", selected);
selected.forEach((item) => {
this.$store.dispatch("addPersonsInvolved", item);
}, this);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
this.setPersonsInBloc();
},
addSuggestedEntity(person) {
this.$store.dispatch("addPersonsInvolved", {
result: person,
type: "person",
});
this.setPersonsInBloc();
},
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped></style>
</style>

View File

@@ -1,32 +1,30 @@
<template> <template>
<li> <li>
<span :title="person.text" @click.prevent="$emit('remove', person)"> <span :title="person.text" @click.prevent="$emit('remove', person)">
<span class="chill_denomination"> <span class="chill_denomination">
<person-text :person="person" :isCut="true"></person-text> <person-text :person="person" :is-cut="true" />
</span> </span>
</span> </span>
</li> </li>
</template> </template>
<script> <script>
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue'; import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
export default { export default {
name: "PersonBadge", name: "PersonBadge",
props: ['person'], props: ["person"],
components: { components: {
PersonText PersonText,
}, },
// computed: { // computed: {
// textCutted() { // textCutted() {
// let more = (this.person.text.length > 15) ?'…' : ''; // let more = (this.person.text.length > 15) ?'…' : '';
// return this.person.text.slice(0,15) + more; // return this.person.text.slice(0,15) + more;
// } // }
// }, // },
emits: ['remove'], emits: ["remove"],
} };
</script> </script>
<style lang="css" scoped> <style lang="css" scoped></style>
</style>

View File

@@ -1,41 +1,39 @@
<template> <template>
<div class="item-bloc" :style="{ 'flex-basis': blocWidth }"> <div class="item-bloc" :style="{ 'flex-basis': blocWidth }">
<div class="item-row"> <div class="item-row">
<div class="item-col"> <div class="item-col">
<h4>{{ $t(bloc.title) }}</h4> <h4>{{ $t(bloc.title) }}</h4>
</div> </div>
<div class="item-col"> <div class="item-col">
<ul class="list-suggest remove-items"> <ul class="list-suggest remove-items">
<person-badge <person-badge
v-for="person in bloc.persons" v-for="person in bloc.persons"
v-bind:key="person.id" :key="person.id"
v-bind:person="person" :person="person"
@remove="removePerson"> @remove="removePerson"
</person-badge> />
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import PersonBadge from './PersonBadge.vue'; import PersonBadge from "./PersonBadge.vue";
export default { export default {
name:"PersonsBloc", name: "PersonsBloc",
components: { components: {
PersonBadge PersonBadge,
}, },
props: ['bloc', 'setPersonsInBloc', 'blocWidth'], props: ["bloc", "setPersonsInBloc", "blocWidth"],
methods: { methods: {
removePerson(item) { removePerson(item) {
console.log('@@ CLICK remove person: item', item); console.log("@@ CLICK remove person: item", item);
this.$store.dispatch('removePersonInvolved', item); this.$store.dispatch("removePersonInvolved", item);
this.setPersonsInBloc(); this.setPersonsInBloc();
} },
} },
} };
</script> </script>
<style lang="scss"> <style lang="scss"></style>
</style>

View File

@@ -2,7 +2,7 @@
<teleport to="#location"> <teleport to="#location">
<div class="mb-3 row"> <div class="mb-3 row">
<label :class="locationClassList"> <label :class="locationClassList">
{{ $t("activity.location") }} {{ trans(ACTIVITY_LOCATION) }}
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<VueMultiselect <VueMultiselect
@@ -13,18 +13,17 @@
open-direction="top" open-direction="top"
:multiple="false" :multiple="false"
:searchable="true" :searchable="true"
:placeholder="$t('activity.choose_location')" :placeholder="trans(ACTIVITY_CHOOSE_LOCATION)"
:custom-label="customLabel" :custom-label="customLabel"
:select-label="$t('multiselect.select_label')" :select-label="trans(MULTISELECT_SELECT_LABEL)"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
:selected-label="$t('multiselect.selected_label')" :selected-label="trans(MULTISELECT_SELECTED_LABEL)"
:options="availableLocations" :options="availableLocations"
group-values="locations" group-values="locations"
group-label="locationGroup" group-label="locationGroup"
v-model="location" v-model="location"
> />
</VueMultiselect> <new-location v-bind:available-locations="availableLocations" />
<new-location v-bind:availableLocations="availableLocations"></new-location>
</div> </div>
</div> </div>
</teleport> </teleport>
@@ -34,6 +33,14 @@
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
import NewLocation from "./Location/NewLocation.vue"; import NewLocation from "./Location/NewLocation.vue";
import {
trans,
ACTIVITY_LOCATION,
ACTIVITY_CHOOSE_LOCATION,
MULTISELECT_SELECT_LABEL,
MULTISELECT_DESELECT_LABEL,
MULTISELECT_SELECTED_LABEL,
} from "translator";
export default { export default {
name: "Location", name: "Location",
@@ -41,11 +48,20 @@ export default {
NewLocation, NewLocation,
VueMultiselect, VueMultiselect,
}, },
setup() {
return {
trans,
ACTIVITY_LOCATION,
ACTIVITY_CHOOSE_LOCATION,
MULTISELECT_SELECT_LABEL,
MULTISELECT_DESELECT_LABEL,
MULTISELECT_SELECTED_LABEL,
};
},
data() { data() {
return { return {
locationClassList: locationClassList: `col-form-label col-sm-4 ${document.querySelector("input#chill_activitybundle_activity_location").getAttribute("required") ? "required" : ""}`,
`col-form-label col-sm-4 ${document.querySelector('input#chill_activitybundle_activity_location').getAttribute('required') ? 'required' : ''}`, };
}
}, },
computed: { computed: {
...mapState(["activity", "availableLocations"]), ...mapState(["activity", "availableLocations"]),
@@ -61,16 +77,16 @@ export default {
}, },
methods: { methods: {
labelAccompanyingCourseLocation(value) { labelAccompanyingCourseLocation(value) {
return `${value.address.text} (${value.locationType.title.fr})` return `${value.address.text} (${value.locationType.title.fr})`;
}, },
customLabel(value) { customLabel(value) {
return value.locationType return value.locationType
? value.name ? value.name
? value.name === '__AccompanyingCourseLocation__' ? value.name === "__AccompanyingCourseLocation__"
? this.labelAccompanyingCourseLocation(value) ? this.labelAccompanyingCourseLocation(value)
: `${value.name} (${value.locationType.title.fr})` : `${value.name} (${value.locationType.title.fr})`
: value.locationType.title.fr : value.locationType.title.fr
: ''; : "";
}, },
}, },
}; };

View File

@@ -3,40 +3,65 @@
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<a class="btn btn-sm btn-create" @click="openModal"> <a class="btn btn-sm btn-create" @click="openModal">
{{ $t('activity.create_new_location') }} {{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
</a> </a>
</li> </li>
</ul> </ul>
<teleport to="body"> <teleport to="body">
<modal v-if="modal.showModal" <modal
v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass" :modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false"> @close="modal.showModal = false"
>
<template v-slot:header> <template #header>
<h3 class="modal-title">{{ $t('activity.create_new_location') }}</h3> <h3 class="modal-title">
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
</h3>
</template> </template>
<template v-slot:body> <template #body>
<form> <form>
<div class="alert alert-warning" v-if="errors.length"> <div class="alert alert-warning" v-if="errors.length">
<ul> <ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li> <li v-for="(e, i) in errors" :key="i">
{{ e }}
</li>
</ul> </ul>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<select class="form-select form-select-lg" id="type" required v-model="selectType"> <select
<option selected disabled value="">{{ $t('activity.choose_location_type') }}</option> class="form-select form-select-lg"
<option v-for="t in locationTypes" :value="t" :key="t.id"> id="type"
required
v-model="selectType"
>
<option selected disabled value="">
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
</option>
<option
v-for="t in locationTypes"
:value="t"
:key="t.id"
>
{{ t.title.fr }} {{ t.title.fr }}
</option> </option>
</select> </select>
<label>{{ $t('activity.location_fields.type') }}</label> <label>{{
trans(ACTIVITY_LOCATION_FIELDS_TYPE)
}}</label>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input class="form-control form-control-lg" id="name" v-model="inputName" placeholder /> <input
<label for="name">{{ $t('activity.location_fields.name') }}</label> class="form-control form-control-lg"
id="name"
v-model="inputName"
placeholder
/>
<label for="name">{{
trans(ACTIVITY_LOCATION_FIELDS_NAME)
}}</label>
</div> </div>
<add-address <add-address
@@ -44,43 +69,74 @@
:options="addAddress.options" :options="addAddress.options"
:addressChangedCallback="submitNewAddress" :addressChangedCallback="submitNewAddress"
v-if="showAddAddress" v-if="showAddAddress"
ref="addAddress"> ref="addAddress"
</add-address> />
<div class="form-floating mb-3" v-if="showContactData"> <div class="form-floating mb-3" v-if="showContactData">
<input class="form-control form-control-lg" id="phonenumber1" v-model="inputPhonenumber1" placeholder /> <input
<label for="phonenumber1">{{ $t('activity.location_fields.phonenumber1') }}</label> class="form-control form-control-lg"
id="phonenumber1"
v-model="inputPhonenumber1"
placeholder
/>
<label for="phonenumber1">{{
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1)
}}</label>
</div> </div>
<div class="form-floating mb-3" v-if="hasPhonenumber1"> <div class="form-floating mb-3" v-if="hasPhonenumber1">
<input class="form-control form-control-lg" id="phonenumber2" v-model="inputPhonenumber2" placeholder /> <input
<label for="phonenumber2">{{ $t('activity.location_fields.phonenumber2') }}</label> class="form-control form-control-lg"
id="phonenumber2"
v-model="inputPhonenumber2"
placeholder
/>
<label for="phonenumber2">{{
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2)
}}</label>
</div> </div>
<div class="form-floating mb-3" v-if="showContactData"> <div class="form-floating mb-3" v-if="showContactData">
<input class="form-control form-control-lg" id="email" v-model="inputEmail" placeholder /> <input
<label for="email">{{ $t('activity.location_fields.email') }}</label> class="form-control form-control-lg"
id="email"
v-model="inputEmail"
placeholder
/>
<label for="email">{{
trans(ACTIVITY_LOCATION_FIELDS_EMAIL)
}}</label>
</div> </div>
</form> </form>
</template> </template>
<template v-slot:footer> <template #footer>
<button class="btn btn-save" <button
class="btn btn-save"
@click.prevent="saveNewLocation" @click.prevent="saveNewLocation"
> >
{{ $t('action.save') }} {{ trans(SAVE) }}
</button> </button>
</template> </template>
</modal> </modal>
</teleport> </teleport>
</div> </div>
</template> </template>
<script> <script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue'; import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue"; import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import { mapState } from "vuex"; import { mapState } from "vuex";
import { getLocationTypes } from "../../api"; import { getLocationTypes } from "../../api";
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods'; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import {
SAVE,
ACTIVITY_LOCATION_FIELDS_EMAIL,
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
ACTIVITY_LOCATION_FIELDS_NAME,
ACTIVITY_LOCATION_FIELDS_TYPE,
ACTIVITY_CHOOSE_LOCATION_TYPE,
ACTIVITY_CREATE_NEW_LOCATION,
trans,
} from "translator";
export default { export default {
name: "NewLocation", name: "NewLocation",
@@ -88,7 +144,20 @@ export default {
Modal, Modal,
AddAddress, AddAddress,
}, },
props: ['availableLocations'], setup() {
return {
trans,
SAVE,
ACTIVITY_LOCATION_FIELDS_EMAIL,
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
ACTIVITY_LOCATION_FIELDS_NAME,
ACTIVITY_LOCATION_FIELDS_TYPE,
ACTIVITY_CHOOSE_LOCATION_TYPE,
ACTIVITY_CREATE_NEW_LOCATION,
};
},
props: ["availableLocations"],
data() { data() {
return { return {
errors: [], errors: [],
@@ -103,35 +172,42 @@ export default {
locationTypes: [], locationTypes: [],
modal: { modal: {
showModal: false, showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl" modalDialogClass: "modal-dialog-scrollable modal-xl",
}, },
addAddress: { addAddress: {
options: { options: {
button: { button: {
text: { create: 'activity.create_address', edit: 'activity.edit_address' }, text: {
size: 'btn-sm' create: "activity.create_address",
edit: "activity.edit_address",
},
size: "btn-sm",
},
title: {
create: "activity.create_address",
edit: "activity.edit_address",
}, },
title: { create: 'activity.create_address', edit: 'activity.edit_address' },
}, },
context: { context: {
target: { //name, id target: {
}, //name, id
},
edit: false, edit: false,
addressId: null, addressId: null,
defaults: window.addaddress defaults: window.addaddress,
} },
} },
} };
}, },
computed: { computed: {
...mapState(['activity']), ...mapState(["activity"]),
selectType: { selectType: {
get() { get() {
return this.selected.type; return this.selected.type;
}, },
set(value) { set(value) {
this.selected.type = value; this.selected.type = value;
} },
}, },
inputName: { inputName: {
get() { get() {
@@ -139,7 +215,7 @@ export default {
}, },
set(value) { set(value) {
this.selected.name = value; this.selected.name = value;
} },
}, },
inputEmail: { inputEmail: {
get() { get() {
@@ -147,7 +223,7 @@ export default {
}, },
set(value) { set(value) {
this.selected.email = value; this.selected.email = value;
} },
}, },
inputPhonenumber1: { inputPhonenumber1: {
get() { get() {
@@ -155,7 +231,7 @@ export default {
}, },
set(value) { set(value) {
this.selected.phonenumber1 = value; this.selected.phonenumber1 = value;
} },
}, },
inputPhonenumber2: { inputPhonenumber2: {
get() { get() {
@@ -163,15 +239,18 @@ export default {
}, },
set(value) { set(value) {
this.selected.phonenumber2 = value; this.selected.phonenumber2 = value;
} },
}, },
hasPhonenumber1() { hasPhonenumber1() {
return this.selected.phonenumber1 !== null && this.selected.phonenumber1 !== ""; return (
this.selected.phonenumber1 !== null &&
this.selected.phonenumber1 !== ""
);
}, },
showAddAddress() { showAddAddress() {
let cond = false; let cond = false;
if (this.selected.type) { if (this.selected.type) {
if (this.selected.type.addressRequired !== 'never') { if (this.selected.type.addressRequired !== "never") {
cond = true; cond = true;
} }
} }
@@ -180,7 +259,7 @@ export default {
showContactData() { showContactData() {
let cond = false; let cond = false;
if (this.selected.type) { if (this.selected.type) {
if (this.selected.type.contactData !== 'never') { if (this.selected.type.contactData !== "never") {
cond = true; cond = true;
} }
} }
@@ -195,28 +274,39 @@ export default {
let cond = true; let cond = true;
this.errors = []; this.errors = [];
if (!this.selected.type) { if (!this.selected.type) {
this.errors.push('Type de localisation requis'); this.errors.push("Type de localisation requis");
cond = false; cond = false;
} else { } else {
if (this.selected.type.addressRequired === 'required' && !this.selected.addressId) { if (
this.errors.push('Adresse requise'); this.selected.type.addressRequired === "required" &&
!this.selected.addressId
) {
this.errors.push("Adresse requise");
cond = false; cond = false;
} }
if (this.selected.type.contactData === 'required' && !this.selected.phonenumber1) { if (
this.errors.push('Numéro de téléphone requis'); this.selected.type.contactData === "required" &&
!this.selected.phonenumber1
) {
this.errors.push("Numéro de téléphone requis");
cond = false; cond = false;
} }
if (this.selected.type.contactData === 'required' && !this.selected.email) { if (
this.errors.push('Adresse email requise'); this.selected.type.contactData === "required" &&
!this.selected.email
) {
this.errors.push("Adresse email requise");
cond = false; cond = false;
} }
} }
return cond; return cond;
}, },
getLocationTypesList() { getLocationTypesList() {
getLocationTypes().then(results => { getLocationTypes().then((results) => {
this.locationTypes = results.filter(t => t.availableForUsers === true); this.locationTypes = results.filter(
}) (t) => t.availableForUsers === true,
);
});
}, },
openModal() { openModal() {
this.modal.showModal = true; this.modal.showModal = true;
@@ -224,11 +314,11 @@ export default {
saveNewLocation() { saveNewLocation() {
if (this.checkForm()) { if (this.checkForm()) {
let body = { let body = {
type: 'location', type: "location",
name: this.selected.name, name: this.selected.name,
locationType: { locationType: {
id: this.selected.type.id, id: this.selected.type.id,
type: 'location-type' type: "location-type",
}, },
phonenumber1: this.selected.phonenumber1, phonenumber1: this.selected.phonenumber1,
phonenumber2: this.selected.phonenumber2, phonenumber2: this.selected.phonenumber2,
@@ -237,36 +327,36 @@ export default {
if (this.selected.addressId) { if (this.selected.addressId) {
body = Object.assign(body, { body = Object.assign(body, {
address: { address: {
id: this.selected.addressId id: this.selected.addressId,
} },
}); });
} }
makeFetch('POST', '/api/1.0/main/location.json', body) makeFetch("POST", "/api/1.0/main/location.json", body)
.then(response => { .then((response) => {
this.$store.dispatch('addAvailableLocationGroup', { this.$store.dispatch("addAvailableLocationGroup", {
locationGroup: 'Localisations nouvellement créées', locationGroup: "Localisations nouvellement créées",
locations: [response] locations: [response],
}); });
this.$store.dispatch('updateLocation', response); this.$store.dispatch("updateLocation", response);
this.modal.showModal = false; this.modal.showModal = false;
}) })
.catch((error) => { .catch((error) => {
if (error.name === 'ValidationException') { if (error.name === "ValidationException") {
for (let v of error.violations) { for (let v of error.violations) {
this.errors.push(v); this.errors.push(v);
} }
} else { } else {
this.errors.push('An error occurred'); this.errors.push("An error occurred");
} }
}) });
}; }
}, },
submitNewAddress(payload) { submitNewAddress(payload) {
this.selected.addressId = payload.addressId; this.selected.addressId = payload.addressId;
this.addAddress.context.addressId = payload.addressId; this.addAddress.context.addressId = payload.addressId;
this.addAddress.context.edit = true; this.addAddress.context.edit = true;
} },
} },
} };
</script> </script>

View File

@@ -1,228 +1,280 @@
<template> <template>
<teleport to="#social-issues-acc"> <teleport to="#social-issues-acc">
<div class="mb-3 row">
<div class="mb-3 row"> <div class="col-4">
<div class="col-4"> <label :class="socialIssuesClassList">{{
<label :class="socialIssuesClassList">{{ $t('activity.social_issues') }}</label> trans(ACTIVITY_SOCIAL_ISSUES)
</div> }}</label>
<div class="col-8">
<check-social-issue
v-for="issue in socialIssuesList"
:key="issue.id"
:issue="issue"
:selection="socialIssuesSelected"
@updateSelected="updateIssuesSelected">
</check-social-issue>
<div class="my-3">
<VueMultiselect
name="otherIssues"
label="text"
track-by="id"
open-direction="bottom"
:close-on-select="true"
:preserve-search="false"
:reset-after="true"
:hide-selected="true"
:taggable="false"
:multiple="false"
:searchable="true"
:allow-empty="true"
:show-labels="false"
:loading="issueIsLoading"
:placeholder="$t('activity.choose_other_social_issue')"
:options="socialIssuesOther"
@select="addIssueInList">
</VueMultiselect>
</div> </div>
<div class="col-8">
<check-social-issue
v-for="issue in socialIssuesList"
:key="issue.id"
:issue="issue"
:selection="socialIssuesSelected"
@updateSelected="updateIssuesSelected"
>
</check-social-issue>
</div> <div class="my-3">
</div> <VueMultiselect
name="otherIssues"
<div class="mb-3 row"> label="text"
<div class="col-4"> track-by="id"
<label :class="socialActionsClassList">{{ $t('activity.social_actions') }}</label> open-direction="bottom"
</div> :close-on-select="true"
<div class="col-8"> :preserve-search="false"
:reset-after="true"
<div v-if="actionIsLoading === true"> :hide-selected="true"
<i class="chill-green fa fa-circle-o-notch fa-spin fa-lg"></i> :taggable="false"
:multiple="false"
:searchable="true"
:allow-empty="true"
:show-labels="false"
:loading="issueIsLoading"
:placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)"
:options="socialIssuesOther"
@select="addIssueInList"
>
</VueMultiselect>
</div>
</div> </div>
</div>
<span v-else-if="socialIssuesSelected.length === 0" class="inline-choice chill-no-data-statement mt-3"> <div class="mb-3 row">
{{ $t('activity.select_first_a_social_issue') }} <div class="col-4">
</span> <label :class="socialActionsClassList">{{
trans(ACTIVITY_SOCIAL_ACTIONS)
}}</label>
</div>
<div class="col-8">
<div v-if="actionIsLoading === true">
<i
class="chill-green fa fa-circle-o-notch fa-spin fa-lg"
></i>
</div>
<template v-else-if="socialActionsList.length > 0"> <span
<check-social-action v-else-if="socialIssuesSelected.length === 0"
v-if="socialIssuesSelected.length || socialActionsSelected.length" class="inline-choice chill-no-data-statement mt-3"
v-for="action in socialActionsList" >
:key="action.id" {{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }}
:action="action" </span>
:selection="socialActionsSelected"
@updateSelected="updateActionsSelected">
</check-social-action>
</template>
<span v-else-if="actionAreLoaded && socialActionsList.length === 0" class="inline-choice chill-no-data-statement mt-3"> <template
{{ $t('activity.social_action_list_empty') }} v-else-if="
</span> socialActionsList.length > 0 &&
(socialIssuesSelected.length ||
socialActionsSelected.length)
"
>
<div
id="actionsList"
v-for="group in socialActionsList"
:key="group.issue"
>
<span class="badge bg-chill-l-gray text-dark">{{
group.issue
}}</span>
<check-social-action
v-for="action in group.actions"
:key="action.id"
:action="action"
:selection="socialActionsSelected"
@updateSelected="updateActionsSelected"
>
</check-social-action>
</div>
</template>
<span
</div> v-else-if="
</div> actionAreLoaded && socialActionsList.length === 0
"
</teleport> class="inline-choice chill-no-data-statement mt-3"
>
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }}
</span>
</div>
</div>
</teleport>
</template> </template>
<script> <script>
import VueMultiselect from 'vue-multiselect'; import VueMultiselect from "vue-multiselect";
import CheckSocialIssue from './SocialIssuesAcc/CheckSocialIssue.vue'; import CheckSocialIssue from "./SocialIssuesAcc/CheckSocialIssue.vue";
import CheckSocialAction from './SocialIssuesAcc/CheckSocialAction.vue'; import CheckSocialAction from "./SocialIssuesAcc/CheckSocialAction.vue";
import { getSocialIssues, getSocialActionByIssue } from '../api.js'; import { getSocialIssues, getSocialActionByIssue } from "../api.js";
import {
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
ACTIVITY_SOCIAL_ACTIONS,
ACTIVITY_SOCIAL_ISSUES,
ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE,
trans,
} from "translator";
export default { export default {
name: "SocialIssuesAcc", name: "SocialIssuesAcc",
components: { components: {
CheckSocialIssue, CheckSocialIssue,
CheckSocialAction, CheckSocialAction,
VueMultiselect VueMultiselect,
}, },
data() { setup() {
return { return {
issueIsLoading: false, trans,
actionIsLoading: false, ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
actionAreLoaded: false, ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
socialIssuesClassList: ACTIVITY_SOCIAL_ACTIONS,
`col-form-label ${document.querySelector('input#chill_activitybundle_activity_socialIssues').getAttribute('required') ? 'required' : ''}`, ACTIVITY_SOCIAL_ISSUES,
socialActionsClassList: ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE,
`col-form-label ${document.querySelector('input#chill_activitybundle_activity_socialActions').getAttribute('required') ? 'required' : ''}`, };
} },
}, data() {
computed: { return {
socialIssuesList() { issueIsLoading: false,
return this.$store.state.activity.accompanyingPeriod.socialIssues; actionIsLoading: false,
}, actionAreLoaded: false,
socialIssuesSelected() { socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`,
return this.$store.state.activity.socialIssues; socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`,
}, };
socialIssuesOther() { },
return this.$store.state.socialIssuesOther; computed: {
}, socialIssuesList() {
socialActionsList() { return this.$store.state.activity.accompanyingPeriod.socialIssues;
return this.$store.getters.socialActionsListSorted; },
}, socialIssuesSelected() {
socialActionsSelected() { return this.$store.state.activity.socialIssues;
return this.$store.state.activity.socialActions; },
} socialIssuesOther() {
}, return this.$store.state.socialIssuesOther;
mounted() { },
/* Load others issues in multiselect socialActionsList() {
*/ return this.$store.getters.socialActionsListSorted;
this.issueIsLoading = true; },
this.actionAreLoaded = false; socialActionsSelected() {
getSocialIssues().then(response => new Promise((resolve, reject) => { return this.$store.state.activity.socialActions;
this.$store.commit('updateIssuesOther', response.results); },
},
mounted() {
/* Load other issues in multiselect */
this.issueIsLoading = true;
this.actionAreLoaded = false;
/* Add in list the issues already associated (if not yet listed) getSocialIssues().then((response) => {
*/ /* Add issues to the store */
this.socialIssuesSelected.forEach(issue => { this.$store.commit("updateIssuesOther", response);
if (this.socialIssuesList.filter(i => i.id === issue.id).length !== 1) {
this.$store.commit('addIssueInList', issue);
}
}, this);
/* Remove from multiselect the issues that are not yet in checkbox list /* Add in list the issues already associated (if not yet listed) */
*/ this.socialIssuesSelected.forEach((issue) => {
this.socialIssuesList.forEach(issue => { if (
this.$store.commit('removeIssueInOther', issue); this.socialIssuesList.filter((i) => i.id === issue.id)
}, this); .length !== 1
) {
this.$store.commit("addIssueInList", issue);
}
});
/* Filter issues /* Remove from multiselect the issues that are not yet in the checkbox list */
*/ this.socialIssuesList.forEach((issue) => {
this.$store.commit('filterList', 'issues'); this.$store.commit("removeIssueInOther", issue);
});
/* Add in list the actions already associated (if not yet listed) /* Filter issues */
*/ this.$store.commit("filterList", "issues");
this.socialActionsSelected.forEach(action => {
this.$store.commit('addActionInList', action);
}, this);
/* Filter issues /* Add in list the actions already associated (if not yet listed) */
*/ this.socialActionsSelected.forEach((action) => {
this.$store.commit('filterList', 'actions'); this.$store.commit("addActionInList", action);
});
this.issueIsLoading = false; /* Filter actions */
this.actionAreLoaded = true; this.$store.commit("filterList", "actions");
this.updateActionsList();
resolve();
}));
},
methods: {
/* When choosing an issue in multiselect, add it in checkboxes (as selected), this.issueIsLoading = false;
this.actionAreLoaded = true;
this.updateActionsList();
});
},
methods: {
/* When choosing an issue in multiselect, add it in checkboxes (as selected),
remove it from multiselect, and add socialActions concerned remove it from multiselect, and add socialActions concerned
*/ */
addIssueInList(value) { addIssueInList(value) {
//console.log('addIssueInList', value); //console.log('addIssueInList', value);
this.$store.commit('addIssueInList', value); this.$store.commit("addIssueInList", value);
this.$store.commit('removeIssueInOther', value); this.$store.commit("removeIssueInOther", value);
this.$store.dispatch('addIssueSelected', value); this.$store.dispatch("addIssueSelected", value);
this.updateActionsList(); this.updateActionsList();
}, },
/* Update value for selected issues checkboxes /* Update value for selected issues checkboxes
*/ */
updateIssuesSelected(issues) { updateIssuesSelected(issues) {
//console.log('updateIssuesSelected', issues); //console.log('updateIssuesSelected', issues);
this.$store.dispatch('updateIssuesSelected', issues); this.$store.dispatch("updateIssuesSelected", issues);
this.updateActionsList(); this.updateActionsList();
}, },
/* Update value for selected actions checkboxes /* Update value for selected actions checkboxes
*/ */
updateActionsSelected(actions) { updateActionsSelected(actions) {
//console.log('updateActionsSelected', actions); //console.log('updateActionsSelected', actions);
this.$store.dispatch('updateActionsSelected', actions); this.$store.dispatch("updateActionsSelected", actions);
}, },
/* Add socialActions concerned: after reset, loop on each issue selected /* Add socialActions concerned: after reset, loop on each issue selected
to get social actions concerned to get social actions concerned
*/ */
updateActionsList() { updateActionsList() {
this.resetActionsList(); this.resetActionsList();
this.socialIssuesSelected.forEach(item => { this.socialIssuesSelected.forEach((item) => {
this.actionIsLoading = true;
getSocialActionByIssue(item.id).then(
(actions) =>
new Promise((resolve) => {
actions.results.forEach((action) => {
this.$store.commit("addActionInList", action);
}, this);
this.actionIsLoading = true; this.$store.commit("filterList", "actions");
getSocialActionByIssue(item.id)
.then(actions => new Promise((resolve, reject) => {
actions.results.forEach(action => { this.actionIsLoading = false;
this.$store.commit('addActionInList', action); this.actionAreLoaded = true;
}, this); resolve();
}),
this.$store.commit('filterList', 'actions'); );
}, this);
this.actionIsLoading = false; },
this.actionAreLoaded = true; /* Reset socialActions List: flush list and restore selected actions
resolve(); */
})); resetActionsList() {
}, this); this.$store.commit("resetActionsList");
}, this.actionAreLoaded = false;
/* Reset socialActions List: flush list and restore selected actions this.socialActionsSelected.forEach((item) => {
*/ this.$store.commit("addActionInList", item);
resetActionsList() { }, this);
this.$store.commit('resetActionsList'); },
this.actionAreLoaded = false; },
this.socialActionsSelected.forEach(item => { };
this.$store.commit('addActionInList', item);
}, this);
}
}
}
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss" scoped> <style lang="scss" scoped>
span.multiselect__single { @import "ChillMainAssets/module/bootstrap/shared";
display: none !important; @import "ChillPersonAssets/chill/scss/mixins";
} @import "ChillMainAssets/chill/scss/chill_variables";
span.multiselect__single {
display: none !important;
}
#actionsList {
border-radius: 0.5rem;
padding: 1rem;
margin: 0.5rem;
background-color: whitesmoke;
}
span.badge {
margin-bottom: 0.5rem;
@include badge_social($social-issue-color);
}
</style> </style>

View File

@@ -1,48 +1,53 @@
<template> <template>
<span class="inline-choice"> <span class="inline-choice">
<div class="form-check"> <div class="form-check">
<input
<input class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
v-model="selected" v-model="selected"
name="action" name="action"
v-bind:id="action.id" :id="action.id"
v-bind:value="action" :value="action"
/> />
<label class="form-check-label" v-bind:for="action.id"> <label class="form-check-label" :for="action.id">
<span class="badge bg-light text-dark">{{ action.text }}</span> <span class="badge bg-light text-dark" :title="action.text">{{
</label> action.text
}}</span>
</div> </label>
</span> </div>
</span>
</template> </template>
<script> <script>
export default { export default {
name: "CheckSocialAction", name: "CheckSocialAction",
props: [ 'action', 'selection' ], props: ["action", "selection"],
emits: [ 'updateSelected' ], emits: ["updateSelected"],
computed: { computed: {
selected: { selected: {
set(value) { set(value) {
this.$emit('updateSelected', value); this.$emit("updateSelected", value);
}, },
get() { get() {
return this.selection; return this.selection;
} },
} },
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import 'ChillMainAssets/module/bootstrap/shared'; @import "ChillMainAssets/module/bootstrap/shared";
@import 'ChillPersonAssets/chill/scss/mixins'; @import "ChillPersonAssets/chill/scss/mixins";
@import 'ChillMainAssets/chill/scss/chill_variables'; @import "ChillMainAssets/chill/scss/chill_variables";
span.badge { span.badge {
@include badge_social($social-action-color); @include badge_social($social-action-color);
font-size: 95%; font-size: 95%;
margin-bottom: 5px; margin-bottom: 5px;
margin-right: 1em; margin-right: 1em;
max-width: 100%; /* Adjust as needed */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
</style> </style>

View File

@@ -1,44 +1,45 @@
<template> <template>
<span class="inline-choice"> <span class="inline-choice">
<div class="form-check"> <div class="form-check">
<input
<input class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
v-model="selected" v-model="selected"
name="issue" name="issue"
v-bind:id="issue.id" :id="issue.id"
v-bind:value="issue" :value="issue"
/> />
<label class="form-check-label" v-bind:for="issue.id"> <label class="form-check-label" :for="issue.id">
<span class="badge bg-chill-l-gray text-dark">{{ issue.text }}</span> <span class="badge bg-chill-l-gray text-dark">{{
</label> issue.text
}}</span>
</div> </label>
</span> </div>
</span>
</template> </template>
<script> <script>
export default { export default {
name: "CheckSocialIssue", name: "CheckSocialIssue",
props: [ 'issue', 'selection' ], props: ["issue", "selection"],
emits: [ 'updateSelected' ], emits: ["updateSelected"],
computed: { computed: {
selected: { selected: {
set(value) { set(value) {
this.$emit('updateSelected', value); this.$emit("updateSelected", value);
}, },
get() { get() {
return this.selection; return this.selection;
} },
} },
} },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import 'ChillMainAssets/module/bootstrap/shared'; @import "ChillMainAssets/module/bootstrap/shared";
@import 'ChillPersonAssets/chill/scss/mixins'; @import "ChillPersonAssets/chill/scss/mixins";
@import 'ChillMainAssets/chill/scss/chill_variables'; @import "ChillMainAssets/chill/scss/chill_variables";
span.badge { span.badge {
@include badge_social($social-issue-color); @include badge_social($social-issue-color);
font-size: 95%; font-size: 95%;

View File

@@ -1,45 +1,44 @@
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n' import { personMessages } from "ChillPersonAssets/vuejs/_js/i18n";
import { multiSelectMessages } from 'ChillMainAssets/vuejs/_js/i18n' import { multiSelectMessages } from "ChillMainAssets/vuejs/_js/i18n";
const activityMessages = { const activityMessages = {
fr: { fr: {
activity: { activity: {
// //
errors: "Le formulaire contient des erreurs", errors: "Le formulaire contient des erreurs",
social_issues: "Problématiques sociales", social_issues: "Problématiques sociales",
choose_other_social_issue: "Ajouter une autre problématique sociale...", choose_other_social_issue: "Ajouter une autre problématique sociale...",
social_actions: "Actions d'accompagnement", social_actions: "Actions d'accompagnement",
select_first_a_social_issue: "Sélectionnez d'abord une problématique sociale", select_first_a_social_issue:
social_action_list_empty: "Aucune action sociale disponible", "Sélectionnez d'abord une problématique sociale",
social_action_list_empty: "Aucune action sociale disponible",
// //
add_persons: "Ajouter des personnes concernées", add_persons: "Ajouter des personnes concernées",
bloc_persons: "Usagers", bloc_persons: "Usagers",
bloc_persons_associated: "Usagers du parcours", bloc_persons_associated: "Usagers du parcours",
bloc_persons_not_associated: "Tiers non-pro.", bloc_persons_not_associated: "Tiers non-pro.",
bloc_thirdparty: "Tiers professionnels", bloc_thirdparty: "Tiers professionnels",
bloc_users: "T(M)S", bloc_users: "T(M)S",
// //
location: "Localisation", location: "Localisation",
choose_location: "Choisissez une localisation", choose_location: "Choisissez une localisation",
choose_location_type: "Choisissez un type de localisation", choose_location_type: "Choisissez un type de localisation",
create_new_location: "Créer une nouvelle localisation", create_new_location: "Créer une nouvelle localisation",
location_fields: { location_fields: {
name: "Nom", name: "Nom",
type: "Type", type: "Type",
phonenumber1: "Téléphone", phonenumber1: "Téléphone",
phonenumber2: "Autre téléphone", phonenumber2: "Autre téléphone",
email: "Adresse courriel", email: "Adresse courriel",
}, },
create_address: 'Créer une adresse', create_address: "Créer une adresse",
edit_address: "Modifier l'adresse" edit_address: "Modifier l'adresse",
} },
} },
} };
Object.assign(activityMessages.fr, personMessages.fr, multiSelectMessages.fr); Object.assign(activityMessages.fr, personMessages.fr, multiSelectMessages.fr);
export { export { activityMessages };
activityMessages
};

View File

@@ -1,86 +1,89 @@
import { createApp } from 'vue'; import { createApp } from "vue";
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n' import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import { activityMessages } from './i18n' import { activityMessages } from "./i18n";
import store from './store' import store from "./store";
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue'; import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
import {fetchTemplates} from 'ChillDocGeneratorAssets/api/pickTemplate.js'; import { fetchTemplates } from "ChillDocGeneratorAssets/api/pickTemplate.js";
import App from './App.vue'; import App from "./App.vue";
const i18n = _createI18n(activityMessages); const i18n = _createI18n(activityMessages);
// app for activity // app for activity
const hasSocialIssues = document.querySelector('#social-issues-acc') !== null; const hasSocialIssues = document.querySelector("#social-issues-acc") !== null;
const hasLocation = document.querySelector('#location') !== null; const hasLocation = document.querySelector("#location") !== null;
const hasPerson = document.querySelector('#add-persons') !== null; const hasPerson = document.querySelector("#add-persons") !== null;
const isSimpleEditor = true;
const app = createApp({ const app = createApp({
template: `<app template: `<app
:hasSocialIssues="hasSocialIssues" :hasSocialIssues="hasSocialIssues"
:hasLocation="hasLocation" :hasLocation="hasLocation"
:hasPerson="hasPerson" :hasPerson="hasPerson"
:isSimpleEditor = "isSimpleEditor"
></app>`, ></app>`,
data() { data() {
return { return {
hasSocialIssues, hasSocialIssues,
hasLocation, hasLocation,
hasPerson, hasPerson,
}; isSimpleEditor
} };
},
}) })
.use(store) .use(store)
.use(i18n) .use(i18n)
.component('app', App) .component("app", App)
.mount('#activity'); .mount("#activity");
// app for picking template // app for picking template
const i18nGendoc = _createI18n({}); const i18nGendoc = _createI18n({});
document.querySelectorAll('div[data-docgen-template-picker]').forEach(el => { document.querySelectorAll("div[data-docgen-template-picker]").forEach((el) => {
fetchTemplates(el.dataset.entityClass).then(templates => { fetchTemplates(el.dataset.entityClass).then((templates) => {
const picker = { const picker = {
template: template:
'<pick-template :templates="this.templates" :preventDefaultMoveToGenerate="true" ' + '<pick-template :templates="this.templates" :preventDefaultMoveToGenerate="true" ' +
':entityClass="faked" @go-to-generate-document="generateDoc"></pick-template>', ':entityClass="faked" @go-to-generate-document="generateDoc"></pick-template>',
components: { components: {
PickTemplate, PickTemplate,
}, },
data() { data() {
return { return {
templates: templates, templates: templates,
entityId: el.dataset.entityId, entityId: el.dataset.entityId,
}
},
methods: {
generateDoc({event, link, template}) {
console.log('generateDoc');
console.log('link', link);
console.log('template', template);
let hiddenInput = document.querySelector("input[data-template-id]");
if (hiddenInput === null) {
console.error('hidden input not found');
return;
}
hiddenInput.value = template;
let form = document.querySelector('form[name="chill_activitybundle_activity"');
if (form === null) {
console.error('form not found');
return;
}
form.submit();
}
}
}; };
createApp(picker).use(i18nGendoc).mount(el); },
}) methods: {
generateDoc({ event, link, template }) {
console.log("generateDoc");
console.log("link", link);
console.log("template", template);
let hiddenInput = document.querySelector("input[data-template-id]");
if (hiddenInput === null) {
console.error("hidden input not found");
return;
}
hiddenInput.value = template;
let form = document.querySelector(
'form[name="chill_activitybundle_activity"',
);
if (form === null) {
console.error("form not found");
return;
}
form.submit();
},
},
};
createApp(picker).use(i18nGendoc).mount(el);
});
}); });

View File

@@ -1,350 +1,389 @@
import 'es6-promise/auto'; import "es6-promise/auto";
import { createStore } from 'vuex'; import { createStore } from "vuex";
import { postLocation } from './api'; import { postLocation } from "./api";
import prepareLocations from './store.locations.js'; import prepareLocations from "./store.locations.js";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== "production";
//console.log('window.activity', window.activity); //console.log('window.activity', window.activity);
const addIdToValue = (string, id) => { const addIdToValue = (string, id) => {
let array = string ? string.split(',') : []; let array = string ? string.split(",") : [];
array.push(id.toString()); array.push(id.toString());
let str = array.join(); let str = array.join();
return str; return str;
}; };
const removeIdFromValue = (string, id) => { const removeIdFromValue = (string, id) => {
let array = string.split(','); let array = string.split(",");
array = array.filter(el => el !== id.toString()); array = array.filter((el) => el !== id.toString());
let str = array.join(); let str = array.join();
return str; return str;
}; };
const store = createStore({ const store = createStore({
strict: debug, strict: debug,
state: { state: {
activity: window.activity, me: null,
socialIssuesOther: [], activity: window.activity,
socialActionsList: [], accompanyingPeriodWorks: [],
availableLocations: [], socialIssuesOther: [],
socialActionsList: [],
availableLocations: [],
},
getters: {
suggestedEntities(state) {
if (
typeof state.activity.accompanyingPeriod === "undefined" ||
state.activity.accompanyingPeriod === null
) {
return [];
}
const allEntities = [
...store.getters.suggestedPersons,
...store.getters.suggestedRequestor,
...store.getters.suggestedUsers,
...store.getters.suggestedResources,
];
const uniqueIds = [
...new Set(allEntities.map((i) => `${i.type}-${i.id}`)),
];
return Array.from(
uniqueIds,
(id) => allEntities.filter((r) => `${r.type}-${r.id}` === id)[0],
);
}, },
getters: { suggestedPersons(state) {
suggestedEntities(state) { const existingPersonIds = state.activity.persons.map((p) => p.id);
if (typeof state.activity.accompanyingPeriod === "undefined" || state.activity.accompanyingPeriod === null) { return state.activity.activityType.personsVisible === 0
return []; ? []
} : state.activity.accompanyingPeriod.participations
const allEntities = [ .filter((p) => p.endDate === null)
...store.getters.suggestedPersons, .map((p) => p.person)
...store.getters.suggestedRequestor, .filter((p) => !existingPersonIds.includes(p.id));
...store.getters.suggestedUser, },
...store.getters.suggestedResources, suggestedRequestor(state) {
]; if (state.activity.accompanyingPeriod.requestor === null) {
const uniqueIds = [ return [];
...new Set(allEntities.map((i) => `${i.type}-${i.id}`)), }
]; const existingPersonIds = state.activity.persons.map((p) => p.id);
return Array.from( const existingThirdPartyIds = state.activity.thirdParties.map(
uniqueIds, (p) => p.id,
(id) => allEntities.filter((r) => `${r.type}-${r.id}` === id)[0] );
);
}, return [state.activity.accompanyingPeriod.requestor].filter(
suggestedPersons(state) { (r) =>
const existingPersonIds = state.activity.persons.map((p) => p.id); (r.type === "person" &&
return state.activity.activityType.personsVisible === 0 !existingPersonIds.includes(r.id) &&
? [] state.activity.activityType.personsVisible !== 0) ||
: state.activity.accompanyingPeriod.participations (r.type === "thirdparty" &&
.filter((p) => p.endDate === null) !existingThirdPartyIds.includes(r.id) &&
.map((p) => p.person) state.activity.activityType.thirdPartiesVisible !== 0),
.filter((p) => !existingPersonIds.includes(p.id)); );
}, },
suggestedRequestor(state) { suggestedUsers(state) {
if (state.activity.accompanyingPeriod.requestor === null) { const existingUserIds = state.activity.users.map((p) => p.id);
return []; let suggestedUsers =
} state.activity.activityType.usersVisible === 0
const existingPersonIds = state.activity.persons.map((p) => p.id); ? []
const existingThirdPartyIds = state.activity.thirdParties.map( : [state.activity.accompanyingPeriod.user].filter(
(p) => p.id (u) => u !== null && !existingUserIds.includes(u.id),
); );
return [state.activity.accompanyingPeriod.requestor].filter( state.accompanyingPeriodWorks.forEach((work) => {
(r) => work.referrers.forEach((r) => {
(r.type === "person" && if (!existingUserIds.includes(r.id)) {
!existingPersonIds.includes(r.id) && suggestedUsers.push(r);
state.activity.activityType.personsVisible !== 0) || }
(r.type === "thirdparty" && });
!existingThirdPartyIds.includes(r.id) && });
state.activity.activityType.thirdPartiesVisible !== 0) // Add the current user from the state
); if (state.me && !existingUserIds.includes(state.me.id)) {
}, suggestedUsers.push(state.me);
suggestedUser(state) { }
const existingUserIds = state.activity.users.map((p) => p.id); // console.log("suggested users", suggestedUsers);
return state.activity.activityType.usersVisible === 0
? []
: [state.activity.accompanyingPeriod.user].filter(
(u) => u !== null && !existingUserIds.includes(u.id)
);
},
suggestedResources(state) {
const resources = state.activity.accompanyingPeriod.resources;
const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(
(p) => p.id
);
return state.activity.accompanyingPeriod.resources
.map((r) => r.resource)
.filter(
(r) =>
(r.type === "person" &&
!existingPersonIds.includes(r.id) &&
state.activity.activityType.personsVisible !== 0) ||
(r.type === "thirdparty" &&
!existingThirdPartyIds.includes(r.id) &&
state.activity.activityType.thirdPartiesVisible !== 0)
);
},
socialActionsListSorted(state) {
return [ ...state.socialActionsList].sort((a, b) => a.ordering - b.ordering);
},
},
mutations: {
// SocialIssueAcc
addIssueInList(state, issue) {
//console.log('add issue list', issue.id);
state.activity.accompanyingPeriod.socialIssues.push(issue);
},
addIssueSelected(state, issue) {
//console.log('add issue selected', issue.id);
state.activity.socialIssues.push(issue);
},
updateIssuesSelected(state, issues) {
//console.log('update issues selected', issues);
state.activity.socialIssues = issues;
},
updateIssuesOther(state, payload) {
//console.log('update issues other');
state.socialIssuesOther = payload;
},
removeIssueInOther(state, issue) {
//console.log('remove issue other', issue.id);
state.socialIssuesOther = state.socialIssuesOther.filter(
(i) => i.id !== issue.id
);
},
resetActionsList(state) {
//console.log('reset list actions');
state.socialActionsList = [];
},
addActionInList(state, action) {
state.socialActionsList.push(action);
},
updateActionsSelected(state, actions) {
//console.log('update actions selected', actions);
state.activity.socialActions = actions;
},
filterList(state, list) {
const filterList = (list) => {
// remove duplicates entries
list = list.filter(
(value, index) =>
list.findIndex((array) => array.id === value.id) ===
index
);
// alpha sort
list.sort((a, b) =>
a.text > b.text ? 1 : b.text > a.text ? -1 : 0
);
return list;
};
if (list === "issues") {
state.activity.accompanyingPeriod.socialIssues = filterList(
state.activity.accompanyingPeriod.socialIssues
);
}
if (list === "actions") {
state.socialActionsList = filterList(state.socialActionsList);
}
},
// ConcernedGroups return suggestedUsers;
addPersonsInvolved(state, payload) { },
console.log("### mutation addPersonsInvolved", payload); suggestedResources(state) {
switch (payload.result.type) { // const resources = state.activity.accompanyingPeriod.resources;
case "person": const existingPersonIds = state.activity.persons.map((p) => p.id);
state.activity.persons.push(payload.result); const existingThirdPartyIds = state.activity.thirdParties.map(
break; (p) => p.id,
case "thirdparty": );
state.activity.thirdParties.push(payload.result); return state.activity.accompanyingPeriod.resources
break; .map((r) => r.resource)
case "user": .filter(
state.activity.users.push(payload.result); (r) =>
break; (r.type === "person" &&
} !existingPersonIds.includes(r.id) &&
}, state.activity.activityType.personsVisible !== 0) ||
removePersonInvolved(state, payload) { (r.type === "thirdparty" &&
//console.log('### mutation removePersonInvolved', payload.type); !existingThirdPartyIds.includes(r.id) &&
switch (payload.type) { state.activity.activityType.thirdPartiesVisible !== 0),
case "person": );
state.activity.persons = state.activity.persons.filter( },
(person) => person !== payload socialActionsListSorted(state) {
); return [...state.socialActionsList]
break; .sort((a, b) => a.ordering - b.ordering)
case "thirdparty": .reduce((acc, action) => {
state.activity.thirdParties = const issueText = action.issue?.text || "Uncategorized";
state.activity.thirdParties.filter( // Find if the group for the issue already exists
(thirdparty) => thirdparty !== payload let group = acc.find((item) => item.issue === issueText);
); if (!group) {
break; group = { issue: issueText, actions: [] };
case "user": acc.push(group);
state.activity.users = state.activity.users.filter( }
(user) => user !== payload group.actions.push(action);
); return acc;
break; }, []);
} },
}, },
updateLocation(state, value) { mutations: {
console.log("### mutation: updateLocation", value); setWhoAmI(state, me) {
state.activity.location = value; state.me = me;
}, },
addAvailableLocationGroup(state, group) { // SocialIssueAcc
state.availableLocations.push(group); addIssueInList(state, issue) {
//console.log('add issue list', issue.id);
state.activity.accompanyingPeriod.socialIssues.push(issue);
},
addIssueSelected(state, issue) {
//console.log('add issue selected', issue.id);
state.activity.socialIssues.push(issue);
},
updateIssuesSelected(state, issues) {
//console.log('update issues selected', issues);
state.activity.socialIssues = issues;
},
updateIssuesOther(state, payload) {
//console.log('update issues other');
state.socialIssuesOther = payload;
},
removeIssueInOther(state, issue) {
//console.log('remove issue other', issue.id);
state.socialIssuesOther = state.socialIssuesOther.filter(
(i) => i.id !== issue.id,
);
},
resetActionsList(state) {
//console.log('reset list actions');
state.socialActionsList = [];
},
addActionInList(state, action) {
state.socialActionsList.push(action);
},
updateActionsSelected(state, actions) {
//console.log('update actions selected', actions);
state.activity.socialActions = actions;
},
filterList(state, list) {
const filterList = (list) => {
// remove duplicates entries
list = list.filter(
(value, index) =>
list.findIndex((array) => array.id === value.id) === index,
);
// alpha sort
list.sort((a, b) => (a.text > b.text ? 1 : b.text > a.text ? -1 : 0));
return list;
};
if (list === "issues") {
state.activity.accompanyingPeriod.socialIssues = filterList(
state.activity.accompanyingPeriod.socialIssues,
);
}
if (list === "actions") {
state.socialActionsList = filterList(state.socialActionsList);
}
},
// ConcernedGroups
addPersonsInvolved(state, payload) {
console.log("### mutation addPersonsInvolved", payload);
switch (payload.result.type) {
case "person":
state.activity.persons.push(payload.result);
break;
case "thirdparty":
state.activity.thirdParties.push(payload.result);
break;
case "user":
state.activity.users.push(payload.result);
break;
}
},
removePersonInvolved(state, payload) {
//console.log('### mutation removePersonInvolved', payload.type);
switch (payload.type) {
case "person":
state.activity.persons = state.activity.persons.filter(
(person) => person !== payload,
);
break;
case "thirdparty":
state.activity.thirdParties = state.activity.thirdParties.filter(
(thirdparty) => thirdparty !== payload,
);
break;
case "user":
state.activity.users = state.activity.users.filter(
(user) => user !== payload,
);
break;
}
},
updateLocation(state, value) {
console.log("### mutation: updateLocation", value);
state.activity.location = value;
},
addAvailableLocationGroup(state, group) {
state.availableLocations.push(group);
},
setAccompanyingPeriodWorks(state, works) {
state.accompanyingPeriodWorks = works;
},
},
actions: {
addIssueSelected({ commit }, issue) {
let aSocialIssues = document.getElementById(
"chill_activitybundle_activity_socialIssues",
);
aSocialIssues.value = addIdToValue(aSocialIssues.value, issue.id);
commit("addIssueSelected", issue);
},
updateIssuesSelected({ commit }, payload) {
let aSocialIssues = document.getElementById(
"chill_activitybundle_activity_socialIssues",
);
aSocialIssues.value = "";
payload.forEach((item) => {
aSocialIssues.value = addIdToValue(aSocialIssues.value, item.id);
});
commit("updateIssuesSelected", payload);
},
updateActionsSelected({ commit }, payload) {
let aSocialActions = document.getElementById(
"chill_activitybundle_activity_socialActions",
);
aSocialActions.value = "";
payload.forEach((item) => {
aSocialActions.value = addIdToValue(aSocialActions.value, item.id);
});
commit("updateActionsSelected", payload);
},
addAvailableLocationGroup({ commit }, payload) {
commit("addAvailableLocationGroup", payload);
},
addPersonsInvolved({ commit }, payload) {
//console.log('### action addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons",
);
aPersons.value = addIdToValue(aPersons.value, payload.result.id);
break;
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_thirdParties",
);
aThirdParties.value = addIdToValue(
aThirdParties.value,
payload.result.id,
);
break;
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users",
);
aUsers.value = addIdToValue(aUsers.value, payload.result.id);
break;
}
commit("addPersonsInvolved", payload);
},
removePersonInvolved({ commit }, payload) {
//console.log('### action removePersonInvolved', payload);
switch (payload.type) {
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons",
);
aPersons.value = removeIdFromValue(aPersons.value, payload.id);
break;
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_thirdParties",
);
aThirdParties.value = removeIdFromValue(
aThirdParties.value,
payload.id,
);
break;
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users",
);
aUsers.value = removeIdFromValue(aUsers.value, payload.id);
break;
}
commit("removePersonInvolved", payload);
},
updateLocation({ commit }, value) {
console.log("### action: updateLocation", value);
let hiddenLocation = document.getElementById(
"chill_activitybundle_activity_location",
);
if (value.onthefly) {
const body = {
type: "location",
name:
value.name === "__AccompanyingCourseLocation__" ? null : value.name,
locationType: {
id: value.locationType.id,
type: "location-type",
},
};
if (value.address.id) {
Object.assign(body, {
address: {
id: value.address.id,
},
});
} }
postLocation(body)
.then((location) => (hiddenLocation.value = location.id))
.catch((err) => {
console.log(err.message);
});
} else {
hiddenLocation.value = value.id;
}
commit("updateLocation", value);
}, },
actions: { async fetchAccompanyingPeriodWorks({ state, commit }) {
addIssueSelected({ commit }, issue) { const accompanyingPeriodId = state.activity.accompanyingPeriod.id;
let aSocialIssues = document.getElementById( const url = `/api/1.0/person/accompanying-course/${accompanyingPeriodId}/works.json`;
"chill_activitybundle_activity_socialIssues" try {
); const works = await makeFetch("GET", url);
aSocialIssues.value = addIdToValue(aSocialIssues.value, issue.id); // console.log("works", works);
commit("addIssueSelected", issue); commit("setAccompanyingPeriodWorks", works);
}, } catch (error) {
updateIssuesSelected({ commit }, payload) { console.error("Failed to fetch accompanying period works:", error);
let aSocialIssues = document.getElementById( }
"chill_activitybundle_activity_socialIssues"
);
aSocialIssues.value = "";
payload.forEach((item) => {
aSocialIssues.value = addIdToValue(
aSocialIssues.value,
item.id
);
});
commit("updateIssuesSelected", payload);
},
updateActionsSelected({ commit }, payload) {
let aSocialActions = document.getElementById(
"chill_activitybundle_activity_socialActions"
);
aSocialActions.value = "";
payload.forEach((item) => {
aSocialActions.value = addIdToValue(
aSocialActions.value,
item.id
);
});
commit("updateActionsSelected", payload);
},
addAvailableLocationGroup({ commit }, payload) {
commit("addAvailableLocationGroup", payload);
},
addPersonsInvolved({ commit }, payload) {
//console.log('### action addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons"
);
aPersons.value = addIdToValue(
aPersons.value,
payload.result.id
);
break;
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_thirdParties"
);
aThirdParties.value = addIdToValue(
aThirdParties.value,
payload.result.id
);
break;
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users"
);
aUsers.value = addIdToValue(
aUsers.value,
payload.result.id
);
break;
}
commit("addPersonsInvolved", payload);
},
removePersonInvolved({ commit }, payload) {
//console.log('### action removePersonInvolved', payload);
switch (payload.type) {
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons"
);
aPersons.value = removeIdFromValue(
aPersons.value,
payload.id
);
break;
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_thirdParties"
);
aThirdParties.value = removeIdFromValue(
aThirdParties.value,
payload.id
);
break;
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users"
);
aUsers.value = removeIdFromValue(aUsers.value, payload.id);
break;
}
commit("removePersonInvolved", payload);
},
updateLocation({ commit }, value) {
console.log("### action: updateLocation", value);
let hiddenLocation = document.getElementById(
"chill_activitybundle_activity_location"
);
if (value.onthefly) {
const body = {
"type": "location",
"name": value.name === '__AccompanyingCourseLocation__' ? null : value.name,
"locationType": {
"id": value.locationType.id,
"type": "location-type"
}
};
if (value.address.id) {
Object.assign(body, {
"address": {
"id": value.address.id
},
})
}
postLocation(body)
.then(
location => hiddenLocation.value = location.id
).catch(
err => {
console.log(err.message);
}
);
} else {
hiddenLocation.value = value.id;
}
commit("updateLocation", value);
},
}, },
getWhoAmI({ commit }) {
const url = `/api/1.0/main/whoami.json`;
makeFetch("GET", url).then((user) => {
commit("setWhoAmI", user);
});
},
},
}); });
store.dispatch("getWhoAmI");
prepareLocations(store); prepareLocations(store);
store.dispatch("fetchAccompanyingPeriodWorks");
export default store; export default store;

View File

@@ -1,124 +1,132 @@
import {getLocations, getLocationTypeByDefaultFor, getUserCurrentLocation} from "./api"; import {
getLocations,
getLocationTypeByDefaultFor,
getUserCurrentLocation,
} from "./api";
const makeConcernedPersonsLocation = (locationType, store) => { const makeConcernedPersonsLocation = (locationType, store) => {
let locations = []; let locations = [];
store.getters.suggestedEntities.forEach( store.getters.suggestedEntities.forEach((e) => {
(e) => { if (e.type === "person" && e.current_household_address !== null) {
if (e.type === 'person' && e.current_household_address !== null){ locations.push({
locations.push({ type: "location",
type: 'location', id: -store.getters.suggestedEntities.indexOf(e) * 10,
id: -store.getters.suggestedEntities.indexOf(e)*10, onthefly: true,
onthefly: true, name: e.text,
name: e.text, address: {
address: { id: e.current_household_address.address_id,
id: e.current_household_address.address_id, },
}, locationType: locationType,
locationType: locationType });
}); }
} });
} return locations;
)
return locations;
}; };
const makeConcernedThirdPartiesLocation = (locationType, store) => { const makeConcernedThirdPartiesLocation = (locationType, store) => {
let locations = []; let locations = [];
store.getters.suggestedEntities.forEach( store.getters.suggestedEntities.forEach((e) => {
(e) => { if (e.type === "thirdparty" && e.address !== null) {
if (e.type === 'thirdparty' && e.address !== null){ locations.push({
locations.push({ type: "location",
type: 'location', id: -store.getters.suggestedEntities.indexOf(e) * 10,
id: -store.getters.suggestedEntities.indexOf(e)*10, onthefly: true,
onthefly: true, name: e.text,
name: e.text, address: { id: e.address.address_id },
address: { id: e.address.address_id }, locationType: locationType,
locationType: locationType });
}); }
} });
} return locations;
)
return locations;
}; };
const makeAccompanyingPeriodLocation = (locationType, store) => { const makeAccompanyingPeriodLocation = (locationType, store) => {
if (store.state.activity.accompanyingPeriod === null) { if (store.state.activity.accompanyingPeriod === null) {
return {}; return {};
} }
const accPeriodLocation = store.state.activity.accompanyingPeriod.location; const accPeriodLocation = store.state.activity.accompanyingPeriod.location;
return { return {
type: 'location', type: "location",
id: -1, id: -1,
onthefly: true, onthefly: true,
name: '__AccompanyingCourseLocation__', name: "__AccompanyingCourseLocation__",
address: { address: {
id: accPeriodLocation.address_id, id: accPeriodLocation.address_id,
text: `${accPeriodLocation.text} - ${accPeriodLocation.postcode.code} ${accPeriodLocation.postcode.name}` text: `${accPeriodLocation.text} - ${accPeriodLocation.postcode.code} ${accPeriodLocation.postcode.name}`,
}, },
locationType: locationType locationType: locationType,
} };
}; };
export default function prepareLocations(store) { export default function prepareLocations(store) {
// find the locations // find the locations
let allLocations = getLocations().then( let allLocations = getLocations().then((results) => {
(results) => { store.commit("addAvailableLocationGroup", {
store.commit('addAvailableLocationGroup', { locationGroup: "Autres localisations",
locationGroup: 'Autres localisations', locations: results,
locations: results });
});
let currentLocation = getUserCurrentLocation().then((userCurrentLocation) => {
if (null !== userCurrentLocation) {
store.commit("addAvailableLocationGroup", {
locationGroup: "Ma localisation",
locations: [userCurrentLocation],
});
}
});
let partiesLocations = [],
partyPromise;
["person", "thirdparty"].forEach((kind) => {
partyPromise = getLocationTypeByDefaultFor(kind).then(
(kindLocationType) => {
if (kindLocationType) {
let concernedKindLocations;
if (kind === "person") {
concernedKindLocations = makeConcernedPersonsLocation(
kindLocationType,
store,
);
// add location for the parcours into suggestions
const personLocation = makeAccompanyingPeriodLocation(
kindLocationType,
store,
);
store.commit("addAvailableLocationGroup", {
locationGroup: "Localisation du parcours",
locations: [personLocation],
}); });
} else {
concernedKindLocations = makeConcernedThirdPartiesLocation(
kindLocationType,
store,
);
}
store.commit("addAvailableLocationGroup", {
locationGroup:
kind === "person" ? "Usagers concernés" : "Tiers concernés",
locations: concernedKindLocations,
});
} }
},
); );
partiesLocations.push(partyPromise);
});
let currentLocation = getUserCurrentLocation().then( // when all location are loaded
userCurrentLocation => { Promise.all([allLocations, currentLocation, ...partiesLocations]).then(() => {
if (null !== userCurrentLocation) { console.log("current location in activity", store.state.activity.location);
store.commit('addAvailableLocationGroup', { console.log("default loation id", window.default_location_id);
locationGroup: 'Ma localisation', if (window.default_location_id) {
locations: [userCurrentLocation] for (let group of store.state.availableLocations) {
}); let location = group.locations.find(
} (l) => l.id === window.default_location_id,
}
);
let partiesLocations = [], partyPromise;
['person', 'thirdparty'].forEach(kind => {
partyPromise = getLocationTypeByDefaultFor(kind).then(
(kindLocationType) => {
if (kindLocationType) {
let concernedKindLocations;
if (kind === 'person') {
concernedKindLocations = makeConcernedPersonsLocation(kindLocationType, store);
// add location for the parcours into suggestions
const personLocation = makeAccompanyingPeriodLocation(kindLocationType, store);
store.commit('addAvailableLocationGroup', {
locationGroup: 'Localisation du parcours',
locations: [personLocation]
});
} else {
concernedKindLocations = makeConcernedThirdPartiesLocation(kindLocationType, store);
}
store.commit('addAvailableLocationGroup', {
locationGroup: kind === 'person' ? 'Usagers concernés' : 'Tiers concernés',
locations: concernedKindLocations,
});
}
}
); );
partiesLocations.push(partyPromise); if (location !== undefined && store.state.activity.location === null) {
}); store.dispatch("updateLocation", location);
break;
// when all location are loaded
Promise.all([allLocations, currentLocation, ...partiesLocations]).then(() => {
console.log('current location in activity', store.state.activity.location);
console.log('default loation id', window.default_location_id);
if (window.default_location_id) {
for (let group of store.state.availableLocations) {
let location = group.locations.find((l) => l.id === window.default_location_id);
if (location !== undefined && store.state.activity.location === null) {
store.dispatch('updateLocation', location);
break;
}
}
} }
}); }
}
});
} }

View File

@@ -126,4 +126,4 @@
{% block css %} {% block css %}
{{ encore_entry_link_tags('mod_pickentity_type') }} {{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %} {% endblock %}

View File

@@ -1,83 +1,3 @@
{% 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 person_id = null %}
{% if activity.person %}
{% set person_id = activity.person.id %}
{% endif %}
{% set accompanying_course_id = null %}
{% if activity.accompanyingPeriod %}
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
{% endif %}
<div class="item-bloc activity-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}"> <div class="item-bloc activity-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<div class="item-row"> {{ include('@ChillActivity/GenericDoc/activity_document_row.html.twig') }}
<div class="item-col" style="width: unset">
{% if document.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div>
{% if activity.accompanyingPeriod is not null and context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %}
<div class="badge-activity-type">
<span class="title_label"></span>
<span class="title_action">
{{ activity.type.name | localize_translatable_string }}
{% if activity.emergency %}
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
{% endif %}
</span>
</div>
</div>
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.hasTemplate %}
<div>
<p>{{ document.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
<div class="dates row text-end">
<span>{{ document.createdAt|format_date('short') }}</span>
</div>
</div>
</div>
</div>
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
<li>
{{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false}) }}
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_SEE', activity)%}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a>
</li>
{% endif %}
</ul>
</div>
</div> </div>

View File

@@ -0,0 +1,81 @@
{% 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 person_id = null %}
{% if activity.person %}
{% set person_id = activity.person.id %}
{% endif %}
{% set accompanying_course_id = null %}
{% if activity.accompanyingPeriod %}
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
{% endif %}
<div class="item-row">
<div class="item-two-col-grid">
<div class="title">
{% if document.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div>
<div>
<div class="badge-activity-type-simple">
{{ activity.type.name | localize_translatable_string }}
</div>
{% if activity.emergency %}
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
{% endif %}
</div>
</div>
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.hasTemplate %}
<div>
<p>{{ document.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="aside">
<div class="dates row text-end">
<span>{{ document.createdAt|format_date('short') }}</span>
</div>
{% if activity.accompanyingPeriod is not null and context == 'person' %}
<div class="text-end">
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
</span>&nbsp;
</div>
{% endif %}
</div>
</div>
</div>
{% if show_actions %}
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
<li>
{{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false}) }}
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_SEE', activity)%}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a>
</li>
{% endif %}
</ul>
</div>
{% endif %}

View File

@@ -143,7 +143,10 @@ class ListActivitiesByAccompanyingPeriodContext implements
array_filter( array_filter(
$works, $works,
function ($work) use ($user) { function ($work) use ($user) {
$workUsernames = array_map(static fn (User $user) => $user['username'], $work['referrers'] ?? []); $workUsernames = [];
foreach ($work['referrers'] as $referrer) {
$workUsernames[] = $referrer['username'];
}
return \in_array($user->getUserIdentifier(), $workUsernames, true); return \in_array($user->getUserIdentifier(), $workUsernames, true);
} }

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Service\GenericDoc\Normalizer;
use Chill\ActivityBundle\Service\GenericDoc\Providers\AccompanyingPeriodActivityGenericDocProvider;
use Chill\ActivityBundle\Service\GenericDoc\Renderers\AccompanyingPeriodActivityGenericDocRenderer;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
final readonly class AccompanyingPeriodActivityGenericDocNormalizer implements GenericDocNormalizerInterface
{
public function __construct(
private StoredObjectRepositoryInterface $storedObjectRepository,
private AccompanyingPeriodActivityGenericDocRenderer $renderer,
private Environment $twig,
private TranslatorInterface $translator,
) {}
public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool
{
return AccompanyingPeriodActivityGenericDocProvider::KEY === $genericDocDTO->key
&& 'json' == $format;
}
public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array
{
$storedObject = $this->storedObjectRepository->find($genericDocDTO->identifiers['id']);
if (null === $storedObject) {
return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false];
}
return [
'isPresent' => true,
'title' => $storedObject->getTitle(),
'html' => $this->twig->render(
$this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
$this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]),
),
];
}
}

View File

@@ -13,10 +13,12 @@ namespace Chill\ActivityBundle\Service\GenericDoc\Providers;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface; use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
@@ -34,8 +36,47 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen
private EntityManagerInterface $em, private EntityManagerInterface $em,
private Security $security, private Security $security,
private ActivityDocumentACLAwareRepositoryInterface $activityDocumentACLAwareRepository, private ActivityDocumentACLAwareRepositoryInterface $activityDocumentACLAwareRepository,
private ActivityRepository $activityRepository,
) {} ) {}
public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject
{
if (null === $activity = $this->getRelatedEntity($genericDocDTO->key, $genericDocDTO->identifiers)) {
return null;
}
return $activity->getDocuments()->findFirst(fn (int $key, StoredObject $storedObject) => $storedObject->getId() === $genericDocDTO->identifiers['id']);
}
public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool
{
return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers);
}
public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool
{
return self::KEY === $key && array_key_exists('activity_id', $identifiers);
}
private function getRelatedEntity(string $key, array $identifiers): ?Activity
{
return $this->activityRepository->find($identifiers['activity_id']);
}
public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO
{
if (null === $activity = $this->getRelatedEntity($key, $identifiers)) {
return null;
}
return new GenericDocDTO(
self::KEY,
$identifiers,
\DateTimeImmutable::createFromInterface($activity->getDate()),
$activity->getAccompanyingPeriod(),
);
}
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{ {
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);

View File

@@ -18,6 +18,9 @@ use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepository; use Chill\DocStoreBundle\Repository\StoredObjectRepository;
/**
* @implements GenericDocRendererInterface<array{row-only?: bool, show-actions?: bool}>
*/
final readonly class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface final readonly class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface
{ {
public function __construct(private StoredObjectRepository $objectRepository, private ActivityRepository $activityRepository) {} public function __construct(private StoredObjectRepository $objectRepository, private ActivityRepository $activityRepository) {}
@@ -29,7 +32,8 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
{ {
return '@ChillActivity/GenericDoc/activity_document.html.twig'; return ($options['row-only'] ?? false) ? '@ChillActivity/GenericDoc/activity_document_row.html.twig' :
'@ChillActivity/GenericDoc/activity_document.html.twig';
} }
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
@@ -38,6 +42,7 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen
'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']), 'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']),
'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']), 'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']),
'context' => $genericDocDTO->getContext(), 'context' => $genericDocDTO->getContext(),
'show_actions' => $options['show-actions'] ?? true,
]; ];
} }
} }

View File

@@ -1,13 +1,18 @@
// this file loads all assets from the Chill person bundle // this file loads all assets from the Chill person bundle
module.exports = function(encore, entries) module.exports = function (encore, entries) {
{ entries.push(__dirname + "/Resources/public/chill/index.js");
entries.push(__dirname + '/Resources/public/chill/index.js');
encore.addAliases({ encore.addAliases({
ChillActivityAssets: __dirname + '/Resources/public' ChillActivityAssets: __dirname + "/Resources/public",
}); });
encore.addEntry('page_edit_activity', __dirname + '/Resources/public/page/edit_activity/index.scss'); encore.addEntry(
"page_edit_activity",
__dirname + "/Resources/public/page/edit_activity/index.scss",
);
encore.addEntry('vue_activity', __dirname + '/Resources/public/vuejs/Activity/index.js'); encore.addEntry(
"vue_activity",
__dirname + "/Resources/public/vuejs/Activity/index.js",
);
}; };

View File

@@ -14,3 +14,5 @@ export:
describe_action_with_subject: >- describe_action_with_subject: >-
Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}, et un de ces sujets choisis: {reasons} Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}, et un de ces sujets choisis: {reasons}
activity:
title: Échange du {date, date, long} - {type}

View File

@@ -101,6 +101,33 @@ activity:
Insert a document: Insérer un document Insert a document: Insérer un document
Remove a document: Supprimer le document Remove a document: Supprimer le document
comment: Commentaire comment: Commentaire
deleted: Échange supprimé
errors: Le formulaire contient des erreurs
social_issues: Problématiques sociales
choose_other_social_issue: Ajouter une autre problématique sociale...
social_actions: Actions d'accompagnement
select_first_a_social_issue: Sélectionnez d'abord une problématique sociale
social_action_list_empty: Aucune action sociale disponible
add_persons: Ajouter des personnes concernées
bloc_persons: Usagers
bloc_persons_associated: Usagers du parcours
bloc_persons_not_associated: Tiers non-pro.
bloc_thirdparty: Tiers professionnels
bloc_users: T(M)S
location: Localisation
choose_location: Choisissez une localisation
choose_location_type: Choisissez un type de localisation
create_new_location: Créer une nouvelle localisation
location_fields:
name: Nom
type: Type
phonenumber1: Téléphone
phonenumber2: Autre téléphone
email: Adresse courriel
create_address: Créer une adresse
edit_address: Modifier l'adresse
No documents: Aucun document No documents: Aucun document
# activity filter in list page # activity filter in list page

View File

@@ -1 +1 @@
require('./chillbudget.scss'); require("./chillbudget.scss");

View File

@@ -1,9 +1,8 @@
// this file loads all assets from the Chill budget bundle // this file loads all assets from the Chill budget bundle
module.exports = function(encore, entries) module.exports = function (encore, entries) {
{ encore.addAliases({
encore.addAliases({ ChillBudgetAssets: __dirname + "/Resources/public",
ChillBudgetAssets: __dirname + '/Resources/public' });
});
encore.addEntry('page_budget', __dirname + '/Resources/public/page/index.js'); encore.addEntry("page_budget", __dirname + "/Resources/public/page/index.js");
}; };

View File

@@ -21,9 +21,7 @@ namespace Chill\CalendarBundle\Command;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface; use Chill\CalendarBundle\Service\ShortMessageNotification\ShortMessageForCalendarBuilderInterface;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Service\ShortMessage\ShortMessageTransporterInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\PersonRepository;
use libphonenumber\PhoneNumber; use libphonenumber\PhoneNumber;
@@ -36,6 +34,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Question\Question;
use Symfony\Component\Notifier\TexterInterface;
class SendTestShortMessageOnCalendarCommand extends Command class SendTestShortMessageOnCalendarCommand extends Command
{ {
@@ -44,9 +43,8 @@ class SendTestShortMessageOnCalendarCommand extends Command
public function __construct( public function __construct(
private readonly PersonRepository $personRepository, private readonly PersonRepository $personRepository,
private readonly PhoneNumberUtil $phoneNumberUtil, private readonly PhoneNumberUtil $phoneNumberUtil,
private readonly PhoneNumberHelperInterface $phoneNumberHelper,
private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder, private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder,
private readonly ShortMessageTransporterInterface $transporter, private readonly TexterInterface $transporter,
private readonly UserRepositoryInterface $userRepository, private readonly UserRepositoryInterface $userRepository,
) { ) {
parent::__construct('chill:calendar:test-send-short-message'); parent::__construct('chill:calendar:test-send-short-message');
@@ -152,10 +150,6 @@ class SendTestShortMessageOnCalendarCommand extends Command
return $phone; return $phone;
}); });
$phone = $helper->ask($input, $output, $question);
$question = new ConfirmationQuestion('really send the message to the phone ?');
$reallySend = (bool) $helper->ask($input, $output, $question);
$messages = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar); $messages = $this->messageForCalendarBuilder->buildMessageForCalendar($calendar);
@@ -165,8 +159,12 @@ class SendTestShortMessageOnCalendarCommand extends Command
foreach ($messages as $key => $message) { foreach ($messages as $key => $message) {
$output->writeln("The short message for SMS {$key} will be: "); $output->writeln("The short message for SMS {$key} will be: ");
$output->writeln($message->getContent()); $output->writeln($message->getSubject());
$message->setPhoneNumber($phone); $output->writeln('The destination number will be:');
$output->writeln($message->getPhone());
$question = new ConfirmationQuestion('really send the message to the phone ?');
$reallySend = (bool) $helper->ask($input, $output, $question);
if ($reallySend) { if ($reallySend) {
$this->transporter->send($message); $this->transporter->send($message);

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository; namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\CalendarDoc; use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
@@ -49,4 +50,21 @@ class CalendarDocRepository implements ObjectRepository, CalendarDocRepositoryIn
{ {
return CalendarDoc::class; return CalendarDoc::class;
} }
/**
* @param StoredObject|int $storedObject the StoredObject instance, or the id of the stored object
*/
public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc
{
$storedObjectId = $storedObject instanceof StoredObject ? $storedObject->getId() : $storedObject;
$qb = $this->repository->createQueryBuilder('c');
$qb->where(
$qb->expr()->eq(':storedObject', 'c.storedObject')
);
$qb->setParameter('storedObject', $storedObjectId);
return $qb->getQuery()->getOneOrNullResult();
}
} }

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository; namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\CalendarDoc; use Chill\CalendarBundle\Entity\CalendarDoc;
use Chill\DocStoreBundle\Entity\StoredObject;
interface CalendarDocRepositoryInterface interface CalendarDocRepositoryInterface
{ {
@@ -29,5 +30,7 @@ interface CalendarDocRepositoryInterface
public function findOneBy(array $criteria): ?CalendarDoc; public function findOneBy(array $criteria): ?CalendarDoc;
public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc;
public function getClassName(); public function getClassName();
} }

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