Compare commits

...

64 Commits

Author SHA1 Message Date
3b75f43e80 Update chill bundles to v4.1.0 2025-08-26 15:43:21 +02:00
a40eb95c43 Add changie for new event bundle features 2025-08-26 15:41:58 +02:00
8429c6e693 Merge branch 'improvements_event_module' into 'master'
Improvements event module

See merge request Chill-Projet/chill-bundles!825
2025-08-26 13:35:36 +00:00
6db7f6827c Update eslint baseline 2025-08-26 15:24:44 +02:00
3c60c57985 Adapt export list events to new export features 2025-08-26 15:18:08 +02:00
10aa36aae0 Set required to false for entitychoice filter field 2025-08-26 15:18:08 +02:00
eed9913a49 Allow select2 option for entityChoice filterOrderHelper 2025-08-26 15:18:08 +02:00
1a847d36a0 Fixes in template parameters + remove budget elements when removing event 2025-08-26 15:18:08 +02:00
d916962d9b Phpstan fix import Serializer instead of SerializerInterface 2025-08-26 15:18:08 +02:00
1092fc64ae Fix voter for the create event permission 2025-08-26 15:18:08 +02:00
4e99b6ecbd Allow filtering of event list by center and responsable 2025-08-26 15:18:08 +02:00
60d107b541 Create internal and external animators 2025-08-26 15:18:08 +02:00
4c3befe489 WIP change animator field to animator intern and animator extern 2025-08-26 15:18:08 +02:00
e176319775 WIP change animator field 2025-08-26 15:18:08 +02:00
5d810b4230 Add center to the show page of an event 2025-08-26 15:18:08 +02:00
52b8eea069 Fix passing of id parameter to route 2025-08-26 15:18:08 +02:00
4bebeaeaaa Fix wrong import of serializer 2025-08-26 15:18:08 +02:00
3969e12633 Fix cs and phpstan issues 2025-08-26 15:18:08 +02:00
d60312d4a2 Move styling to scss file and fix styling of participation list 2025-08-26 15:18:08 +02:00
d2454ae134 use key for column names in export 2025-08-26 15:18:08 +02:00
17c2cb1fdc Add missing translations 2025-08-26 15:18:08 +02:00
94d7a2a0bb Reverse deleting of organizationCost property on event entity to keep db data 2025-08-26 15:18:08 +02:00
aef1efc6cd Add missing translations and add eventThemeType missing config 2025-08-26 15:18:08 +02:00
dd0c662c9e Add missing description to migration 2025-08-26 15:18:08 +02:00
6b1696b62e phpstan, rector, phpcs fixes 2025-08-26 15:18:08 +02:00
c4b760c452 eslint fixes and new baseline 2025-08-26 15:18:08 +02:00
69fe2a8256 Add translations 2025-08-26 15:18:08 +02:00
8c98242896 Split budget elements in charges and resources column 2025-08-26 15:18:08 +02:00
7eecfd3882 Add new columns to export list event 2025-08-26 15:18:08 +02:00
6713658569 Add animators property to event 2025-08-26 15:18:08 +02:00
342b786106 Create export list of events 2025-08-26 15:18:08 +02:00
80a7437769 Update twig templates for display budget elements 2025-08-26 15:18:08 +02:00
8a38ce1a5c Add event budget element entity, forms and event property 2025-08-26 15:18:08 +02:00
5d94bf0556 Create an event budget kind admin entity 2025-08-26 15:18:08 +02:00
bb71e084b8 Create address on the fly field in event form 2025-08-26 15:18:08 +02:00
27f0bf28e9 Adjust templates and translations 2025-08-26 15:18:08 +02:00
383f588795 Add field in event for themes 2025-08-26 15:18:08 +02:00
e7a1ff1ac8 Add event theme property to event entity 2025-08-26 15:18:08 +02:00
adc9c47d0a Add event theme color for badge 2025-08-26 15:18:08 +02:00
e594b65d1e Create event theme admin entity 2025-08-26 15:18:08 +02:00
c0826bc65c Merge branch '400-add-filter-mes-actions' into 'master'
Add a filter to list for acpw where current user intervenes

Closes #400

See merge request Chill-Projet/chill-bundles!859
2025-08-18 16:26:20 +00:00
904f4e5ed9 Add a filter to list for acpw where current user intervenes 2025-08-18 16:26:20 +00:00
481f82b4c7 Merge branch '355-fusion-thirdparty' into 'master'
Resolve "Fusion des tiers"

Closes #355

See merge request Chill-Projet/chill-bundles!795
2025-08-18 15:34:48 +00:00
f5668592ca Resolve "Fusion des tiers" 2025-08-18 15:34:48 +00:00
aa085a1562 **fix:** add min and step attributes to integer field in DateIntervalType 2025-08-06 17:35:45 +02:00
2754251fdc Merge branch 'master' of https://gitlab.com/Chill-Projet/chill-bundles 2025-08-06 14:20:29 +02:00
2f6cef4238 - **fix:** move closing motive up to be coherent with display elsewhere 2025-08-06 14:20:09 +02:00
2309636eae - **fix:** adjust display logic for accompanying period dates, include closing date if period is closed. 2025-08-06 13:47:29 +02:00
56ec8fb516 Remove 'to_validate' as default for task filter 2025-08-06 09:05:39 +02:00
fe6e6e54c1 Show filters on list pages unfolded by default 2025-07-22 15:50:49 +02:00
2a09594b4a UI improvement: limit display of particapations in event list page 2025-07-22 13:26:44 +02:00
7c798e1f63 Merge branch '387-notification-user-group' into 'master'
Resolve "Notification: envoi à des groupes utilisateurs"

Closes #387

See merge request Chill-Projet/chill-bundles!842
2025-07-20 20:18:49 +00:00
ab8da4ab7a Resolve "Notification: envoi à des groupes utilisateurs" 2025-07-20 20:18:49 +00:00
5bdb2df929 Merge branch 'revert-5f016734' into 'master'
Revert "Merge branch 'ticket/supplementary-comments-on-motive' into 'master'"

See merge request Chill-Projet/chill-bundles!863
2025-07-20 18:51:51 +00:00
e3a6b60fa2 Revert "Merge branch 'ticket/supplementary-comments-on-motive' into 'master'"
This reverts merge request !855
2025-07-20 18:50:33 +00:00
5f01673404 Merge branch 'ticket/supplementary-comments-on-motive' into 'master'
Ajout de commentaires supplémentaires aux motifs

See merge request Chill-Projet/chill-bundles!855
2025-07-11 14:06:40 +00:00
63d0a52ea1 Ajout de commentaires supplémentaires aux motifs 2025-07-11 14:06:40 +00:00
837089ff5d Fix testMerge method in AccompanyingPeriodWorkMergeServiceTest.php 2025-07-10 11:33:23 +02:00
f383fab578 Fix styling 2025-07-09 15:30:39 +02:00
f3cc4a89af Update chill bundles to v4.0.2 2025-07-09 15:23:59 +02:00
703f5dc32d Transfer evaluations (and related documents) during merge 2025-07-09 15:21:42 +02:00
b870e71f77 Add translation for validation message in social action merger 2025-07-09 15:21:24 +02:00
a7e278204f fix changelog 2025-07-09 09:22:28 +02:00
4cfdcb2f02 Release v4.0.1 with fix in package.json file 2025-07-08 17:00:24 +02:00
142 changed files with 5394 additions and 1057 deletions

4
.changes/v4.0.1.md Normal file
View File

@@ -0,0 +1,4 @@
## v4.0.1 - 2025-07-08
### Fixed
* Fix package.json for compilation

4
.changes/v4.0.2.md Normal file
View File

@@ -0,0 +1,4 @@
## v4.0.2 - 2025-07-09
### Fixed
* Fix add missing translation
* Fix the transfer of evaluations and documents during of accompanyingperiodwork

12
.changes/v4.1.0.md Normal file
View File

@@ -0,0 +1,12 @@
## v4.1.0 - 2025-08-26
### Feature
* ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes
* ([#399](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/399)) Show filters on list pages unfolded by default
* Expansion of event module with new fields in the creation form: thematic, internal/external animator, responsable, and budget elements. Filtering options in the event list + adapted exports
**Schema Change**: Add columns or tables
### Fixed
* ([#382](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/382)) adjust display logic for accompanying period dates, include closing date if period is closed.
* ([#384](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/384)) add min and step attributes to integer field in DateIntervalType
### UX
* Limit display of participations in event list

View File

@@ -7,14 +7,6 @@
"message": "'app' is assigned a value but never used.",
"hash": "f8c2979921289906e3baabae31ba101ead91504f"
},
{
"path": "src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/index.js",
"line": 57,
"column": 23,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'event' is defined but never used.",
"hash": "cf0cf378f71403f62a6425f384ccbbdec433d1f2"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/module/Invite/answer.js",
"line": 7,
@@ -127,46 +119,6 @@
"message": "'payload' is defined but never used.",
"hash": "66c545917093ba30f1d6ca10ddaa676140e749bd"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 224,
"column": 10,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reactive' is defined but never used.",
"hash": "96ed76a9828138fb125fc36c4b55e900bbfe87c2"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 230,
"column": 5,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'DropArg' is defined but never used.",
"hash": "bd405399a4091d65e8391404bfb0c4611816c8e0"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 251,
"column": 9,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'t' is assigned a value but never used.",
"hash": "bc09207a496405f7a71c178e522b89aeb1f7ebd3"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 356,
"column": 32,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'arg' is defined but never used.",
"hash": "aeae152f0669b946a1ad681dd52b0ef03393ae79"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue",
"line": 434,
"column": 11,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'changedEvent' is assigned a value but never used.",
"hash": "a7a81a6bf09d00c0364e3aa8207ffad853f0547b"
},
{
"path": "src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/Components/EditLocation.vue",
"line": 77,
@@ -399,14 +351,6 @@
"message": "'error' is defined but never used.",
"hash": "e26e5e101e90d2b7ee84d6f5de8c819e52129c17"
},
{
"path": "src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/index.ts",
"line": 29,
"column": 14,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'vm' is defined but never used.",
"hash": "8e7f5e89dd72c54459cf82156389b88988f97d63"
},
{
"path": "src/Bundle/ChillDocStoreBundle/Resources/public/module/async_upload/uploader.js",
"line": 39,
@@ -615,14 +559,6 @@
"message": "'ref' is defined but never used.",
"hash": "2a27cd6d06a26e1326654c929068e3704137e24b"
},
{
"path": "src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue",
"line": 57,
"column": 17,
"ruleId": "vue/valid-v-for",
"message": "Custom elements in iteration require 'v-bind:key' directives.",
"hash": "cce787939524e83dd135869e13738ef332d7156c"
},
{
"path": "src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/WopiEditButton.vue",
"line": 15,
@@ -983,22 +919,6 @@
"message": "'_e' is defined but never used.",
"hash": "1d6448401778e8c56554020fe5abd47851ed33f3"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/module/wopi-link/index.js",
"line": 21,
"column": 55,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'e' is defined but never used.",
"hash": "eae499e4f6e9f43a9d17f9cd917cb6d3d97be25c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/page/export/download-export.js",
"line": 3,
"column": 55,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'e' is defined but never used.",
"hash": "088fd383e7807e484aefc9825209bc7c8942bd22"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/page/homepage_widget/index.js",
"line": 9,
@@ -1089,115 +1009,19 @@
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 247,
"column": 5,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'postAddressToPerson' is defined but never used.",
"hash": "8a41c437cf2b5554cbbe1704cd51f3102b3d5994"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 248,
"column": 5,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'postAddressToHousehold' is defined but never used.",
"hash": "66dec84b2ece299daf21308e5e60d497ba442b27"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 490,
"line": 516,
"column": 21,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"context\" prop.",
"hash": "0d3f40c47974a4371072b3b9ee04b197c830162d"
"hash": "984c4203f2ac1e1bb65f9ce76ecd03b763cfaa83"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 491,
"line": 517,
"column": 21,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"context\" prop.",
"hash": "8e877b7e588c30e182f7b572bdb9685360f9cf99"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 508,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "5a3e3401bc3c765d91faaf4cfde57697af1262b7"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 525,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "35a741d90379574b9323279f5802193d0c98a9dc"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 553,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "c23d1ddf6c0d10ae97948e74aee9c14b9320b86c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 572,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "4322e81c6ea9d9734c680633a724d5bd4fabacb2"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 803,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "7928a6461b9d394c7d97f048933553936f7d8963"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue",
"line": 852,
"column": 47,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "e5afdb8efccb5470a08dde48f755b1268fa947b5"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 93,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "68f5e1cf5c03f9ada59c9e0afca0b74c7f3fca4b"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 101,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "50d730f6109092baff2db66adc44dc1315e2bda2"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 109,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "573e4c041ce663f28b933d7a675c2a525aba644c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 117,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "293f845eeab515b1df4649d136c2d8219ed59c4d"
"hash": "c9fb019bc21bfa77d989ed596913b99dd653c594"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
@@ -1224,204 +1048,180 @@
"hash": "2d5a5e680ff207ad97c7e7b7d999064b561dfd8a"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 106,
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 149,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "d52356f2af31d0167c02330ec22d09fbfa6b2b9f"
"hash": "e4c1ecd7ae77d46ac3625c5bbe92a24d6a964db9"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 114,
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 157,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "c8e8e06f370f93bf05867e93b5f037dfa46937b1"
"hash": "4dece2db87c6ce1c04ae06c088ddfe916c1c0c61"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 165,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "facc7a0f17bdf19396fae3d0de3da82e60503c0d"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressMore.vue",
"line": 173,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "19de32c76518387218264d7c4dab914d143a9cca"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 128,
"column": 13,
"line": 130,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "9abaf71ca4b4f292b3b01e724d0a7733365e71f1"
"hash": "239ac02a02694d5b20ab30d4c7ce5838c51d1515"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 129,
"column": 13,
"line": 138,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "0b0743959778a9e3d93089b132608816ee4e6646"
"hash": "a54f9bc6d1edfa4df93c7dd7d409cfef3fccf99e"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 132,
"line": 152,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "9759da7b7859b8ee8efaf74876430658ac6b6fe2"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 133,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "dba8be9a27ab74ec743b7d9e07c05d857b407dd3"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 134,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "9b1f5bce779aafc46b19d7a5d266eaa29f8f9be9"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 139,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "fe6fc4aea0994ba9da15b7c09d308842b67958cb"
"hash": "74a5f664d18f3916ea908897fcd0291cb0128f29"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 153,
"column": 55,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "bd0e024fcad2e3f4566f15293e3c25c840f6dd3e"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 154,
"column": 37,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "596c4b180b926b7829f987384328bf5636cd367a"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 171,
"column": 59,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "5b41d5f9b45da074fb7bbbbd45e0da501da72071"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 172,
"column": 41,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "d92b92a25043244cca809bd129633b7e024e26b4"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 190,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "dd9a85ea740742d620e864796f67c5bff834486d"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 191,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "e3e59960d0d50709a57b336f66b586710b774892"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 192,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "fe11b0e54396511e7b3b08615a78d22fc27e2fad"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 222,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "63c14c2150c33ec701bc4a0ff94efde69537d490"
"hash": "740ea5d793c7a34c9f352d8b333f3aa04cc80ee8"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 156,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "af8aca18f0226a5988ed90d44d95e2d607bfb5e6"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 157,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "7bc2453017793ae20cd6c10005f941d384b59d84"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 158,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "571b4ee5f22358dd165ec59696bb3439b7c9ff6c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 163,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "cfcb5946c86e289fc61623a794284a5a272d02e8"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 178,
"column": 37,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "0ec402e43cb08bf129e0737c0d2c4f6d0c7af8bd"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 196,
"column": 41,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "ec178d33e067aac892e015002afb6f3a2ff98762"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 214,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "c0f4e5454e672b6064eb9cf6c235c6810f7bfa80"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 215,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "e3dd840d2474f9865a45822872bf9ecfb15961d7"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 216,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "a32a60382b145cc7a4a7ebe01ec435b8e3103320"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue",
"line": 246,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "082447e5c731012f3acc282943502775dfd24797"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 96,
"line": 118,
"column": 20,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "d2a9fdaeef0e2810f480022d4c6f99e4f76a818e"
"hash": "d4fba4fe09af3c0937c0dd164928c8930c1591b5"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 96,
"line": 118,
"column": 20,
"ruleId": "vue/no-side-effects-in-computed-properties",
"message": "Unexpected side effect in \"cities\" computed property.",
"hash": "dd92a60a9b1ebefeb9a90941d45326fbfa483733"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 102,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "04be01ab638ce01f568fb0216929e65e1175ca23"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 110,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "8619c8e0b63e87d09268832f90e4fba06b87e41f"
"hash": "1113a114d5aaf9f32f442916d25458541c5af35c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 124,
"column": 13,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "281f918da00635079501418b1e6b2c05b62eb4a7"
"hash": "fa56a7c93583f0a9d0c2ecac10228c4f4fc1bc3a"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 125,
"column": 13,
"line": 132,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "c131b09fa67ab1d069f1d04a54582d6b0f206153"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 126,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "3d3a2a4add64c291b8f5f1cddd90a173cd6a819d"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 131,
"column": 21,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "ed48f4988914d7897018a2e06830a97e6740b3e8"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 145,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "744f3a7610d4d6015e50e25149bceffd6c6e2763"
"hash": "9fe87937ea67d1dae95fb3d44d4be0da2eba0905"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
@@ -1441,115 +1241,139 @@
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 149,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "1e7b1ad55866f708baaca72dfa4ff26d6f8e5d21"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 152,
"line": 148,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "84779331536ffceec8d4a8c5ca4307310b882549"
"hash": "ab4f478fbfbc954b8dff75176dcd432f9ff28cfc"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 161,
"line": 153,
"column": 21,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "1d907d149f9ddb62e32140a90efe9a74b3e71fef"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 167,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "0789999841be671a4d8ab080d6fdb679f843eb52"
"hash": "8aa37d2d4f011773e68838a2c88017875de563b5"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 170,
"column": 51,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "bbb17afa114f016e2058d90aa32d2a625804f0d1"
"line": 168,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "a4827a357e52a51fa9262319114d81a130296acf"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 169,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "a4c9715664202949e3242b8d4aa4098288b46dc4"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 171,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "f3e9e21e433e90ec7b615b8940d43c4177372b66"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 174,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "770b7a24cc24b380e88db47d62422c8e1ece2571"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 183,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "2aef3c519a9ec6abcfe7573989d3de19d5c4c752"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 193,
"column": 33,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "5fbe407ceceb37bff2ac800ceddd7942540132f1"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 190,
"column": 55,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'reject' is defined but never used.",
"hash": "e2af91def877befbabef8e93deba4c58a3ee2ded"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 191,
"column": 37,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "ee8544ee45681a650ed7d4918ae979685cdd8f0f"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 210,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "5d9d2217c8c7e6571bc9f72a98ea5b370edb4968"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 211,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "6e04619b373c23c91f6c36c2aad314ac16cdb697"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 212,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "39df045639a62f64ccdb03a80e286bc3ad772587"
"hash": "5d1f97e4d7d9f47399d312e8b9f95ef9e3843b8c"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 213,
"column": 17,
"column": 37,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "c399a43fa797a8ce61c9d96a644a39cc84a387b7"
"hash": "c1df874f790ef0c036bf58ae8a8db1ee173685d4"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 245,
"line": 232,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "476e6588a28ac9382e8b9d2e63a8babecd23bad8"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 233,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "6a0c82ba72d6d87217bf33a6ad8e40a4b81bc802"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 234,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "741d5af6c7d90041c0dc1c1df2e8699b80fca69a"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 235,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "c3ffd141f58d532663875cc5c7d338ed00db2a6d"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue",
"line": 267,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "04337a07944caaa4819cfebcf29e1a7cbfdf248b"
"hash": "2700f258396516a2fe971618fafbcdf72cdda3ab"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue",
"line": 76,
"line": 94,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "373a2e31f110d138c66d77f1faf5dc61545c55af"
"hash": "4be1b0592efa775092a91a1d744e16ce98bd216e"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue",
"line": 81,
"line": 99,
"column": 13,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "421eb6a63224b4b1d81b216677a710c5c99ddee3"
"hash": "19b54b6d76c30249d520a296f826eda9d6eb0668"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/DatePane.vue",
@@ -1569,19 +1393,19 @@
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue",
"line": 155,
"line": 169,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "b3a822914fcb5e2fcf28efc331a45b9205002eeb"
"hash": "dcb7b34098062760ddbb849655a5bb3ca65c36d3"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue",
"line": 164,
"line": 178,
"column": 17,
"ruleId": "vue/no-mutating-props",
"message": "Unexpected mutation of \"entity\" prop.",
"hash": "72c7d850f6cdeaf65b373a33234222f9766ee30b"
"hash": "86b3ecf201025cac36878c5e4bf8850fb9d58cb5"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/index.js",
@@ -1631,14 +1455,6 @@
"message": "'app' is assigned a value but never used.",
"hash": "9e6125f4fc387dc362c69cc6e3ce360eb2851f1b"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue",
"line": 60,
"column": 22,
"ruleId": "vue/require-valid-default-prop",
"message": "Type of the default value for 'suggested' prop must be a function.",
"hash": "d30212820bc2e97fa02d75dbc3a014558693f169"
},
{
"path": "src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue",
"line": 24,
@@ -1727,14 +1543,6 @@
"message": "'tags' is assigned a value but never used.",
"hash": "ae9bb2e0651c118ed9efd227e88b86cc83f5d80d"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/StickyNav.vue",
"line": 116,
"column": 18,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'event' is defined but never used.",
"hash": "201f182769c6dfb87148b841e7d9b592be429669"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/index.js",
"line": 19,
@@ -1767,14 +1575,6 @@
"message": "'app' is assigned a value but never used.",
"hash": "aaaaa63e7a60443b8cbf8191feb9142852ebdf1c"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/components/FormEvaluation.vue",
"line": 79,
"column": 13,
"ruleId": "vue/require-v-for-key",
"message": "Elements in iteration expect to have 'v-bind:key' directives.",
"hash": "422f53925922e59655d0f71624c19af75d41628c"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/index.js",
"line": 12,
@@ -1815,22 +1615,6 @@
"message": "'evalFQDN' is assigned a value but never used.",
"hash": "7fc32caafa23addddf44f3acbc5045b4523a0271"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js",
"line": 611,
"column": 9,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'errors' is assigned a value but never used.",
"hash": "c41cf979fc1626c38328dbf1028800c3395496bd"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/ExportFormActionGoalResult/App.vue",
"line": 282,
"column": 7,
"ruleId": "@typescript-eslint/no-unused-expressions",
"message": "Expected an assignment or function call and instead saw an expression.",
"hash": "de3a6e2bb10a80a2bacba665be74266c7efc7d64"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/ExportFormActionGoalResult/index.js",
"line": 16,
@@ -1847,38 +1631,6 @@
"message": "'app' is assigned a value but never used.",
"hash": "2f161e663689e3e4dfe2c53b0d64c91a4d2b1a60"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue",
"line": 263,
"column": 19,
"ruleId": "vue/return-in-computed-property",
"message": "Expected to return a value in \"refreshNetwork\" computed property.",
"hash": "2c1b08a49098c83b09058cedc0a962126e91e544"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue",
"line": 270,
"column": 7,
"ruleId": "vue/no-side-effects-in-computed-properties",
"message": "Unexpected side effect in \"legendLayers\" computed property.",
"hash": "760948d2187c853f17ac9a1bd7107e883092d4f4"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue",
"line": 281,
"column": 5,
"ruleId": "vue/no-dupe-keys",
"message": "Duplicate key 'checkedLayers'. May cause name collision in script or template tag.",
"hash": "447edb461e15e3ff5c60c8ecba88131e442539aa"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue",
"line": 353,
"column": 7,
"ruleId": "@typescript-eslint/no-unused-expressions",
"message": "Expected an assignment or function call and instead saw an expression.",
"hash": "9cf656cbf1eb3d7cc0082e63adcd320b6093d14f"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js",
"line": 20,
@@ -1887,22 +1639,6 @@
"message": "'app' is assigned a value but never used.",
"hash": "9e94e6412b8a44e47bfe8e66218cad09cff5bed4"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AccompanyingPeriod/SetReferrer.vue",
"line": 42,
"column": 16,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'response' is defined but never used.",
"hash": "62de07b13c662e32332bb062038acee23978ea70"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue",
"line": 356,
"column": 28,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'_response' is defined but never used.",
"hash": "097e7788a2b5dea500b80b8a3cf968e57063a66a"
},
{
"path": "src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/TypeUserGroup.vue",
"line": 6,
@@ -1918,45 +1654,5 @@
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'UserRenderBoxBadge' is defined but never used.",
"hash": "99eba0d8633b2c9497417f4f61ec4194dbb2a96b"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 4,
"column": 3,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'StoredObjectStatus' is defined but never used.",
"hash": "63f8c4572293916850d6165647774b27d4b732c6"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 5,
"column": 3,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'StoredObjectStatusChange' is defined but never used.",
"hash": "a87c178e3eb5999bf0f46b3fa1c6da77e1be08b9"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 30,
"column": 61,
"ruleId": "@typescript-eslint/no-unused-vars",
"message": "'e' is defined but never used.",
"hash": "02953121583f4f73742a19adab099ab63df9076e"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 31,
"column": 32,
"ruleId": "@typescript-eslint/no-explicit-any",
"message": "Unexpected any. Specify a different type.",
"hash": "af48e21a1651b6017ede882dab249c00a818a44d"
},
{
"path": "src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts",
"line": 37,
"column": 16,
"ruleId": "@typescript-eslint/no-explicit-any",
"message": "Unexpected any. Specify a different type.",
"hash": "7513ea552a0a649ce4ab93b6cf9d40bfef4f68d9"
}
]

View File

@@ -185,14 +185,57 @@ When we need to use a DateTime or DateTimeImmutable that need to express "now",
`Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities,
where injection does not work when restoring an entity from database, but usually possible in services.
In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface`
where we have full and easy control of the date.
### Testing Information
The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level.
#### Use of mock in tests
##### General mocking
For creating mock, we prefer using prophecy (library phpspec/prophecy).
##### Useful helpers and tips that avoid create a mock
Some notable implementations that are tests helper, and avoid to create a mock:
- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`;
- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above);
- `\Symfony\Component\HttpClient\MockHttpClient`, an implementation of `\Symfony\Contracts\HttpClient\HttpClientInterface`;
- When using `\Symfony\Component\Mailer\MailerInterface`, we can create the mock with "InMemoryTransport":
```php
use Symfony\Component\Mailer\Transport\InMemoryTransport;
use \Symfony\Component\Mailer\Mailer;
$transport = new InMemoryTransport();
$mailer = new Mailer($transport);
// After sending:
$messages = $transport->getSent(); // array of SentMessage
```
- When using `\Symfony\Contracts\EventDispatcher\EventDispatcherInterface`, we can use directly an instance of `\Symfony\Component\EventDispatcher\EventDispatcher`;
##### When we prefer not creating a mock
- When we use Doctrine Entities related to the project, we prefer not to use a mock: we instantiate them directly (unless it requires too much code to write);
##### Mocking final and readonly classes
Classes marked as final can't be mocked. To avoid that, either:
- we remove the `final` keyword from the class;
- we extract an interface from the final class.
This must be a decision made by a human, not by an AI. Every AI task must abort with an explicit message in that case.
#### Running Tests
The tests are run from the project's root (not from the bundle's root).
```bash
# Run all tests
vendor/bin/phpunit

View File

@@ -6,6 +6,29 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.1.0 - 2025-08-26
### Feature
* ([#400](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/400)) Add filter to social actions list to filter out actions where current user intervenes
* ([#399](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/399)) Show filters on list pages unfolded by default
* Expansion of event module with new fields in the creation form: thematic, internal/external animator, responsable, and budget elements. Filtering options in the event list + adapted exports
**Schema Change**: Add columns or tables
### Fixed
* ([#382](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/382)) adjust display logic for accompanying period dates, include closing date if period is closed.
* ([#384](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/384)) add min and step attributes to integer field in DateIntervalType
### UX
* Limit display of participations in event list
## v4.0.2 - 2025-07-09
### Fixed
* Fix add missing translation
* Fix the transfer of evaluations and documents during of accompanyingperiodwork
## v4.0.1 - 2025-07-08
### Fixed
* Fix package.json for compilation
## v4.0.0 - 2025-07-08
### Feature
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works

View File

@@ -62,8 +62,10 @@ framework:
'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority
'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage': async
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
# end of routes added by chill-bundles recipes
# Route your messages to the transports
# 'App\Message\YourMessage': async

View File

@@ -11,7 +11,6 @@
"@hotwired/stimulus": "^3.0.0",
"@luminateone/eslint-baseline": "^1.0.9",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node20": "^20.1.4",
"@types/dompurify": "^3.0.5",

View File

@@ -24,7 +24,11 @@ use Doctrine\ORM\EntityManagerInterface;
class CalendarForShortMessageProvider
{
public function __construct(private readonly CalendarRepository $calendarRepository, private readonly EntityManagerInterface $em, private readonly RangeGeneratorInterface $rangeGenerator) {}
public function __construct(
private readonly CalendarRepository $calendarRepository,
private readonly EntityManagerInterface $em,
private readonly RangeGeneratorInterface $rangeGenerator,
) {}
/**
* Generate calendars instance.

View File

@@ -21,7 +21,6 @@ namespace Chill\CalendarBundle\Tests\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
@@ -82,10 +81,16 @@ final class CalendarForShortMessageProviderTest extends TestCase
$em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldBeCalled();
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
'startDate' => new \DateTimeImmutable('yesterday'),
'endDate' => new \DateTimeImmutable('now'),
]);
$provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(),
$em->reveal(),
new DefaultRangeGenerator()
$calendarRangeGenerator->reveal(),
);
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
@@ -103,26 +108,32 @@ final class CalendarForShortMessageProviderTest extends TestCase
Argument::type(\DateTimeImmutable::class),
Argument::type('int'),
Argument::exact(0)
)->will(static fn ($args) => array_fill(0, 1, new Calendar()))->shouldBeCalledTimes(1);
)->will(static fn ($args) => array_fill(0, 10, new Calendar()))->shouldBeCalledTimes(1);
$calendarRepository->findByNotificationAvailable(
Argument::type(\DateTimeImmutable::class),
Argument::type(\DateTimeImmutable::class),
Argument::type('int'),
Argument::not(0)
Argument::exact(10)
)->will(static fn ($args) => [])->shouldBeCalledTimes(1);
$em = $this->prophesize(EntityManagerInterface::class);
$em->clear()->shouldBeCalled();
$calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
$calendarRangeGenerator->generateRange(Argument::any())->willReturn([
'startDate' => new \DateTimeImmutable('yesterday'),
'endDate' => new \DateTimeImmutable('now'),
]);
$provider = new CalendarForShortMessageProvider(
$calendarRepository->reveal(),
$em->reveal(),
new DefaultRangeGenerator()
$calendarRangeGenerator->reveal(),
);
$calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now')));
$this->assertEquals(1, \count($calendars));
$this->assertEquals(10, \count($calendars));
$this->assertContainsOnly(Calendar::class, $calendars);
}
}

View File

@@ -0,0 +1,28 @@
<?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\EventBundle\Controller\Admin;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
class EventBudgetKindController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
/* @var QueryBuilder $query */
$query->addOrderBy('e.type', 'ASC');
return parent::orderQuery($action, $query, $request, $paginator);
}
}

View File

@@ -23,11 +23,11 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Chill\PersonBundle\Privacy\PrivacyEvent;
use Doctrine\Persistence\ManagerRegistry;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Csv;
use PhpOffice\PhpSpreadsheet\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -41,6 +41,8 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@@ -58,7 +60,8 @@ final class EventController extends AbstractController
private readonly TranslatorInterface $translator,
private readonly PaginatorFactory $paginator,
private readonly Security $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
private readonly ManagerRegistry $managerRegistry,
private readonly NormalizerInterface $normalizer,
) {}
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])]
@@ -75,6 +78,7 @@ final class EventController extends AbstractController
/** @var array $participations */
$participations = $event->getParticipations();
$budgetElements = $event->getBudgetElements();
$form = $this->createDeleteForm($event_id);
@@ -86,6 +90,10 @@ final class EventController extends AbstractController
$em->remove($participation);
}
foreach ($budgetElements as $e) {
$em->remove($e);
}
$em->remove($event);
$em->flush();
@@ -103,7 +111,7 @@ final class EventController extends AbstractController
}
return $this->render('@ChillEvent/Event/confirm_delete.html.twig', [
'event_id' => $event->getId(),
'id' => $event->getId(),
'delete_form' => $form->createView(),
]);
}
@@ -169,6 +177,8 @@ final class EventController extends AbstractController
/**
* Displays a form to create a new Event entity.
*
* @throws ExceptionInterface
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new', name: 'chill_event__event_new', methods: ['GET', 'POST'])]
public function newAction(?Center $center, Request $request): Response
@@ -199,26 +209,23 @@ final class EventController extends AbstractController
$this->addFlash('success', $this->translator
->trans('The event was created'));
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $entity->getId()]);
return $this->redirectToRoute('chill_event__event_show', ['id' => $entity->getId()]);
}
$entity_array = $this->normalizer->normalize($entity, 'json', ['groups' => 'read']);
return $this->render('@ChillEvent/Event/new.html.twig', [
'entity' => $entity,
'form' => $form->createView(),
'entity_json' => $entity_array,
]);
}
/**
* First step of new Event form.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/new/pick-center', name: 'chill_event__event_new_pickcenter', options: [null])]
public function newPickCenterAction(): Response
{
$role = 'CHILL_EVENT_CREATE';
/**
* @var Center $centers
*/
$centers = $this->authorizationHelper->getReachableCenters($this->getUser(), $role);
if (1 === \count($centers)) {
@@ -238,7 +245,7 @@ final class EventController extends AbstractController
->add('center_id', EntityType::class, [
'class' => Center::class,
'choices' => $centers,
'placeholder' => '',
'placeholder' => $this->translator->trans('Pick a center'),
'label' => 'To which centre should the event be associated ?',
])
->add('submit', SubmitType::class, [
@@ -251,16 +258,7 @@ final class EventController extends AbstractController
]);
}
/**
* Finds and displays a Event entity.
*
* @ParamConverter("event", options={"id": "event_id"})
*
* @return Response
*
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/show', name: 'chill_event__event_show')]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{id}/show', name: 'chill_event__event_show')]
public function showAction(Event $event, Request $request)
{
if (!$event) {
@@ -317,7 +315,7 @@ final class EventController extends AbstractController
$this->addFlash('success', $this->translator->trans('The event was updated'));
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
return $this->redirectToRoute('chill_event__event_show', ['id' => $event_id]);
}
return $this->render('@ChillEvent/Event/edit.html.twig', [

View File

@@ -15,11 +15,15 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Repository\EventACLAwareRepositoryInterface;
use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormFactoryInterface;
@@ -29,17 +33,18 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
final readonly class EventListController
final class EventListController extends AbstractController
{
public function __construct(
private Environment $environment,
private EventACLAwareRepositoryInterface $eventACLAwareRepository,
private EventTypeRepository $eventTypeRepository,
private FilterOrderHelperFactory $filterOrderHelperFactory,
private FormFactoryInterface $formFactory,
private PaginatorFactoryInterface $paginatorFactory,
private TranslatableStringHelperInterface $translatableStringHelper,
private UrlGeneratorInterface $urlGenerator,
private readonly Environment $environment,
private readonly EventACLAwareRepositoryInterface $eventACLAwareRepository,
private readonly EventTypeRepository $eventTypeRepository,
private readonly FilterOrderHelperFactory $filterOrderHelperFactory,
private readonly FormFactoryInterface $formFactory,
private readonly PaginatorFactoryInterface $paginatorFactory,
private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly AuthorizationHelper $authorizationHelper,
) {}
#[Route(path: '{_locale}/event/event/list', name: 'chill_event_event_list')]
@@ -50,6 +55,8 @@ final readonly class EventListController
'q' => (string) $filter->getQueryString(),
'dates' => $filter->getDateRangeData('dates'),
'event_types' => $filter->getEntityChoiceData('event_types'),
'responsables' => $filter->getUserPickerData('responsables'),
'centers' => $filter->getEntityChoiceData('centers'),
];
$total = $this->eventACLAwareRepository->countAllViewable($filterData);
$pagination = $this->paginatorFactory->create($total);
@@ -73,6 +80,7 @@ final readonly class EventListController
private function buildFilterOrder(): FilterOrderHelper
{
$types = $this->eventTypeRepository->findAllActive();
$centers = $this->authorizationHelper->getReachableCenters($this->getUser(), EventVoter::SEE);
$builder = $this->filterOrderHelperFactory->create(__METHOD__);
$builder
@@ -80,6 +88,16 @@ final readonly class EventListController
->addSearchBox(['name'])
->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [
'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()),
'expanded' => false,
'required' => false,
'attr' => ['class' => 'select2'],
])
->addUserPicker('responsables', 'event.filter.pick_responsable', ['multiple' => true, 'required' => false])
->addEntityChoice('centers', 'event.filter.center', Center::class, $centers, [
'choice_label' => fn (Center $c) => $c->getName(),
'expanded' => false,
'required' => false,
'attr' => ['class' => 'select2'],
]);
return $builder->build();

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
class EventThemeController extends CRUDController
{
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
{
if ('new' === $action) {
return parent::createFormFor($action, $entity, $formClass, ['step' => 'create']);
}
if ('edit' === $action) {
return parent::createFormFor($action, $entity, $formClass, ['step' => 'edit']);
}
throw new \LogicException('action is not supported: '.$action);
}
/**
* @param QueryBuilder|mixed $query
*/
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator): QueryBuilder
{
/* @var QueryBuilder $query */
return $query->orderBy('e.ordering', 'ASC')
->addOrderBy('e.id', 'ASC');
}
}

View File

@@ -228,7 +228,7 @@ final class ParticipationController extends AbstractController
}
return $this->redirectToRoute('chill_event__event_show', [
'event_id' => $participation->getEvent()->getId(),
'id' => $participation->getEvent()->getId(),
]);
}
@@ -242,7 +242,7 @@ final class ParticipationController extends AbstractController
/**
* @param int $participation_id
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/delete', name: 'chill_event_participation_delete', requirements: ['participation_id' => '\d+'], methods: ['GET', 'DELETE'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/delete', name: 'chill_event_participation_delete', requirements: ['participation_id' => '\d+'])]
public function deleteAction($participation_id, Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
{
$em = $this->managerRegistry->getManager();
@@ -273,7 +273,7 @@ final class ParticipationController extends AbstractController
);
return $this->redirectToRoute('chill_event__event_show', [
'event_id' => $event->getId(),
'id' => $event->getId(),
]);
}
}
@@ -442,7 +442,7 @@ final class ParticipationController extends AbstractController
));
return $this->redirectToRoute('chill_event__event_show', [
'event_id' => $participation->getEvent()->getId(),
'id' => $participation->getEvent()->getId(),
]);
}

View File

@@ -11,6 +11,12 @@ declare(strict_types=1);
namespace Chill\EventBundle\DependencyInjection;
use Chill\EventBundle\Controller\Admin\EventBudgetKindController;
use Chill\EventBundle\Controller\EventThemeController;
use Chill\EventBundle\Entity\EventBudgetKind;
use Chill\EventBundle\Entity\EventTheme;
use Chill\EventBundle\Form\EventBudgetKindType;
use Chill\EventBundle\Form\EventThemeType;
use Chill\EventBundle\Security\EventVoter;
use Chill\EventBundle\Security\ParticipationVoter;
use Symfony\Component\Config\FileLocator;
@@ -26,7 +32,10 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
*/
class ChillEventExtension extends Extension implements PrependExtensionInterface
{
public function load(array $configs, ContainerBuilder $container)
/**
* @throws \Exception
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
@@ -45,16 +54,17 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
/** (non-PHPdoc).
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/
public function prepend(ContainerBuilder $container)
public function prepend(ContainerBuilder $container): void
{
$this->prependAuthorization($container);
$this->prependCruds($container);
$this->prependRoute($container);
}
/**
* add authorization hierarchy.
*/
protected function prependAuthorization(ContainerBuilder $container)
protected function prependAuthorization(ContainerBuilder $container): void
{
$container->prependExtensionConfig('security', [
'role_hierarchy' => [
@@ -70,7 +80,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
/**
* add route to route loader for chill.
*/
protected function prependRoute(ContainerBuilder $container)
protected function prependRoute(ContainerBuilder $container): void
{
// add routes for custom bundle
$container->prependExtensionConfig('chill_main', [
@@ -81,4 +91,54 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
],
]);
}
protected function prependCruds(ContainerBuilder $container): void
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
[
'class' => EventTheme::class,
'name' => 'event_theme',
'base_path' => '/admin/event/theme',
'form_class' => EventThemeType::class,
'controller' => EventThemeController::class,
'actions' => [
'index' => [
'template' => '@ChillEvent/Admin/EventTheme/index.html.twig',
'role' => 'ROLE_ADMIN',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/EventTheme/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/EventTheme/edit.html.twig',
],
],
],
[
'class' => EventBudgetKind::class,
'name' => 'event_budget_kind',
'base_path' => '/admin/event/budget',
'form_class' => EventBudgetKindType::class,
'controller' => EventBudgetKindController::class,
'actions' => [
'index' => [
'template' => '@ChillEvent/Admin/BudgetKind/index.html.twig',
'role' => 'ROLE_ADMIN',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/BudgetKind/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/BudgetKind/edit.html.twig',
],
],
],
],
]);
}
}

View File

@@ -0,0 +1,18 @@
<?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\EventBundle\Entity;
enum BudgetTypeEnum: string
{
case CHARGE = 'Charge';
case RESOURCE = 'Resource';
}

View File

@@ -23,10 +23,13 @@ use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* Class Event.
@@ -46,35 +49,63 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
#[ORM\ManyToOne(targetEntity: Scope::class)]
private ?Scope $circle = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE)]
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTime $date = null;
#[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
private ?User $moderator = null;
/**
* @var Collection<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_animatorsintern')]
private Collection $animatorsIntern;
/**
* @var Collection<int, ThirdParty>
*/
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_animatorsextern')]
private Collection $animatorsExtern;
#[Assert\NotBlank]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 150)]
#[Serializer\Groups(['read'])]
#[ORM\Column(type: Types::STRING, length: 150)]
private ?string $name = null;
/**
* @var Collection<int, Participation>
*/
#[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)]
#[Serializer\Groups(['read'])]
private Collection $participations;
#[Assert\NotNull]
#[Serializer\Groups(['read'])]
#[ORM\ManyToOne(targetEntity: EventType::class)]
private ?EventType $type = null;
/**
* @var Collection<int, EventTheme>
*/
#[ORM\ManyToMany(targetEntity: EventTheme::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinTable('chill_event_eventtheme')]
private Collection $themes;
#[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_')]
private CommentEmbeddable $comment;
#[ORM\ManyToOne(targetEntity: Location::class)]
#[Serializer\Groups(['read'])]
#[ORM\JoinColumn(nullable: true)]
private ?Location $location = null;
@@ -85,7 +116,17 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
#[ORM\JoinTable('chill_event_event_documents')]
private Collection $documents;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DECIMAL, precision: 10, scale: 4, nullable: true, options: ['default' => '0.0'])]
/**
* @var Collection<int, EventBudgetElement>
*/
#[ORM\OneToMany(mappedBy: 'event', targetEntity: EventBudgetElement::class, cascade: ['persist'])]
#[Serializer\Groups(['read'])]
private Collection $budgetElements;
/**
* @deprecated use budgetElements instead
*/
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 4, nullable: true, options: ['default' => '0.0'])]
private string $organizationCost = '0.0';
/**
@@ -96,6 +137,20 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
$this->participations = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->comment = new CommentEmbeddable();
$this->themes = new ArrayCollection();
$this->budgetElements = new ArrayCollection();
$this->animatorsIntern = new ArrayCollection();
$this->animatorsExtern = new ArrayCollection();
}
public function addBudgetElement(EventBudgetElement $budgetElement)
{
if (!$this->budgetElements->contains($budgetElement)) {
$this->budgetElements[] = $budgetElement;
$budgetElement->setEvent($this);
}
return $this;
}
/**
@@ -126,38 +181,79 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
return $this;
}
/**
* @return Center
*/
public function getCenter()
public function getThemes(): Collection
{
return $this->themes;
}
public function addTheme(EventTheme $theme): self
{
$this->themes->add($theme);
return $this;
}
public function removeTheme(EventTheme $theme): void
{
$this->themes->removeElement($theme);
}
public function getAnimatorsIntern(): Collection
{
return $this->animatorsIntern;
}
public function getAnimatorsExtern(): Collection
{
return $this->animatorsExtern;
}
public function addAnimatorsIntern(User $ai): self
{
$this->animatorsIntern->add($ai);
return $this;
}
public function removeAnimatorsIntern(User $ai): void
{
$this->animatorsIntern->removeElement($ai);
}
public function addAnimatorsExtern(ThirdParty $ae): self
{
$this->animatorsExtern->add($ae);
return $this;
}
public function removeAnimatorsExtern(ThirdParty $ae): void
{
$this->animatorsExtern->removeElement($ae);
}
public function getCenter(): Center
{
return $this->center;
}
/**
* @return Scope
*/
public function getCircle()
public function getCircle(): ?Scope
{
return $this->circle;
}
/**
* Get date.
*
* @return \DateTime
*/
public function getDate()
public function getDate(): ?\DateTime
{
return $this->date;
}
/**
* Get id.
*
* @return int
*/
public function getId()
public function getId(): ?int
{
return $this->id;
}
@@ -169,14 +265,20 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/**
* Get label.
*
* @return string
*/
public function getName()
public function getName(): ?string
{
return $this->name;
}
/**
* @return Collection<int, EventBudgetElement>
*/
public function getBudgetElements(): Collection
{
return $this->budgetElements;
}
/**
* @return Collection<int, Participation>
*/
@@ -199,26 +301,26 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/**
* @deprecated
*
* @return Scope
*/
public function getScope()
public function getScope(): Scope
{
return $this->getCircle();
}
/**
* @return EventType
*/
public function getType()
public function getType(): ?EventType
{
return $this->type;
}
public function removeBudgetElement(EventBudgetElement $budgetElement): void
{
$this->budgetElements->removeElement($budgetElement);
}
/**
* Remove participation.
*/
public function removeParticipation(Participation $participation)
public function removeParticipation(Participation $participation): void
{
$this->participations->removeElement($participation);
}
@@ -314,11 +416,17 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
$this->documents = $documents;
}
/**
* @deprecated
*/
public function getOrganizationCost(): string
{
return $this->organizationCost;
}
/**
* @deprecated
*/
public function setOrganizationCost(string $organizationCost): void
{
$this->organizationCost = $organizationCost;

View File

@@ -0,0 +1,103 @@
<?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\EventBundle\Entity;
use Chill\EventBundle\Repository\EventThemeRepository;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: EventThemeRepository::class)]
#[ORM\Table(name: 'chill_event_budget_element')]
class EventBudgetElement
{
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[Assert\GreaterThan(value: 0)]
#[Assert\NotNull(message: 'The amount cannot be empty')]
#[ORM\Column(name: 'amount', type: Types::DECIMAL, precision: 10, scale: 2)]
private string $amount;
#[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_budget_element_')]
private ?CommentEmbeddable $comment = null;
#[ORM\ManyToOne(targetEntity: Event::class)]
private Event $event;
#[ORM\ManyToOne(targetEntity: EventBudgetKind::class, inversedBy: 'EventBudgetElement')]
#[ORM\JoinColumn]
private EventBudgetKind $kind;
/* Getters and Setters */
public function getId(): ?int
{
return $this->id;
}
public function setId(?int $id): void
{
$this->id = $id;
}
public function getAmount(): float
{
return (float) $this->amount;
}
public function getComment(): ?CommentEmbeddable
{
return $this->comment;
}
public function getEvent(): Event
{
return $this->event;
}
public function getKind(): EventBudgetKind
{
return $this->kind;
}
public function setAmount(string $amount): self
{
$this->amount = $amount;
return $this;
}
public function setComment(?CommentEmbeddable $comment = null): self
{
$this->comment = $comment;
return $this;
}
public function setEvent(Event $event): self
{
$this->event = $event;
return $this;
}
public function setKind(EventBudgetKind $kind): self
{
$this->kind = $kind;
return $this;
}
}

View File

@@ -0,0 +1,78 @@
<?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\EventBundle\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Type of event budget element.
*/
#[ORM\Entity]
#[ORM\Table(name: 'chill_event_budget_kind')]
class EventBudgetKind
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => true])]
private bool $isActive = true;
#[ORM\Column(enumType: BudgetTypeEnum::class)]
private BudgetTypeEnum $type;
#[ORM\Column(type: Types::JSON, length: 255, options: ['default' => '{}', 'jsonb' => true])]
private array $name = [];
public function getId(): ?int
{
return $this->id;
}
public function getIsActive(): bool
{
return $this->isActive;
}
public function getType(): BudgetTypeEnum
{
return $this->type;
}
public function getName(): ?array
{
return $this->name;
}
public function setIsActive(bool $isActive): self
{
$this->isActive = $isActive;
return $this;
}
public function setType(BudgetTypeEnum $type): self
{
$this->type = $type;
return $this;
}
public function setName(array $name): self
{
$this->name = $name;
return $this;
}
}

View File

@@ -0,0 +1,158 @@
<?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\EventBundle\Entity;
use Chill\EventBundle\Repository\EventThemeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Class EventTheme.
*/
#[ORM\HasLifecycleCallbacks]
#[ORM\Entity(repositoryClass: EventThemeRepository::class)]
#[ORM\Table(name: 'chill_event_event_theme')]
class EventTheme
{
#[ORM\Column(type: Types::BOOLEAN, nullable: false)]
private bool $isActive = true;
#[ORM\Id]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[ORM\Column(type: Types::JSON)]
private array $name;
/**
* @var Collection<int, EventTheme>
*/
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: EventTheme::class)]
private Collection $children;
#[ORM\ManyToOne(targetEntity: EventTheme::class, inversedBy: 'children')]
private ?EventTheme $parent = null;
#[ORM\Column(name: 'ordering', type: Types::FLOAT, options: ['default' => '0.0'])]
private float $ordering = 0.0;
/**
* Constructor.
*/
public function __construct()
{
$this->children = new ArrayCollection();
}
/**
* Get active.
*/
public function getIsActive(): bool
{
return $this->isActive;
}
/**
* Get id.
*/
public function getId(): ?int
{
return $this->id;
}
/**
* Get label.
*/
public function getName(): array
{
return $this->name;
}
public function setIsActive(bool $active): self
{
$this->isActive = $active;
return $this;
}
public function setName(array $label): self
{
$this->name = $label;
return $this;
}
public function addChild(self $child): self
{
if (!$this->children->contains($child)) {
$this->children[] = $child;
}
return $this;
}
public function removeChild(self $child): self
{
if ($this->children->removeElement($child)) {
// set the owning side to null (unless already changed)
if ($child->getParent() === $this) {
$child->setParent(null);
}
}
return $this;
}
public function getChildren(): Collection
{
return $this->children;
}
public function hasChildren(): bool
{
return 0 < $this->getChildren()->count();
}
public function hasParent(): bool
{
return null !== $this->parent;
}
public function getOrdering(): float
{
return $this->ordering;
}
public function setOrdering(float $ordering): EventTheme
{
$this->ordering = $ordering;
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;
$parent?->addChild($this);
return $this;
}
}

View File

@@ -0,0 +1,377 @@
<?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\EventBundle\Export\Export;
use Chill\EventBundle\Entity\BudgetTypeEnum;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\EventBudgetElementRepository;
use Chill\EventBundle\Repository\EventThemeRepository;
use Chill\EventBundle\Security\EventVoter;
use Chill\EventBundle\Templating\Entity\EventThemeRender;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatableInterface;
/**
* Render a list of events.
*/
class ListEvents implements ListInterface, GroupedExportInterface
{
protected array $fields = [
'event_id',
'event_center',
'event_name',
'event_date',
'event_location',
'event_type',
'event_themes',
'event_moderator',
'event_animators',
'event_participants_count',
'event_budget_resources',
'event_budget_charges',
];
private readonly bool $filterStatsByCenters;
public function __construct(
protected readonly EntityManagerInterface $entityManager,
ParameterBagInterface $parameterBag,
protected readonly TranslatableStringHelperInterface $translatableStringHelper,
protected readonly EventThemeRender $eventThemeRender,
protected readonly EventThemeRepository $eventThemeRepository,
protected readonly LabelThirdPartyHelper $labelThirdPartyHelper,
protected readonly EventBudgetElementRepository $eventBudgetElementRepository,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('fields', ChoiceType::class, [
'multiple' => true,
'expanded' => true,
'choices' => array_combine($this->fields, $this->fields),
'label' => 'Fields to include in export',
'constraints' => [new Callback([
'callback' => static function ($selected, ExecutionContextInterface $context) {
if (0 === \count($selected)) {
$context->buildViolation('You must select at least one element')
->atPath('fields')
->addViolation();
}
},
])],
]);
}
public function getFormDefaultData(): array
{
return [
'fields' => $this->fields,
];
}
public function getAllowedFormattersTypes(): array
{
return [FormatterInterface::TYPE_LIST];
}
public function getDescription(): string
{
return 'export.event.list.description';
}
public function getGroup(): string
{
return 'Exports of events';
}
public function getLabels($key, array $values, $data)
{
return match ($key) {
'event_id' => fn ($value) => '_header' === $value ? $key : $value,
'event_name' => fn ($value) => '_header' === $value ? $key : $value,
'event_date' => function ($value) use ($key) {
if ('_header' === $value) {
return $key;
}
if ($value instanceof \DateTime) {
return $value->format('Y-m-d');
}
$date = \DateTime::createFromFormat('Y-m-d H:i:s', $value);
return $date ? $date->format('Y-m-d') : $value;
},
'event_type' => function ($value) use ($key) {
if ('_header' === $value) {
return 'export.event.list.'.$key;
}
return $this->translatableStringHelper->localize(json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR));
},
'event_center' => fn ($value) => '_header' === $value ? $key : $value,
'event_moderator' => fn ($value) => '_header' === $value ? $key : $value,
'event_participants_count' => fn ($value) => '_header' === $value ? $key : $value,
'event_location' => fn ($value) => '_header' === $value ? $key : $value,
'event_animators' => $this->labelThirdPartyHelper->getLabelMulti($key, $values, $key),
'event_themes' => function ($value) use ($key) {
if ('_header' === $value) {
return $key;
}
if (null === $value) {
return '';
}
return implode(
'|',
array_map(
fn ($t) => $this->eventThemeRender->renderString($this->eventThemeRepository->find($t), []),
json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR)
)
);
},
'event_budget_resources' => function ($value) use ($key) {
if ('_header' === $value) {
return $key;
}
if (!$value) {
return '';
}
$ids = explode(',', $value);
$ids = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
$elements = $this->eventBudgetElementRepository->findBy(['id' => $ids]);
return implode('|', array_map(function ($element) {
$name = $this->translatableStringHelper->localize($element->getKind()->getName());
$amount = number_format($element->getAmount(), 2, '.', '');
return $name.': '.$amount;
}, $elements));
},
'event_budget_charges' => function ($value) use ($key) {
if ('_header' === $value) {
return $key;
}
if (!$value) {
return '';
}
$ids = explode(',', $value);
$ids = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
$elements = $this->eventBudgetElementRepository->findBy(['id' => $ids]);
return implode('|', array_map(function ($element) {
$name = $this->translatableStringHelper->localize($element->getKind()->getName());
$amount = number_format($element->getAmount(), 2, '.', '');
return $name.': '.$amount;
}, $elements));
},
default => fn ($value) => '_header' === $value ? $key : $value,
};
}
public function getQueryKeys(array $data): array
{
return $data['fields'];
}
public function getResult($query, $data, ExportGenerationContext $context): array
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle(): string|TranslatableInterface
{
return 'export.event.list.title';
}
public function getType(): string
{
return Declarations::EVENT;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): NativeQuery|QueryBuilder
{
$centers = array_map(static fn ($el) => $el['center'], $acl);
// Throw an error if no fields are present
if (!\array_key_exists('fields', $data)) {
throw new \InvalidArgumentException('No fields have been checked.');
}
$qb = $this->entityManager->createQueryBuilder()
->from(Event::class, 'event');
if ($this->filterStatsByCenters) {
$qb
->andWhere('event.center IN (:authorized_centers)')
->setParameter('authorized_centers', $centers);
}
// Add fields based on selection
foreach ($this->fields as $field) {
if (\in_array($field, $data['fields'], true)) {
switch ($field) {
case 'event_id':
$qb->addSelect('event.id AS event_id');
break;
case 'event_name':
$qb->addSelect('event.name AS event_name');
break;
case 'event_date':
$qb->addSelect('event.date AS event_date');
break;
case 'event_type':
if (!$this->hasJoin($qb, 'event.type')) {
$qb->leftJoin('event.type', 'type');
}
$qb->addSelect('type.name AS event_type');
break;
case 'event_center':
if (!$this->hasJoin($qb, 'event.center')) {
$qb->leftJoin('event.center', 'center');
}
$qb->addSelect('center.name AS event_center');
break;
case 'event_moderator':
if (!$this->hasJoin($qb, 'event.moderator')) {
$qb->leftJoin('event.moderator', 'user');
}
$qb->addSelect('user.username AS event_moderator');
break;
case 'event_participants_count':
$qb->addSelect('(SELECT COUNT(p.id) FROM Chill\EventBundle\Entity\Participation p WHERE p.event = event.id) AS event_participants_count');
break;
case 'event_location':
if (!$this->hasJoin($qb, 'event.location')) {
$qb->leftJoin('event.location', 'location');
}
$qb->addSelect('location.name AS event_location');
break;
case 'event_animators':
$qb->addSelect(
'(SELECT AGGREGATE(tp.id) FROM Chill\ThirdPartyBundle\Entity\ThirdParty tp WHERE tp MEMBER OF event.animators) AS event_animators'
);
break;
case 'event_themes':
$qb->addSelect(
'(SELECT AGGREGATE(t.id) FROM Chill\EventBundle\Entity\EventTheme t WHERE t MEMBER OF event.themes) AS event_themes'
);
break;
case 'event_budget_resources':
$qb->addSelect(
'(SELECT AGGREGATE(ebr.id)
FROM Chill\EventBundle\Entity\EventBudgetElement ebr
JOIN ebr.kind kr
WHERE ebr.event = event.id AND kr.type = :resource_type) AS event_budget_resources'
);
$qb->setParameter('resource_type', BudgetTypeEnum::RESOURCE->value);
break;
case 'event_budget_charges':
$qb->addSelect(
'(SELECT AGGREGATE(ebc.id)
FROM Chill\EventBundle\Entity\EventBudgetElement ebc
JOIN ebc.kind kc
WHERE ebc.event = event.id AND kc.type = :charge_type) AS event_budget_charges'
);
$qb->setParameter('charge_type', BudgetTypeEnum::CHARGE->value);
break;
}
}
}
return $qb;
}
public function requiredRole(): string
{
return EventVoter::STATS;
}
public function supportsModifiers()
{
return [Declarations::EVENT];
}
/**
* Helper method to check if a join already exists in the QueryBuilder.
*/
private function hasJoin($queryBuilder, $joinPath): bool
{
$joins = $queryBuilder->getDQLPart('join');
if (!isset($joins['event'])) {
return false;
}
foreach ($joins['event'] as $join) {
if ($join->getJoin() === $joinPath) {
return true;
}
}
return false;
}
public function normalizeFormData(array $formData): array
{
return ['fields' => $formData['fields']];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
return ['fields' => $formData['fields']];
}
public function getNormalizationVersion(): int
{
return 1;
}
}

View File

@@ -0,0 +1,59 @@
<?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\EventBundle\Form;
use Chill\EventBundle\Entity\BudgetTypeEnum;
use Chill\EventBundle\Entity\EventBudgetElement;
use Chill\EventBundle\Entity\EventBudgetKind;
use Chill\EventBundle\Repository\EventBudgetKindRepository;
use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddEventBudgetElementType extends AbstractType
{
public function __construct(private readonly EventBudgetKindRepository $kindRepository, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$charges = $this->kindRepository->findByType(BudgetTypeEnum::CHARGE->value);
$resources = $this->kindRepository->findByType(BudgetTypeEnum::RESOURCE->value);
$builder->add('kind', ChoiceType::class, [
'choices' => [
'event.budget.charges' => $charges,
'event.budget.resources' => $resources,
],
'choice_label' => fn (EventBudgetKind $kind) => $this->translatableStringHelper->localize($kind->getName()),
'choice_value' => fn (?EventBudgetKind $kind) => $kind?->getId(),
'placeholder' => 'event.budget.Select a budget element kind',
])
->add('amount', NumberType::class, [
'required' => true,
])
->add('comment', CommentType::class, [
'label' => 'Comment',
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => EventBudgetElement::class,
]);
}
}

View File

@@ -0,0 +1,53 @@
<?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\EventBundle\Form;
use Chill\EventBundle\Entity\BudgetTypeEnum;
use Chill\EventBundle\Entity\EventBudgetKind;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class EventBudgetKindType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TranslatableStringFormType::class, [
'label' => 'Title',
])
->add('type', EnumType::class, [
'class' => BudgetTypeEnum::class,
'choice_label' => fn (BudgetTypeEnum $type): string => $this->translator->trans($type->value),
'expanded' => true,
'multiple' => false,
'mapped' => true,
'label' => 'event.admin.Select budget type',
])
->add('isActive', CheckboxType::class, [
'label' => 'Actif ?',
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('class', EventBudgetKind::class);
}
}

View File

@@ -0,0 +1,67 @@
<?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\EventBundle\Form;
use Chill\EventBundle\Entity\EventTheme;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\AbstractType;
class EventThemeType extends AbstractType
{
public function __construct(protected TranslatableStringHelperInterface $translatableStringHelper) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TranslatableStringFormType::class, [
'label' => 'Nom',
]);
if ('create' === $options['step']) {
$builder
->add('parent', EntityType::class, [
'class' => EventTheme::class,
'required' => false,
'choice_label' => fn (EventTheme $theme): ?string => $this->translatableStringHelper->localize($theme->getName()),
'mapped' => 'create' == $options['step'],
]);
}
$builder
->add('ordering', NumberType::class, [
'required' => true,
'scale' => 6,
])
->add('isActive', ChoiceType::class, [
'choices' => [
'Yes' => true,
'No' => false,
],
'expanded' => true,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => EventTheme::class,
]);
$resolver->setRequired('step')
->setAllowedValues('step', ['create', 'edit']);
}
}

View File

@@ -13,25 +13,39 @@ namespace Chill\EventBundle\Form;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Form\StoredObjectType;
use Chill\EventBundle\Entity\BudgetTypeEnum;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Form\Type\PickEventThemeType;
use Chill\EventBundle\Form\Type\PickEventTypeType;
use Chill\EventBundle\Repository\EventBudgetKindRepository;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillDateTimeType;
use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\PickUserLocationType;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class EventType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function __construct(
private readonly IdToLocationDataTransformer $idToLocationDataTransformer,
private readonly EventBudgetKindRepository $eventBudgetKindRepository,
private readonly TranslatorInterface $translator,
) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$chargeKinds = $this->eventBudgetKindRepository->findByType(BudgetTypeEnum::CHARGE->value);
$resourceKinds = $this->eventBudgetKindRepository->findByType(BudgetTypeEnum::RESOURCE->value);
$builder
->add('name', TextType::class, [
'required' => true,
@@ -49,11 +63,28 @@ class EventType extends AbstractType
'class' => '',
],
])
->add('themes', PickEventThemeType::class, [
'multiple' => true,
])
->add('moderator', PickUserDynamicType::class, [
'label' => 'Pick a moderator',
])
->add('location', PickUserLocationType::class, [
'label' => 'event.fields.location',
->add('animatorsIntern', PickUserDynamicType::class, [
'multiple' => true,
'label' => $this->translator->trans('event.fields.internal animators'),
'required' => false,
])
->add('animatorsExtern', PickThirdpartyDynamicType::class, [
'multiple' => true,
'label' => $this->translator->trans('event.fields.external animators'),
'required' => false,
])
->add('budgetElements', ChillCollectionType::class, [
'entry_type' => AddEventBudgetElementType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
])
->add('comment', CommentType::class, [
'label' => 'Comment',
@@ -69,11 +100,11 @@ class EventType extends AbstractType
'delete_empty' => fn (StoredObject $storedObject): bool => '' === $storedObject->getFilename(),
'button_remove_label' => 'event.form.remove_document',
'button_add_label' => 'event.form.add_document',
])
->add('organizationCost', MoneyType::class, [
'label' => 'event.fields.organizationCost',
'help' => 'event.form.organisationCost_help',
]);
$builder->add('location', HiddenType::class)
->get('location')
->addModelTransformer($this->idToLocationDataTransformer);
}
public function configureOptions(OptionsResolver $resolver)
@@ -87,11 +118,9 @@ class EventType extends AbstractType
->setAllowedTypes('role', 'string');
}
/**
* @return string
*/
public function getBlockPrefix()
public function getBlockPrefix(): string
{
return 'chill_eventbundle_event';
// as the js shares some hardcoded items from the activity bundle, we have to rewrite block prefix
return 'chill_activitybundle_activity';
}
}

View File

@@ -0,0 +1,45 @@
<?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\EventBundle\Form\Type;
use Chill\EventBundle\Entity\EventTheme;
use Chill\EventBundle\Repository\EventThemeRepository;
use Chill\EventBundle\Templating\Entity\EventThemeRender;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PickEventThemeType extends AbstractType
{
public function __construct(private readonly EventThemeRender $eventThemeRender, private readonly EventThemeRepository $eventThemeRepository) {}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'class' => EventTheme::class,
'choices' => $this->eventThemeRepository->findByActiveOrdered(),
'choice_label' => fn (EventTheme $et) => $this->eventThemeRender->renderString($et, []),
'placeholder' => 'event.form.Select one or more themes',
'required' => true,
'attr' => ['class' => 'select2'],
'label' => 'event.theme.label',
'multiple' => false,
])
->setAllowedTypes('multiple', ['bool']);
}
public function getParent(): string
{
return EntityType::class;
}
}

View File

@@ -23,15 +23,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
class PickEventTypeType extends AbstractType
{
/**
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
public function __construct(TranslatableStringHelper $helper)
{
$this->translatableStringHelper = $helper;
}
public function __construct(protected TranslatableStringHelper $translatableStringHelper) {}
public function configureOptions(OptionsResolver $resolver)
{

View File

@@ -17,17 +17,9 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class AdminMenuBuilder implements LocalMenuBuilderInterface
{
/**
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
public function __construct(protected AuthorizationCheckerInterface $authorizationChecker) {}
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{
if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return;
@@ -52,6 +44,14 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface
$menu->addChild('Role', [
'route' => 'chill_event_admin_role',
])->setExtras(['order' => 6530]);
$menu->addChild('event.theme.label', [
'route' => 'chill_crud_event_theme_index',
])->setExtras(['order' => 6540]);
$menu->addChild('event.budget.label', [
'route' => 'chill_crud_event_budget_kind_index',
])->setExtras(['order' => 6550]);
}
public static function getMenuIds(): array

View File

@@ -88,6 +88,16 @@ final readonly class EventACLAwareRepository implements EventACLAwareRepositoryI
$qb->andWhere('event.type IN (:event_types)');
$qb->setParameter('event_types', $filters['event_types']);
}
if (0 < count($filters['centers'] ?? [])) {
$qb->andWhere('event.center IN (:centers)');
$qb->setParameter('centers', $filters['centers']);
}
if (0 < count($filters['responsables'] ?? [])) {
$qb->andWhere('event.moderator IN (:responsables)');
$qb->setParameter('responsables', $filters['responsables']);
}
}
public function buildQueryByAllViewable(array $filters): QueryBuilder

View File

@@ -0,0 +1,27 @@
<?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\EventBundle\Repository;
use Chill\EventBundle\Entity\EventBudgetElement;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<EventBudgetElement>
*/
class EventBudgetElementRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, EventBudgetElement::class);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Repository;
use Chill\EventBundle\Entity\EventBudgetKind;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<EventBudgetKind>
*/
class EventBudgetKindRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, EventBudgetKind::class);
}
public function findByActive(): array
{
return $this->createQueryBuilder('e')
->select('e')
->where('e.active = True')
->getQuery()
->getResult();
}
public function findByType(string $type): array
{
return $this->createQueryBuilder('e')
->select('e')
->where('e.type = :type')
->setParameter('type', $type)
->getQuery()
->getResult();
}
}

View File

@@ -0,0 +1,37 @@
<?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\EventBundle\Repository;
use Chill\EventBundle\Entity\EventTheme;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<EventTheme>
*/
class EventThemeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, EventTheme::class);
}
public function findByActiveOrdered(): array
{
return $this->createQueryBuilder('t')
->select('t')
->where('t.isActive = True')
->orderBy('t.ordering', 'ASC')
->getQuery()
->getResult();
}
}

View File

@@ -55,3 +55,13 @@ form#export_tableur {
-webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.participation-list {
flex-wrap: wrap;
}
.participations-wrapper {
background-color: whitesmoke;
padding: 1rem;
margin-bottom: .5rem;
}

View File

@@ -0,0 +1,14 @@
<template>
<location />
</template>
<script>
import Location from "ChillActivityAssets/vuejs/Activity/components/Location.vue";
export default {
name: "App",
components: {
Location,
},
};
</script>

View File

@@ -0,0 +1,6 @@
import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";
createApp(App).use(store).mount("#event");

View File

@@ -0,0 +1,76 @@
import "es6-promise/auto";
import { createStore } from "vuex";
import prepareLocations from "ChillActivityAssets/vuejs/Activity/store.locations";
import { whoami } from "ChillMainAssets/lib/api/user";
import { postLocation } from "ChillActivityAssets/vuejs/Activity/api";
const debug = process.env.NODE_ENV !== "production";
const store = createStore({
strict: debug,
state: {
activity: window.entity, // activity is the event entity in this case (re-using component from activity bundle)
currentEvent: null,
availableLocations: [],
me: null,
},
getters: {},
actions: {
addAvailableLocationGroup({ commit }, payload) {
commit("addAvailableLocationGroup", 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);
},
},
mutations: {
setWhoAmiI(state, me) {
state.me = me;
},
addAvailableLocationGroup(state, group) {
state.availableLocations.push(group);
},
updateLocation(state, value) {
// console.log("### mutation: updateLocation", value);
state.activity.location = value;
},
},
});
whoami().then((me) => {
store.commit("setWhoAmiI", me);
});
prepareLocations(store);
export default store;

View File

@@ -0,0 +1,12 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block content_form_actions_view %}{% endblock %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
{% block title %}{{ 'event.admin.title.Event budget element list'|trans }}{% endblock title %}
{% block admin_content %}
<h1>{{ 'event.admin.title.Event budget element list'|trans }}</h1>
<table class="records_list table table-bordered border-dark">
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
<th>{{ 'Type'|trans }}</th>
<th>{{ 'Active'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for entity in entities %}
<tr>
<td>{{ entity.name|localize_translatable_string }}</td>
<td>{{ entity.type.value|trans }}</td>
<td style="text-align:center;">
{%- if entity.isActive -%}
<i class="fa fa-check-square-o"></i>
{%- else -%}
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_crud_event_budget_kind_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ chill_pagination(paginator) }}
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ path('chill_crud_event_budget_kind_new') }}" class="btn btn-create">
{{ 'event.admin.new.Create a new budget kind'|trans }}
</a>
</li>
</ul>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block crud_content_form_rows %}
{{ form_row(form.name) }}
<div class="mb-3 row">
<label class="col-form-label col-sm-4">
{{ 'Parent'|trans }}
</label>
<div class="col-sm-8">
{{ entity.parent|chill_entity_render_box }}
</div>
</div>
{{ form_row(form.ordering) }}
{{ form_row(form.isActive) }}
{% endblock crud_content_form_rows %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock admin_content %}

View File

@@ -0,0 +1,45 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block table_entities_thead_tr %}
<th>{{ 'Id'|trans }}</th>
<th>{{ 'Title'|trans }}</th>
<th>{{ 'Ordering'|trans }}</th>
<th>{{ 'active'|trans }}</th>
<th>&nbsp;</th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
<td>{{ entity.id }}</td>
<td>
{{ entity|chill_entity_render_box }}
</td>
<td>{{ entity.ordering }}</td>
<td style="text-align:center;">
{%- if entity.isActive -%}
<i class="fa fa-check-square-o"></i>
{%- else -%}
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_crud_event_theme_edit', { 'id': entity.id }) }}" class="btn btn-edit"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
{% endblock %}
{% block actions_before %}
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock admin_content %}

View File

@@ -0,0 +1,13 @@
{% set reversed_parents = parents|reverse %}
<span class="chill-entity entity-event-theme">
<span class="badge bg-chill-l-gray text-dark">
{%- for p in reversed_parents %}
<span class="parent-{{ loop.revindex0 }}">
{{ p.name|localize_translatable_string }}{{ options['default.separator'] }}
</span>
{%- endfor -%}
<span class="child">
{{ eventTheme.name|localize_translatable_string }}
</span>
</span>
</span>

View File

@@ -12,7 +12,7 @@
'title' : 'Delete event'|trans,
'confirm_question' : 'Are you sure you want to remove that event ?'|trans,
'cancel_route' : activeRouteKey,
'cancel_parameters' : { 'event_id' : event_id },
'cancel_parameters' : { 'id' : id },
'form' : delete_form
}
) }}

View File

@@ -17,10 +17,12 @@
{{ form_row(edit_form.date) }}
{{ form_row(edit_form.type, { label: "Event type" }) }}
{{ form_row(edit_form.themes) }}
{{ form_row(edit_form.moderator) }}
{{ form_row(edit_form.animatorsIntern) }}
{{ form_row(edit_form.animatorsExtern) }}
{{ form_row(edit_form.location) }}
{{ form_row(edit_form.organizationCost) }}
{{ form_row(edit_form.budgetElements) }}
{{ form_row(edit_form.comment) }}
{{ form_row(edit_form.documents) }}

View File

@@ -34,7 +34,7 @@
<ul class="record_actions">
<li>
{# {% if is_granted('CHILL_EVENT_SEE_DETAILS', event) %} #}
<a href="{{ path('chill_event__event_show', { 'event_id' : event.id } ) }}" class="btn btn-view">
<a href="{{ path('chill_event__event_show', { 'id' : event.id } ) }}" class="btn btn-view">
{{ 'See'|trans }}
</a>
{# {% endif %} #}

View File

@@ -1,25 +1,30 @@
{% extends '@ChillEvent/layout.html.twig' %} {% block js %}
{{ encore_entry_script_tags("mod_async_upload") }}
{{ encore_entry_script_tags("mod_pickentity_type") }}
{% extends '@ChillEvent/layout.html.twig' %}
{% endblock %} {% block css %}
{% block css %}
{{ encore_entry_link_tags("mod_async_upload") }}
{{ encore_entry_link_tags("mod_pickentity_type") }}
{{ encore_entry_link_tags('vue_event') }}
{% endblock %}
{% endblock %} {% block title 'Event creation'|trans %} {% block event_content
-%}
{% block title 'Event creation'|trans %}
{% block event_content -%}
<div class="col-10">
<h1>{{ "Event creation" | trans }}</h1>
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_row(form.circle) }}
{{ form_row(form.name) }}
{{ form_row(form.circle) }}
{{ form_row(form.date) }}
{{ form_row(form.type, { label: "Event type" }) }}
{{ form_row(form.themes) }}
{{ form_row(form.moderator) }}
{{ form_row(form.animatorsIntern) }}
{{ form_row(form.animatorsExtern) }}
{{ form_row(form.location) }}
{{ form_row(form.organizationCost) }}
<div id="location"></div>
{{ form_row(form.budgetElements) }}
{{ form_row(form.comment) }}
{{ form_row(form.documents) }}
@@ -40,5 +45,18 @@
</ul>
{{ form_end(form) }}
<div id="event"></div>
</div>
{% endblock %}
{% block js %}
{{ encore_entry_script_tags("mod_async_upload") }}
{{ encore_entry_script_tags("mod_pickentity_type") }}
{{ encore_entry_script_tags('vue_event') }}
<script type="text/javascript">
window.entity = {{ entity_json|json_encode|raw }};
{% if app.user.currentLocation is not null %}window.default_location_id = {{ app.user.currentLocation.id }};{% endif %}
</script>
{% endblock %}

View File

@@ -11,7 +11,7 @@ block js %}
{{ filter | chill_render_filter_order_helper }}
{# {% if is_granted('CHILL_EVENT_CREATE') %} #}
{% if is_granted('CHILL_EVENT_CREATE') %}
<ul class="record_actions">
<li>
<a
@@ -25,7 +25,9 @@ block js %}
>
</li>
</ul>
{# {% endif %} #} {% if events|length > 0 %}
{% endif %}
{% if events|length > 0 %}
<div class="flex-table">
{% for e in events %}
<div class="item-bloc">
@@ -41,6 +43,12 @@ block js %}
{{ e.moderator | chill_entity_render_box }}
</p>
{% endif %}
<div>
{% for t in e.themes %}
<span>{{ t|chill_entity_render_box }}</span>
{% endfor %}
</div>
</div>
<div class="item-col">
<div class="container" style="text-align: right">
@@ -48,20 +56,21 @@ block js %}
<p>
{{ 'count participations to this event'|trans({'count': e.participations|length}) }}
</p>
<p>{{ "center"|trans }}: {{ e.center.name }}</p>
</div>
</div>
</div>
{% if e.participations|length > 0 %}
<div class="item-row separator">
<div class="participation-list item-row separator">
<strong>{{ "Participations" | trans }}&nbsp;: </strong>
{% for part in e.participations|slice(0, 20) %} {% include
{% for part in e.participations|slice(0, 5) %} {% include
'@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: part.person.id }, action:
'show', displayBadge: true, buttonText:
part.person|chill_entity_render_string, isDead:
part.person.deathdate is not null } %} {% endfor %} {% if
e.participations|length > 20 %}
{{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 20}) }}
part.person.deathdate is not null } %} {% endfor %}
{% if e.participations|length > 5 %}
{{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 5}) }}
{% endif %}
</div>
{% endif %}
@@ -106,7 +115,7 @@ block js %}
href="{{
chill_path_add_return_path(
'chill_event__event_show',
{ event_id: e.id }
{ id: e.id }
)
}}"
class="btn btn-show"

View File

@@ -16,6 +16,16 @@
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
{% endblock %}
{% macro insert_onthefly(type, entity, parent = null) %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: type, id: entity.id },
buttonText: entity|chill_entity_render_string,
isDead: entity.deathdate is defined and entity.deathdate is not null,
parent: parent
} %}
{% endmacro %}
{% block event_content -%}
<div class="col-10">
<h1>{{ 'Details of an event'|trans }}</h1>
@@ -26,6 +36,10 @@
<th>{{ 'Circle'|trans }}</th>
<td>{{ event.circle.name|localize_translatable_string }}</td>
</tr>
<tr>
<th>{{ 'Center'|trans }}</th>
<td>{{ event.center.name }}</td>
</tr>
<tr>
<th>{{ 'Name'|trans }}</th>
<td>{{ event.name }}</td>
@@ -39,12 +53,32 @@
<td>{{ event.type.name|localize_translatable_string }}</td>
</tr>
<tr>
<th>{{ 'Moderator'|trans }}</th>
<td>{{ event.moderator|trans|default('-') }}</td>
<th>{{ 'event.theme.label'|trans }}
<td>
{% for t in event.themes %}
{{ t|chill_entity_render_box }}
{% endfor %}
</td>
</tr>
<tr>
<th>{{ 'event.fields.organizationCost'|trans }}</th>
<td>{{ event.organizationCost|format_currency('EUR') }}</td>
<th>{{ 'Moderator'|trans }}</th>
<td>{{ event.moderator|chill_entity_render_string }}</td>
</tr>
<tr>
<th>{{ 'event.animators.intern'|trans }}</th>
<td>
{% for a in event.animatorsIntern %}
{{ _self.insert_onthefly('user', a) }}
{% endfor %}
</td>
</tr>
<tr>
<th>{{ 'event.animators.extern'|trans }}</th>
<td>
{% for a in event.animatorsExtern %}
{{ _self.insert_onthefly('thirdparty', a) }}
{% endfor %}
</td>
</tr>
<tr>
<th>{{ 'event.fields.location'|trans }}</th>
@@ -60,6 +94,77 @@
</tbody>
</table>
<div class="budget-wrapper" style="background-color: whitesmoke; padding: 1rem; margin-bottom: .5rem;">
{% set resources = event.budgetElements|filter(e => e.kind.type.value == 'Resource') %}
{% set charges = event.budgetElements|filter(e => e.kind.type.value == 'Charge') %}
<h2>Budget de l'événement</h2>
<h3>Ressources</h3>
{% if resources is not empty %}
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'event.budget.resource type'|trans }}</th>
<th>{{ 'event.budget.amount'|trans }}</th>
<th>{{ 'event.budget.comment'|trans }}</th>
</tr>
</thead>
<tbody>
{% set totalResources = 0 %}
{% for res in resources %}
<tr>
<td>{{ res.kind.name|localize_translatable_string }}</td>
<td>{{ res.amount|format_currency('EUR') }}</td>
<td>{{ res.comment.comment|chill_print_or_message(null, 'blockquote') }}</td>
</tr>
{% set totalResources = totalResources + res.amount %}
{% endfor %}
</tbody>
<tfoot>
<tr>
<th>Total</th>
<td>{{ totalResources|format_currency('EUR') }}</td>
</tr>
</tfoot>
</table>
{% else %}
<p class="chill-no-data-statement">{{ 'event.budget.no elements'|trans }}</p>
{% endif %}
<h3>Charges</h3>
{% if charges is not empty %}
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'event.budget.charge type'|trans }}</th>
<th>{{ 'event.budget.amount'|trans }}</th>
<th>{{ 'event.budget.comment'|trans }}</th>
</tr>
</thead>
<tbody>
{% set totalCharges = 0 %}
{% for chg in charges %}
<tr>
<td>{{ chg.kind.name|localize_translatable_string }}</td>
<td>{{ chg.amount|format_currency('EUR') }}</td>
<td>{{ chg.comment.comment|chill_print_or_message(null, 'blockquote') }}</td>
</tr>
{% set totalCharges = totalCharges + chg.amount %}
{% endfor %}
</tbody>
<tfoot>
<tr>
<th>Total</th>
<td>{{ totalCharges|format_currency('EUR') }}</td>
</tr>
</tfoot>
</table>
{% else %}
<p class="chill-no-data-statement">{{ 'event.budget.no elements'|trans }}</p>
{% endif %}
</div>
{% if event.documents|length > 0 %}
<div>
<p><strong>{{ 'event.fields.documents'|trans }}</strong></p>
@@ -80,6 +185,97 @@
</div>
{% endif %}
<div class="participations-wrapper">
<h2>{{ 'Participations'|trans }}</h2>
{% set count = event.participations|length %}
<p class="chill-no-data-statement">{{ 'count participations to this event'|trans({'count': count}) }}</p>
{% if count > 0 %}
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Person'|trans }}</th>
<th>{{ 'Role'|trans }}</th>
<th>{{ 'Status'|trans }}</th>
<th>{{ 'Last update'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for participation in event.participations %}
<tr>
<td>
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: participation.person.id },
action: 'show',
displayBadge: true,
buttonText: participation.person|chill_entity_render_string,
isDead: participation.person.deathdate is not null
} %}
</td>
<td>{{ participation.role.name|localize_translatable_string }}</td>
<td>{{ participation.status.name|localize_translatable_string }}</td>
<td>{{ participation.lastUpdate|ago }} {# sf4 check: filter 'time_diff' is abandoned,
alternative: knplabs/knp-time-bundle provide filter 'ago' #}
<i class="fa fa-info-circle" title="{{ participation.lastUpdate|format_date("long")|escape('html_attr') }}"></i>
</td>
<td>
<ul class="record_actions">
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
<li>
<a href="{{ chill_path_add_return_path('chill_event_participation_edit', { 'participation_id' : participation.id }, false, 'See'|trans ) }}"
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_event_participation_delete', {'participation_id' : participation.id } ) }}"
class="btn btn-delete" title="{{ 'Delete'|trans }}"></a>
</li>
{% endif %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div class="row">
<div class="col-8">
{{ form_start(form_add_participation_by_person) }}
<div class="input-group">
{{ form_widget(form_add_participation_by_person.person_id, { 'attr' : {
'class' : 'custom-select',
'style': 'min-width: 15em; max-width: 18em; display: inline-block;'
}} ) }}
</div>
<input type="hidden" name="returnPath" value="{{ app.request.requestUri }}" />
{{ form_end(form_add_participation_by_person) }}
</div>
</div>
<ul class="record_actions">
{% if count > 0 %}
<li>
{{ form_start(form_export, {'attr': {'id': 'export_tableur'}}) }}
<div class="input-group">
{{ form_widget(form_export.format, { 'attr' : { 'class': 'custom-select' } }) }}
<div class="input-group-append">
{{ form_widget(form_export.submit, { 'attr' : { 'class': 'btn btn-save' } }) }}
</div>
<a class="bt-"></a>
</div>
{{ form_rest(form_export) }}
{{ form_end(form_export) }}
</li>
<li><a href="{{ path('chill_event_participation_edit_multiple', { 'event_id' : event.id } ) }}" class="btn btn-edit">{{ 'Edit all the participations'|trans }}</a></li>
{% endif %}
</ul>
</div>
<div class="post_show">
{{ chill_delegated_block('block_footer_show', { 'event': event }) }}
</div>
<ul class="record_actions">
@@ -100,97 +296,5 @@
</li>
</ul>
<h2>{{ 'Participations'|trans }}</h2>
{% set count = event.participations|length %}
<p>{{ 'count participations to this event'|trans({'count': count}) }}</p>
{% if count > 0 %}
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Person'|trans }}</th>
<th>{{ 'Role'|trans }}</th>
<th>{{ 'Status'|trans }}</th>
<th>{{ 'Last update'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for participation in event.participations %}
<tr>
<td>
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: participation.person.id },
action: 'show',
displayBadge: true,
buttonText: participation.person|chill_entity_render_string,
isDead: participation.person.deathdate is not null
} %}
</td>
<td>{{ participation.role.name|localize_translatable_string }}</td>
<td>{{ participation.status.name|localize_translatable_string }}</td>
<td>{{ participation.lastUpdate|ago }} {# sf4 check: filter 'time_diff' is abandoned,
alternative: knplabs/knp-time-bundle provide filter 'ago' #}
<i class="fa fa-info-circle" title="{{ participation.lastUpdate|format_date("long")|escape('html_attr') }}"></i>
</td>
<td>
<ul class="record_actions">
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
<li>
<a href="{{ chill_path_add_return_path('chill_event_participation_edit', { 'participation_id' : participation.id }, false, 'See'|trans ) }}"
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_event_participation_delete', {'participation_id' : participation.id } ) }}"
class="btn btn-delete" title="{{ 'Delete'|trans }}"></a>
</li>
{% endif %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<ul class="record_actions">
{% if count > 0 %}
<li><a href="{{ path('chill_event_participation_edit_multiple', { 'event_id' : event.id } ) }}" class="btn btn-edit">{{ 'Edit all the participations'|trans }}</a></li>
{% endif %}
</ul>
<div class="row" style="margin-bottom: 10em;">
<div class="col-8">
{{ form_start(form_add_participation_by_person) }}
<div class="input-group">
{{ form_widget(form_add_participation_by_person.person_id, { 'attr' : {
'class' : 'custom-select',
'style': 'min-width: 15em; max-width: 18em; display: inline-block;'
}} ) }}
</div>
<input type="hidden" name="returnPath" value="{{ app.request.requestUri }}" />
{{ form_end(form_add_participation_by_person) }}
</div>
<div class="col-4">
{{ form_start(form_export, {'attr': {'id': 'export_tableur'}}) }}
<div class="input-group">
{{ form_widget(form_export.format, { 'attr' : { 'class': 'custom-select' } }) }}
<div class="input-group-append">
{{ form_widget(form_export.submit, { 'attr' : { 'class': 'btn btn-save' } }) }}
</div>
<a class="bt-"></a>
</div>
{{ form_rest(form_export) }}
{{ form_end(form_export) }}
</div>
</div>
<div class="post_show">
{{ chill_delegated_block('block_footer_show', { 'event': event }) }}
</div>
</div>
{% endblock %}

View File

@@ -11,7 +11,7 @@
'title' : 'Remove participation'|trans,
'confirm_question' : 'Are you sure you want to remove that participation ?'|trans,
'cancel_route' : activeRouteKey,
'cancel_parameters' : { 'event_id' : event_id },
'cancel_parameters' : { 'id' : event_id },
'form' : delete_form
}
) }}

View File

@@ -33,7 +33,7 @@
{% set returnPath = app.request.get('return_path') %}
{% set returnLabel = app.request.get('return_label') %}
<a href="{{ returnPath |default( path('chill_event__event_show', { 'event_id' : participation.event.id } )) }}" class="btn btn-cancel">
<a href="{{ returnPath |default( path('chill_event__event_show', { 'id' : participation.event.id } )) }}" class="btn btn-cancel">
{{ returnLabel |default('Back to the event'|trans) }}
</a>
</li>

View File

@@ -34,7 +34,7 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id } ) }}" class="btn btn-cancel">
<a href="{{ path('chill_event__event_show', { 'id' : participation.event.id } ) }}" class="btn btn-cancel">
{{ 'Back to the event'|trans }}
</a>
</li>

View File

@@ -54,9 +54,9 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
) {
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::SEE])
->addCheckFor(null, [self::SEE, self::CREATE])
->addCheckFor(Event::class, [...self::ROLES])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Person::class, [self::SEE])
->addCheckFor(Center::class, [self::STATS])
->build();
}

View File

@@ -0,0 +1,109 @@
<?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\EventBundle\Templating\Entity;
use Chill\EventBundle\Entity\EventTheme;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
/**
* @implements ChillEntityRenderInterface<EventTheme>
*/
class EventThemeRender implements ChillEntityRenderInterface
{
public const AND_CHILDREN_MENTION = 'show_and_children_mention';
public const DEFAULT_ARGS = [
self::SEPARATOR_KEY => ' > ',
self::SHOW_AND_CHILDREN => false,
self::AND_CHILDREN_MENTION => 'event_theme.and children',
];
public const SEPARATOR_KEY = 'default.separator';
/**
* Show a mention "and children" on each EventTheme, if the event theme
* has at least one child.
*/
public const SHOW_AND_CHILDREN = 'show_and_children';
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {}
/**
* @throws RuntimeError
* @throws SyntaxError
* @throws LoaderError
*/
public function renderBox($entity, array $options): string
{
$options = array_merge(self::DEFAULT_ARGS, $options);
// give some help to twig: an array of parents
$parents = $this->buildParents($entity);
return $this
->engine
->render(
'@ChillEvent/Entity/event_theme.html.twig',
[
'eventTheme' => $entity,
'parents' => $parents,
'options' => $options,
]
);
}
public function renderString($entity, array $options): string
{
/** @var EventTheme $entity */
$options = array_merge(self::DEFAULT_ARGS, $options);
$titles = [$this->translatableStringHelper->localize($entity->getName())];
// loop to parent, until root
while ($entity->hasParent()) {
$entity = $entity->getParent();
$titles[] = $this->translatableStringHelper->localize(
$entity->getName()
);
}
$titles = \array_reverse($titles);
$title = \implode($options[self::SEPARATOR_KEY], $titles);
if ($options[self::SHOW_AND_CHILDREN] && $entity->hasChildren()) {
$title .= ' ('.$this->translator->trans($options[self::AND_CHILDREN_MENTION]).')';
}
return $title;
}
public function supports($entity, array $options): bool
{
return $entity instanceof EventTheme;
}
private function buildParents(EventTheme $entity): array
{
$parents = [];
while ($entity->hasParent()) {
$entity = $parents[] = $entity->getParent();
}
return $parents;
}
}

View File

@@ -1,3 +1,10 @@
module.exports = function (encore, entries) {
entries.push(__dirname + "/Resources/public/chill/index.js");
encore.addEntry(
"vue_event",
__dirname + "/Resources/public/vuejs/index.js",
);
};

View File

@@ -1,6 +1,7 @@
services:
Chill\EventBundle\Controller\:
autowire: true
autoconfigure: true
resource: '../Controller'
tags: ['controller.service_arguments']
@@ -9,3 +10,10 @@ services:
autoconfigure: true
resource: '../Menu/'
tags: ['chill.menu_builder']
Chill\EventBundle\Templating\Entity\:
autowire: true
autoconfigure: true
resource: '../Templating/Entity'
tags:
- 'chill.render_entity'

View File

@@ -0,0 +1,4 @@
services:
_defaults:
autowire: true
autoconfigure: true

View File

@@ -8,6 +8,9 @@ services:
Chill\EventBundle\Export\Export\CountEvents:
tags:
- { name: chill.export, alias: 'count_events' }
Chill\EventBundle\Export\Export\ListEvents:
tags:
- { name: chill.export, alias: 'list_events' }
Chill\EventBundle\Export\Export\CountEventParticipations:
tags:
- { name: chill.export, alias: 'count_event_participants' }

View File

@@ -31,3 +31,29 @@ services:
Chill\EventBundle\Form\Type\PickEventType:
tags:
- { name: form.type }
Chill\EventBundle\Form\EventThemeType:
tags:
- { name: form.type }
Chill\EventBundle\Form\Type\PickEventThemeType:
tags:
- { name: form.type }
Chill\EventBundle\Form\EventType:
tags:
- { name: form.type }
Chill\EventBundle\Form\EventBudgetKindType:
tags:
- { name: form.type }
Chill\EventBundle\Form\AddEventBudgetElementType:
tags:
- { name: form.type }
Chill\EventBundle\Form\Type\PickAnimatorType:
tags:
- { name: form.type }

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Event;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add event theme entity.
*/
final class Version20250428092611 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add an event theme entity';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_event_event_theme_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_event_event_theme (id INT NOT NULL, parent_id INT DEFAULT NULL, isActive BOOLEAN NOT NULL, name JSON NOT NULL, ordering DOUBLE PRECISION DEFAULT \'0.0\' NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_80D7C6B0727ACA70 ON chill_event_event_theme (parent_id)');
$this->addSql('ALTER TABLE chill_event_event_theme ADD CONSTRAINT FK_80D7C6B0727ACA70 FOREIGN KEY (parent_id) REFERENCES chill_event_event_theme (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_event_event_theme DROP CONSTRAINT FK_80D7C6B0727ACA70');
$this->addSql('DROP TABLE chill_event_event_theme');
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Event;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250429062911 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add themes property to event';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE chill_event_eventtheme (event_id INT NOT NULL, eventtheme_id INT NOT NULL, PRIMARY KEY(event_id, eventtheme_id))');
$this->addSql('CREATE INDEX IDX_8D75029771F7E88B ON chill_event_eventtheme (event_id)');
$this->addSql('CREATE INDEX IDX_8D750297A81D3C55 ON chill_event_eventtheme (eventtheme_id)');
$this->addSql('ALTER TABLE chill_event_eventtheme ADD CONSTRAINT FK_8D75029771F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_event_eventtheme ADD CONSTRAINT FK_8D750297A81D3C55 FOREIGN KEY (eventtheme_id) REFERENCES chill_event_event_theme (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_event_eventtheme DROP CONSTRAINT FK_8D75029771F7E88B');
$this->addSql('ALTER TABLE chill_event_eventtheme DROP CONSTRAINT FK_8D750297A81D3C55');
$this->addSql('DROP TABLE chill_event_eventtheme');
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Event;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250505120818 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add budget kind admin entity';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE SEQUENCE chill_event_budget_kind_id_seq INCREMENT BY 1 MINVALUE 1 START 1
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE chill_event_budget_kind (id INT NOT NULL, isActive BOOLEAN DEFAULT true NOT NULL, type VARCHAR(255) NOT NULL, name JSONB DEFAULT '{}' NOT NULL, PRIMARY KEY(id))
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
DROP SEQUENCE chill_event_budget_kind_id_seq CASCADE
SQL);
$this->addSql(<<<'SQL'
DROP TABLE chill_event_budget_kind
SQL);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Event;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250506114531 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create event budget element entity';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE SEQUENCE chill_event_budget_element_id_seq INCREMENT BY 1 MINVALUE 1 START 1
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE chill_event_budget_element (id INT NOT NULL, event_id INT DEFAULT NULL, kind_id INT DEFAULT NULL, amount NUMERIC(10, 2) NOT NULL, comment_budget_element_comment TEXT DEFAULT NULL, comment_budget_element_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, comment_budget_element_userId INT DEFAULT NULL, PRIMARY KEY(id))
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_BA25859071F7E88B ON chill_event_budget_element (event_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_BA25859030602CA9 ON chill_event_budget_element (kind_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_budget_element ADD CONSTRAINT FK_BA25859071F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_budget_element ADD CONSTRAINT FK_BA25859030602CA9 FOREIGN KEY (kind_id) REFERENCES chill_event_budget_kind (id) NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
DROP SEQUENCE chill_event_budget_element_id_seq CASCADE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_budget_element DROP CONSTRAINT FK_BA25859071F7E88B
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_budget_element DROP CONSTRAINT FK_BA25859030602CA9
SQL);
$this->addSql(<<<'SQL'
DROP TABLE chill_event_budget_element
SQL);
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Event;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250507073301 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add animators field to an event';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE chill_event_thirdparty (event_id INT NOT NULL, thirdparty_id INT NOT NULL, PRIMARY KEY(event_id, thirdparty_id))
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_9946573E71F7E88B ON chill_event_thirdparty (event_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_9946573EC7D3A8E6 ON chill_event_thirdparty (thirdparty_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_thirdparty ADD CONSTRAINT FK_9946573E71F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_thirdparty ADD CONSTRAINT FK_9946573EC7D3A8E6 FOREIGN KEY (thirdparty_id) REFERENCES chill_3party.third_party (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_thirdparty DROP CONSTRAINT FK_9946573E71F7E88B
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_thirdparty DROP CONSTRAINT FK_9946573EC7D3A8E6
SQL);
$this->addSql(<<<'SQL'
DROP TABLE chill_event_thirdparty
SQL);
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Event;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250702144312 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add internal and external animators to event entity';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE chill_event_animatorsintern (event_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(event_id, user_id))
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_E699558771F7E88B ON chill_event_animatorsintern (event_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_E6995587A76ED395 ON chill_event_animatorsintern (user_id)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE chill_event_animatorsextern (event_id INT NOT NULL, thirdparty_id INT NOT NULL, PRIMARY KEY(event_id, thirdparty_id))
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_7EFBF7DE71F7E88B ON chill_event_animatorsextern (event_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_7EFBF7DEC7D3A8E6 ON chill_event_animatorsextern (thirdparty_id)
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_animatorsintern ADD CONSTRAINT FK_E699558771F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_animatorsintern ADD CONSTRAINT FK_E6995587A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_animatorsextern ADD CONSTRAINT FK_7EFBF7DE71F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_animatorsextern ADD CONSTRAINT FK_7EFBF7DEC7D3A8E6 FOREIGN KEY (thirdparty_id) REFERENCES chill_3party.third_party (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_animatorsintern DROP CONSTRAINT FK_E699558771F7E88B
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_animatorsintern DROP CONSTRAINT FK_E6995587A76ED395
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_animatorsextern DROP CONSTRAINT FK_7EFBF7DE71F7E88B
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE chill_event_animatorsextern DROP CONSTRAINT FK_7EFBF7DEC7D3A8E6
SQL);
$this->addSql(<<<'SQL'
DROP TABLE chill_event_animatorsintern
SQL);
$this->addSql(<<<'SQL'
DROP TABLE chill_event_animatorsextern
SQL);
}
}

View File

@@ -7,7 +7,8 @@ Participation: Participation
Participations: Participations
Status: Statut
Last update: Dernière mise à jour
Moderator: Animateur
Moderator: Responsable
Animators: Animateurs
#CRUD event
Details of an event: Détails d'un événement
@@ -74,7 +75,7 @@ Show the event: Voir l'événement
Subscribe an event: Ajouter un événement
Pick an event: Choisir un événement
Pick a type of event: Choisir un type d'événement
Pick a moderator: Choisir un animateur
Pick a moderator: Choisir un responsable
# exports
Select a format: Choisir un format
@@ -128,15 +129,70 @@ Create a new type: Créer un nouveau type
Create a new status: Créer un nouveau statut
event:
admin:
title:
Event budget element list: Liste des elements du budget pour un évenement
Select budget type: Selectionner le type d'element du budget
new:
Create a new budget kind: Créér un nouveau element de budget
theme:
label: Thématiques
fields:
organizationCost: Coût d'organisation
location: Localisation
documents: Documents
internal animators: Animateurs internes
external animators: Animateurs externes
form:
organisationCost_help: Coût d'organisation pour la structure. Utile pour les statistiques.
add_document: Ajouter un document
remove_document: Supprimer le document
Select one or more themes: Selectionnez une ou plusieurs thématiques
filter:
event_types: Par types d'événement
event_dates: Par date d'événement
center: Par centre
by_responsable: Par responsable
pick_responsable: Filtrer par responsables
budget:
resources: Ressources
charges: Charges
label: Elements de budget d'un évenement
Select a budget element kind: Selectionner un element de budget
no elements: Il y a aucun element
resource type: Ressource
charge type: Charge
amount: Montant
comment: Commentaire
animators:
intern: Animateurs internes
extern: Animateurs externes
crud:
event_theme:
title_new: Créér une nouvelle thématique
title_edit: Modifier la thématique
index:
title: Liste des thématiques
add_new: Créér une nouvelle thématique
event_budget_kind:
title_new: Créér un nouveau element de budget
export:
event:
list:
title: Liste des évenements
description: Crée la liste des évenements en fonction de différents paramètres.
event_id: Identifiant
event_name: Nom
event_date: Date
event_type: Type d'évenement
event_center: Centre
event_moderator: Responsable
event_participants_count: Nombre de participants
event_location: Localisation
event_budget_resources: Ressources
event_budget_charges: Charges
event_animators: Animateurs
event_themes: Thématiques

View File

@@ -20,6 +20,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompiler
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchApiInterface;
@@ -61,6 +62,8 @@ class ChillMainBundle extends Bundle
->addTag('chill_main.entity_info_provider');
$container->registerForAutoconfiguration(ProvideRoleInterface::class)
->addTag('chill_main.provide_role');
$container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
->addTag('chill_main.notification_flag_provider');
$container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);

View File

@@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Form\NotificationCommentType;
use Chill\MainBundle\Form\NotificationType;
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
use Chill\MainBundle\Notification\FlagProviders\NotificationByUserFlagProvider;
use Chill\MainBundle\Notification\NotificationHandlerManager;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
@@ -57,7 +58,8 @@ class NotificationController extends AbstractController
$notification
->setRelatedEntityClass($request->query->get('entityClass'))
->setRelatedEntityId($request->query->getInt('entityId'))
->setSender($this->security->getUser());
->setSender($this->security->getUser())
->setType(NotificationByUserFlagProvider::FLAG);
$tos = $request->query->all('tos');

View File

@@ -11,14 +11,11 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Form\UserPhonenumberType;
use Chill\MainBundle\Form\UserProfileType;
use Chill\MainBundle\Security\ChillSecurity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
@@ -41,16 +38,19 @@ final class UserProfileController extends AbstractController
}
$user = $this->security->getUser();
$editForm = $this->createPhonenumberEditForm($user);
$editForm = $this->createForm(UserProfileType::class, $user);
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$phonenumber = $editForm->get('phonenumber')->getData();
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
$user->setNotificationFlags($notificationFlagsData);
$user->setPhonenumber($phonenumber);
$this->managerRegistry->getManager()->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!'));
$em = $this->managerRegistry->getManager();
$em->flush();
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
return $this->redirectToRoute('chill_main_user_profile');
}
@@ -60,13 +60,4 @@ final class UserProfileController extends AbstractController
'form' => $editForm->createView(),
]);
}
private function createPhonenumberEditForm(UserInterface $user): FormInterface
{
return $this->createForm(
UserPhonenumberType::class,
$user,
)
->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
}
}

View File

@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -21,10 +22,10 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_main_notification')]
#[ORM\Index(name: 'chill_main_notification_related_entity_idx', columns: ['relatedentityclass', 'relatedentityid'])]
#[ORM\Index(columns: ['relatedentityclass', 'relatedentityid'], name: 'chill_main_notification_related_entity_idx')]
class Notification implements TrackUpdateInterface
{
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)]
#[ORM\Column(type: Types::TEXT, nullable: false)]
private string $accessKey;
private array $addedAddresses = [];
@@ -36,12 +37,19 @@ class Notification implements TrackUpdateInterface
#[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
private Collection $addressees;
/**
* @var Collection<int, UserGroup>
*/
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
#[ORM\JoinTable(name: 'chill_main_notification_addressee_user_group')]
private Collection $addresseeUserGroups;
/**
* a list of destinee which will receive notifications.
*
* @var array|string[]
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
#[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
private array $addressesEmails = [];
/**
@@ -60,21 +68,21 @@ class Notification implements TrackUpdateInterface
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
private Collection $comments;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private \DateTimeImmutable $date;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
#[ORM\Column(type: Types::TEXT)]
private string $message = '';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
#[ORM\Column(type: Types::STRING, length: 255)]
private string $relatedEntityClass = '';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\Column(type: Types::INTEGER)]
private int $relatedEntityId;
private array $removedAddresses = [];
@@ -84,7 +92,7 @@ class Notification implements TrackUpdateInterface
private ?User $sender = null;
#[Assert\NotBlank(message: 'notification.Title must be defined')]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
#[ORM\Column(type: Types::TEXT, options: ['default' => ''])]
private string $title = '';
/**
@@ -94,31 +102,46 @@ class Notification implements TrackUpdateInterface
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
private Collection $unreadBy;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private ?\DateTimeImmutable $updatedAt = null;
#[ORM\ManyToOne(targetEntity: User::class)]
private ?User $updatedBy = null;
#[ORM\Column(name: 'type', type: Types::STRING, nullable: true)]
private string $type = '';
public function __construct()
{
$this->addressees = new ArrayCollection();
$this->addresseeUserGroups = new ArrayCollection();
$this->unreadBy = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->setDate(new \DateTimeImmutable());
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
}
public function addAddressee(User $addressee): self
public function addAddressee(User|UserGroup $addressee): self
{
if (!$this->addressees->contains($addressee)) {
$this->addressees[] = $addressee;
$this->addedAddresses[] = $addressee;
if ($addressee instanceof User) {
if (!$this->addressees->contains($addressee)) {
$this->addressees->add($addressee);
$this->addedAddresses[] = $addressee;
}
return $this;
}
if (!$this->addresseeUserGroups->contains($addressee)) {
$this->addresseeUserGroups->add($addressee);
}
return $this;
}
/**
* @deprecated
*/
public function addAddressesEmail(string $email)
{
if (!\in_array($email, $this->addressesEmails, true)) {
@@ -152,13 +175,23 @@ class Notification implements TrackUpdateInterface
#[Assert\Callback]
public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
{
if (0 === (\count($this->getAddressesEmails()) + \count($this->getAddressees()))) {
if (0 === (\count($this->getAddresseeUserGroups()) + \count($this->getAddressees()))) {
$context->buildViolation('notification.At least one addressee')
->atPath('addressees')
->addViolation();
}
}
public function getAddresseeUserGroups(): Collection
{
return $this->addresseeUserGroups;
}
public function setAddresseeUserGroups(Collection $addresseeUserGroups): void
{
$this->addresseeUserGroups = $addresseeUserGroups;
}
public function getAccessKey(): string
{
return $this->accessKey;
@@ -182,6 +215,23 @@ class Notification implements TrackUpdateInterface
return $this->addressees;
}
public function getAllAddressees(): array
{
$allUsers = [];
foreach ($this->getAddressees() as $user) {
$allUsers[$user->getId()] = $user;
}
foreach ($this->getAddresseeUserGroups() as $userGroup) {
foreach ($userGroup->getUsers() as $user) {
$allUsers[$user->getId()] = $user;
}
}
return array_values($allUsers);
}
/**
* @return array|string[]
*/
@@ -303,12 +353,18 @@ class Notification implements TrackUpdateInterface
$this->addressesOnLoad = null;
}
public function removeAddressee(User $addressee): self
public function removeAddressee(User|UserGroup $addressee): self
{
if ($this->addressees->removeElement($addressee)) {
$this->removedAddresses[] = $addressee;
if ($addressee instanceof User) {
if ($this->addressees->contains($addressee)) {
$this->addressees->removeElement($addressee);
return $this;
}
}
$this->addresseeUserGroups->removeElement($addressee);
return $this;
}
@@ -378,7 +434,7 @@ class Notification implements TrackUpdateInterface
public function setUpdatedAt(\DateTimeInterface $datetime): self
{
$this->updatedAt = $datetime;
$this->updatedAt = \DateTimeImmutable::createFromInterface($datetime);
return $this;
}
@@ -389,4 +445,16 @@ class Notification implements TrackUpdateInterface
return $this;
}
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
public function getType(): string
{
return $this->type;
}
}

View File

@@ -34,6 +34,9 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
#[ORM\Table(name: 'users')]
class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInterface
{
public const NOTIF_FLAG_IMMEDIATE_EMAIL = 'immediate-email';
public const NOTIF_FLAG_DAILY_DIGEST = 'daily-digest';
#[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
@@ -116,6 +119,12 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
#[PhonenumberConstraint]
private ?PhoneNumber $phonenumber = null;
/**
* @var array<string, list<string>>
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
private array $notificationFlags = [];
/**
* User constructor.
*/
@@ -613,4 +622,57 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return $this;
}
/**
* Check if the current object is an instance of User.
*
* @return bool returns true if the current object is an instance of User, false otherwise
*/
public function isUser(): bool
{
return true;
}
public function getNotificationFlags(): array
{
return $this->notificationFlags;
}
public function setNotificationFlags(array $notificationFlags)
{
$this->notificationFlags = $notificationFlags;
}
public function getNotificationFlagData(string $flag): array
{
return $this->notificationFlags[$flag] ?? [];
}
public function setNotificationFlagData(string $flag, array $data): void
{
$this->notificationFlags[$flag] = $data;
}
public function isNotificationSendImmediately(string $type): bool
{
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
return true;
}
return false;
}
public function isNotificationDailyDigest(string $type): bool
{
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
return true;
}
return false;
}
public function getLocale(): string
{
return 'fr';
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\DataMapper;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Form\DataMapperInterface;
final readonly class NotificationFlagDataMapper implements DataMapperInterface
{
public function __construct(private array $notificationFlagProviders) {}
public function mapDataToForms($viewData, $forms): void
{
if (null === $viewData) {
$viewData = [];
}
$formsArray = iterator_to_array($forms);
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|| !array_key_exists($flag, $viewData);
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
if ($flagForm->has('immediate_email')) {
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
}
if ($flagForm->has('daily_email')) {
$flagForm->get('daily_email')->setData($dailyEmailChecked);
}
}
}
}
public function mapFormsToData($forms, &$viewData): void
{
$formsArray = iterator_to_array($forms);
$viewData = [];
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
if (isset($formsArray[$flag])) {
$flagForm = $formsArray[$flag];
$viewData[$flag] = [];
if (true === $flagForm['immediate_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
if (true === $flagForm['daily_email']->getData()) {
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
}
if ([] === $viewData[$flag]) {
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
}
}
}
}
}

View File

@@ -12,17 +12,12 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
class NotificationType extends AbstractType
{
@@ -33,29 +28,14 @@ class NotificationType extends AbstractType
'label' => 'Title',
'required' => true,
])
->add('addressees', PickUserDynamicType::class, [
->add('addressees', PickUserGroupOrUserDynamicType::class, [
'multiple' => true,
'required' => false,
'label' => 'notification.Pick user or user group',
'empty_data' => '[]',
'required' => true,
])
->add('message', ChillTextareaType::class, [
'required' => false,
])
->add('addressesEmails', ChillCollectionType::class, [
'label' => 'notification.dest by email',
'help' => 'notification.dest by email help',
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'entry_type' => EmailType::class,
'button_add_label' => 'notification.Add an email',
'button_remove_label' => 'notification.Remove an email',
'empty_collection_explain' => 'notification.Any email',
'entry_options' => [
'constraints' => [
new NotNull(), new NotBlank(), new Email(),
],
'label' => 'Email',
],
]);
}

View File

@@ -55,6 +55,10 @@ class DateIntervalType extends AbstractType
{
$builder
->add('n', IntegerType::class, [
'attr' => [
'min' => 0,
'step' => 1,
],
'constraints' => [
new GreaterThan([
'value' => 0,

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Form\DataMapper\NotificationFlagDataMapper;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NotificationFlagsType extends AbstractType
{
private readonly array $notificationFlagProviders;
public function __construct(NotificationFlagManager $notificationFlagManager)
{
$this->notificationFlagProviders = $notificationFlagManager->getAllNotificationFlagProviders();
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
foreach ($this->notificationFlagProviders as $flagProvider) {
$flag = $flagProvider->getFlag();
$builder->add($flag, FormType::class, [
'label' => $flagProvider->getLabel(),
'required' => false,
]);
$builder->get($flag)
->add('immediate_email', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false,
])
->add('daily_email', CheckboxType::class, [
'label' => false,
'required' => false,
'mapped' => false,
])
;
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\NotificationFlagsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserProfileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('phonenumber', ChillPhoneNumberType::class, [
'required' => false,
])
->add('notificationFlags', NotificationFlagsType::class, [
'label' => false,
'mapped' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => \Chill\MainBundle\Entity\User::class,
]);
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email;
use Chill\MainBundle\Cron\CronJobInterface;
use Chill\MainBundle\Entity\CronJobExecution;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Messenger\MessageBusInterface;
readonly class DailyNotificationDigestCronjob implements CronJobInterface
{
public function __construct(
private ClockInterface $clock,
private Connection $connection,
private MessageBusInterface $messageBus,
private LoggerInterface $logger,
) {}
public function canRun(?CronJobExecution $cronJobExecution): bool
{
$now = $this->clock->now();
if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) {
return false;
}
// Run between 6 and 9 AM
return in_array((int) $now->format('H'), [6, 7, 8], true);
}
public function getKey(): string
{
return 'daily-notification-digest';
}
/**
* @throws \DateInvalidOperationException
* @throws Exception
*/
public function run(array $lastExecutionData): ?array
{
$now = $this->clock->now();
if (isset($lastExecutionData['last_execution'])) {
$lastExecution = \DateTimeImmutable::createFromFormat(
\DateTimeImmutable::ATOM,
$lastExecutionData['last_execution']
);
} else {
$lastExecution = $now->sub(new \DateInterval('P1D'));
}
// Get distinct users who received notifications since the last execution
$sql = <<<'SQL'
SELECT DISTINCT cmnau.user_id
FROM chill_main_notification cmn
JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id
WHERE cmn.date >= :lastExecution AND cmn.date <= :now
SQL;
$sqlStatement = $this->connection->prepare($sql);
$sqlStatement->bindValue('lastExecution', $lastExecution->format(\DateTimeInterface::RFC3339));
$sqlStatement->bindValue('now', $now->format(\DateTimeInterface::RFC3339));
$result = $sqlStatement->executeQuery();
$count = 0;
foreach ($result->fetchAllAssociative() as $row) {
$userId = (int) $row['user_id'];
$message = new ScheduleDailyNotificationDigestMessage(
$userId,
$lastExecution,
$now
);
$this->messageBus->dispatch($message);
++$count;
}
$this->logger->info('[DailyNotificationDigestCronjob] Dispatched daily digest messages', [
'user_count' => $count,
'last_execution' => $lastExecution->format('Y-m-d-H:i:s.u e'),
'current_time' => $now->format('Y-m-d-H:i:s.u e'),
]);
return [
'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
readonly class ScheduleDailyNotificationDigestHandler
{
public function __construct(
private NotificationRepository $notificationRepository,
private UserRepository $userRepository,
private NotificationMailer $notificationMailer,
private LoggerInterface $logger,
) {}
/**
* @throws TransportExceptionInterface
*/
public function __invoke(ScheduleDailyNotificationDigestMessage $message): void
{
$userId = $message->getUserId();
$lastExecutionDate = $message->getLastExecutionDateTime();
$currentDate = $message->getCurrentDateTime();
$user = $this->userRepository->find($userId);
if (null === $user) {
$this->logger->warning('[ScheduleDailyNotificationDigestHandler] User not found', [
'user_id' => $userId,
]);
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $userId));
}
// Get all notifications for this user between last execution and current date
$notifications = $this->notificationRepository->findNotificationsForUserBetweenDates(
$userId,
$lastExecutionDate,
$currentDate
);
// Filter out notifications that should be sent in a daily digest
$dailyNotifications = array_filter($notifications, fn ($notification) => $user->isNotificationDailyDigest($notification->getType()));
if ([] === $dailyNotifications) {
$this->logger->info('[ScheduleDailyNotificationDigestHandler] No daily notifications found for user', [
'user_id' => $userId,
]);
return;
}
$this->notificationMailer->sendDailyDigest($user, $dailyNotifications);
$this->logger->info('[ScheduleDailyNotificationDigestHandler] Sent daily digest', [
'user_id' => $userId,
'notification_count' => count($dailyNotifications),
]);
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
readonly class SendImmediateNotificationEmailHandler
{
public function __construct(
private NotificationRepository $notificationRepository,
private UserRepository $userRepository,
private NotificationMailer $notificationMailer,
private LoggerInterface $logger,
) {}
/**
* @throws TransportExceptionInterface
* @throws \Exception
*/
public function __invoke(SendImmediateNotificationEmailMessage $message): void
{
$notification = $this->notificationRepository->find($message->getNotificationId());
$addressee = $this->userRepository->find($message->getAddresseeId());
if (null === $notification) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [
'notification_id' => $message->getNotificationId(),
]);
throw new \InvalidArgumentException(sprintf('Notification with ID %s not found', $message->getNotificationId()));
}
if (null === $addressee) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [
'addressee_id' => $message->getAddresseeId(),
]);
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId()));
}
try {
$this->notificationMailer->sendEmailToAddressee($notification, $addressee);
} catch (\Exception $e) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [
'notification_id' => $message->getNotificationId(),
'addressee_id' => $message->getAddresseeId(),
'stacktrace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages;
readonly class ScheduleDailyNotificationDigestMessage
{
public function __construct(
private int $userId,
private \DateTimeInterface $lastExecutionDate,
private \DateTimeInterface $currentDate,
) {}
public function getUserId(): int
{
return $this->userId;
}
public function getLastExecutionDateTime(): \DateTimeInterface
{
return $this->lastExecutionDate;
}
public function getCurrentDateTime(): \DateTimeInterface
{
return $this->currentDate;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages;
readonly class SendImmediateNotificationEmailMessage
{
public function __construct(
private int $notificationId,
private int $addresseeId,
) {}
public function getNotificationId(): int
{
return $this->notificationId;
}
public function getAddresseeId(): int
{
return $this->addresseeId;
}
}

View File

@@ -13,22 +13,32 @@ namespace Chill\MainBundle\Notification\Email;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class NotificationMailer
readonly class NotificationMailer
{
public function __construct(private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) {}
public function __construct(
private MailerInterface $mailer,
private LoggerInterface $logger,
private MessageBusInterface $messageBus,
private TranslatorInterface $translator,
) {}
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
{
$dests = [$comment->getNotification()->getSender(), ...$comment->getNotification()->getAddressees()->toArray()];
$dests = [
$comment->getNotification()->getSender(),
...$comment->getNotification()->getAddressees()->toArray(),
];
$uniqueDests = [];
foreach ($dests as $dest) {
@@ -69,55 +79,147 @@ class NotificationMailer
*/
public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void
{
$this->sendNotificationEmailsToAddresses($notification);
$this->sendNotificationEmailsToAddressees($notification);
$this->sendNotificationEmailsToAddressesEmails($notification);
}
public function postUpdateNotification(Notification $notification, PostUpdateEventArgs $eventArgs): void
private function sendNotificationEmailsToAddressees(Notification $notification): void
{
$this->sendNotificationEmailsToAddressesEmails($notification);
}
if ('' === $notification->getType()) {
$this->logger->warning('[NotificationMailer] Notification has no type, skipping email processing', [
'notification_id' => $notification->getId(),
]);
private function sendNotificationEmailsToAddresses(Notification $notification): void
{
foreach ($notification->getAddressees() as $addressee) {
return;
}
foreach ($notification->getAllAddressees() as $addressee) {
if (null === $addressee->getEmail()) {
continue;
}
if ($notification->isSystem()) {
$email = new Email();
$email
->text($notification->getMessage());
} else {
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
->context([
'notification' => $notification,
'dest' => $addressee,
]);
}
$this->processNotificationForAddressee($notification, $addressee);
}
}
private function processNotificationForAddressee(Notification $notification, User $addressee): void
{
$notificationType = $notification->getType();
if ($addressee->isNotificationSendImmediately($notificationType)) {
$this->scheduleImmediateEmail($notification, $addressee);
}
}
private function scheduleImmediateEmail(Notification $notification, User $addressee): void
{
$message = new SendImmediateNotificationEmailMessage(
$notification->getId(),
$addressee->getId()
);
$this->messageBus->dispatch($message);
$this->logger->info('[NotificationMailer] Scheduled immediate email', [
'notification_id' => $notification->getId(),
'addressee_email' => $addressee->getEmail(),
]);
}
/**
* This method sends the email but is now called by the immediate notification email message handler.
*
* @throws TransportExceptionInterface
*/
public function sendEmailToAddressee(Notification $notification, User $addressee): void
{
if (null === $addressee->getEmail()) {
return;
}
if ($notification->isSystem()) {
$email = new Email();
$email->text($notification->getMessage());
} else {
$email = new TemplatedEmail();
$email
->subject($notification->getTitle())
->to($addressee->getEmail());
try {
$this->mailer->send($email);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] could not send an email notification', [
'to' => $addressee->getEmail(),
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
->context([
'notification' => $notification,
'dest' => $addressee,
]);
}
}
$email
->subject($notification->getTitle())
->to($addressee->getEmail());
try {
$this->mailer->send($email);
$this->logger->info('[NotificationMailer] Email sent successfully', [
'notification_id' => $notification->getId(),
'addressee_email' => $addressee->getEmail(),
]);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] Could not send an email notification', [
'to' => $addressee->getEmail(),
'notification_id' => $notification->getId(),
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
/**
* Send daily digest email with multiple notifications to a user.
*
* @throws TransportExceptionInterface
*/
public function sendDailyDigest(User $user, array $notifications): void
{
if (null === $user->getEmail() || [] === $notifications) {
return;
}
$email = new TemplatedEmail();
$email
->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
->context([
'user' => $user,
'notifications' => $notifications,
'notification_count' => count($notifications),
])
->subject($this->translator->trans('notification.Daily Notification Digest'))
->to($user->getEmail());
try {
$this->mailer->send($email);
$this->logger->info('[NotificationMailer] Daily digest email sent successfully', [
'user_email' => $user->getEmail(),
'notification_count' => count($notifications),
]);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] Could not send daily digest email', [
'to' => $user->getEmail(),
'notification_count' => count($notifications),
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
{
foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
foreach ($notification->getAddresseeUserGroups() as $userGroup) {
if (!$userGroup->hasEmail()) {
continue;
}
$emailAddress = $userGroup->getEmail();
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\FlagProviders;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class NotificationByUserFlagProvider implements NotificationFlagProviderInterface
{
public const FLAG = 'notif-by-user';
public function getFlag(): string
{
return self::FLAG;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.by-user');
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\FlagProviders;
use Symfony\Contracts\Translation\TranslatableInterface;
interface NotificationFlagProviderInterface
{
public function getFlag(): string;
public function getLabel(): TranslatableInterface;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification\FlagProviders;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
class WorkflowTransitionNotificationFlagProvider implements NotificationFlagProviderInterface
{
public const FLAG = 'workflow-trans-notif';
public function getFlag(): string
{
return self::FLAG;
}
public function getLabel(): TranslatableInterface
{
return new TranslatableMessage('notification.flags.workflow-trans');
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Notification;
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
final readonly class NotificationFlagManager
{
/**
* @var array<NotificationFlagProviderInterface>
*/
private array $notificationFlagProviders;
public function __construct(
iterable $notificationFlagProviders,
) {
$this->notificationFlagProviders = iterator_to_array($notificationFlagProviders);
}
public function getAllNotificationFlagProviders(): array
{
return $this->notificationFlagProviders;
}
}

View File

@@ -290,12 +290,19 @@ final class NotificationRepository implements ObjectRepository
return $qb;
}
private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
private function queryByAddressee(User $addressee): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('n');
$qb
->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
->leftJoin('n.addresseeUserGroups', 'aug')
->leftJoin('aug.users', 'ugu')
->where(
$qb->expr()->orX(
$qb->expr()->isMemberOf(':addressee', 'n.addressees'),
$qb->expr()->eq('ugu.id', ':addressee')
)
)
->setParameter('addressee', $addressee);
return $qb;
@@ -393,4 +400,30 @@ final class NotificationRepository implements ObjectRepository
return $nq->getResult();
}
/**
* Find all notifications for a user that were created between two dates.
*
* @return array|Notification[]
*/
public function findNotificationsForUserBetweenDates(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
{
$rsm = new Query\ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
'FROM chill_main_notification cmn '.
'JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id '.
'WHERE cmnau.user_id = :userId '.
'AND cmn.date >= :startDate '.
'AND cmn.date <= :endDate '.
'ORDER BY cmn.date DESC';
$nq = $this->em->createNativeQuery($sql, $rsm)
->setParameter('userId', $userId)
->setParameter('startDate', $startDate, Types::DATETIME_MUTABLE)
->setParameter('endDate', $endDate, Types::DATETIME_MUTABLE);
return $nq->getResult();
}
}

View File

@@ -10,6 +10,7 @@ $chill-household-context: #929d69;
// Badges colors
$social-issue-color: #4bafe8;
$social-action-color: $orange;
$event-theme-color: #ecc546;
$activity-color: yellowgreen;
// budget colors

View File

@@ -21,10 +21,12 @@
>
<template #header>
<h2 class="modal-title">
{{ $t(getTextTitle) }}
{{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
<span class="sr-only">{{ $t("loading") }}</span>
<span class="sr-only">{{
trans(ADDRESS_LOADING)
}}</span>
</span>
</h2>
</template>
@@ -43,7 +45,7 @@
<template #footer>
<button @click="openEditPane" class="btn btn-create">
{{ $t("create_a_new_address") }}
{{ trans(CREATE_A_NEW_ADDRESS) }}
</button>
</template>
</modal>
@@ -62,13 +64,13 @@
>
<template #before v-if="!bypassFirstStep">
<a class="btn btn-cancel" @click="resetPane">
{{ $t("action.cancel") }}
{{ trans(CANCEL) }}
</a>
</template>
<template #action>
<li>
<button @click="openEditPane" class="btn btn-create">
{{ $t("create_a_new_address") }}
{{ trans(CREATE_A_NEW_ADDRESS) }}
</button>
</li>
</template>
@@ -85,10 +87,12 @@
>
<template #header>
<h2 class="modal-title">
{{ $t(getTextTitle) }}
{{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
<span class="sr-only">{{ $t("loading") }}</span>
<span class="sr-only">{{
trans(ADDRESS_LOADING)
}}</span>
</span>
</h2>
</template>
@@ -108,17 +112,17 @@
</template>
<template #footer>
<!--<button class="btn btn-cancel change-icon" @click="resetPane">{{ $t('action.cancel') }}</button>-->
<!--<button class="btn btn-cancel change-icon" @click="resetPane">{{ trans(CANCEL) }}</button>-->
<button
v-if="!this.context.edit && this.useDatePane"
class="btn btn-update change-icon"
@click="closeEditPane"
>
{{ $t("nav.next") }}
{{ trans(NEXT) }}
<i class="fa fa-fw fa-arrow-right" />
</button>
<button v-else class="btn btn-save" @click="closeEditPane">
{{ $t("action.save") }}
{{ trans(SAVE) }}
</button>
</template>
</modal>
@@ -139,7 +143,7 @@
>
<template #before>
<a class="btn btn-cancel" @click="resetPane">
{{ $t("action.cancel") }}
{{ trans(CANCEL) }}
</a>
</template>
<template #action>
@@ -148,13 +152,13 @@
class="btn btn-update change-icon"
@click="closeEditPane"
>
{{ $t("nav.next") }}
{{ trans(NEXT) }}
<i class="fa fa-fw fa-arrow-right" />
</button>
</li>
<li v-else>
<button class="btn btn-save" @click="closeEditPane">
{{ $t("action.save") }}
{{ trans(SAVE) }}
</button>
</li>
</template>
@@ -171,10 +175,12 @@
>
<template #header>
<h2 class="modal-title">
{{ $t(getTextTitle) }}
{{ trans(getTextTitle) }}
<span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
<span class="sr-only">{{ $t("loading") }}</span>
<span class="sr-only">{{
trans(ADDRESS_LOADING)
}}</span>
</span>
</h2>
</template>
@@ -193,10 +199,10 @@
<template #footer>
<button class="btn btn-misc" @click="openEditPane">
<i class="fa fa-fw fa-arrow-left" />
{{ $t("nav.previous") }}
{{ trans(PREVIOUS) }}
</button>
<button class="btn btn-save" @click="closeDatePane">
{{ $t("action.save") }}
{{ trans(SAVE) }}
</button>
<!-- -->
</template>
@@ -216,13 +222,13 @@
<template #before>
<button class="btn btn-misc" @click="openEditPane">
<i class="fa fa-fw fa-arrow-left" />
{{ $t("nav.previous") }}
{{ trans(PREVIOUS) }}
</button>
</template>
<template #action>
<li>
<button class="btn btn-save" @click="closeDatePane">
{{ $t("action.save") }}
{{ trans(SAVE) }}
</button>
</li>
</template>
@@ -244,9 +250,16 @@ import {
postPostalCode,
} from "../api";
import {
postAddressToPerson,
postAddressToHousehold,
} from "ChillPersonAssets/vuejs/_api/AddAddress.js";
CREATE_A_NEW_ADDRESS,
ADDRESS_LOADING,
ACTIVITY_CREATE_ADDRESS,
ACTIVITY_EDIT_ADDRESS,
CANCEL,
SAVE,
PREVIOUS,
NEXT,
trans,
} from "translator";
import ShowPane from "./ShowPane.vue";
import SuggestPane from "./SuggestPane.vue";
import EditPane from "./EditPane.vue";
@@ -254,6 +267,17 @@ import DatePane from "./DatePane.vue";
export default {
name: "AddAddress",
setup() {
return {
trans,
CREATE_A_NEW_ADDRESS,
ADDRESS_LOADING,
CANCEL,
SAVE,
PREVIOUS,
NEXT,
};
},
props: ["context", "options", "addressChangedCallback"],
components: {
Modal,
@@ -373,9 +397,11 @@ export default {
(this.options.title.edit !== null ||
this.options.title.create !== null)
) {
console.log("this.options.title", this.options.title);
return this.context.edit
? this.options.title.edit
: this.options.title.create;
? ACTIVITY_EDIT_ADDRESS
: ACTIVITY_CREATE_ADDRESS;
}
return this.context.edit
? this.defaultz.title.edit
@@ -505,7 +531,7 @@ export default {
getAddress(id)
.then(
(address) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.address = address;
this.flag.loading = false;
resolve();
@@ -522,7 +548,7 @@ export default {
fetchCountries()
.then(
(countries) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.loaded.countries = countries.results;
if (this.flag.showPane === true) {
this.closeShowPane();
@@ -550,7 +576,7 @@ export default {
fetchCities(country)
.then(
(cities) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.loaded.cities = cities.results.filter(
(c) => c.origin !== 3,
); // filter out user-defined cities
@@ -569,7 +595,7 @@ export default {
fetchReferenceAddresses(city)
.then(
(addresses) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.loaded.addresses = addresses.results;
this.flag.loading = false;
resolve();
@@ -800,7 +826,7 @@ export default {
return postAddress(payload)
.then(
(address) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.address = address;
this.flag.loading = false;
this.flag.success = true;
@@ -849,7 +875,7 @@ export default {
return patchAddress(payload.addressId, payload.newAddress)
.then(
(address) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.address = address;
this.flag.loading = false;
this.flag.success = true;

View File

@@ -1,6 +1,6 @@
<template>
<h4 class="h3">
{{ $t("fill_an_address") }}
{{ trans(ADDRESS_FILL_AN_ADDRESS) }}
</h4>
<div class="row my-3">
<div class="col-lg-6" v-if="!isNoAddress">
@@ -9,40 +9,40 @@
class="form-control"
type="text"
name="floor"
:placeholder="$t('floor')"
:placeholder="trans(ADDRESS_FLOOR)"
v-model="floor"
/>
<label for="floor">{{ $t("floor") }}</label>
<label for="floor">{{ trans(ADDRESS_FLOOR) }}</label>
</div>
<div class="form-floating my-1">
<input
class="form-control"
type="text"
name="corridor"
:placeholder="$t('corridor')"
:placeholder="trans(ADDRESS_CORRIDOR)"
v-model="corridor"
/>
<label for="corridor">{{ $t("corridor") }}</label>
<label for="corridor">{{ trans(ADDRESS_CORRIDOR) }}</label>
</div>
<div class="form-floating my-1">
<input
class="form-control"
type="text"
name="steps"
:placeholder="$t('steps')"
:placeholder="trans(ADDRESS_STEPS)"
v-model="steps"
/>
<label for="steps">{{ $t("steps") }}</label>
<label for="steps">{{ trans(ADDRESS_STEPS) }}</label>
</div>
<div class="form-floating my-1">
<input
class="form-control"
type="text"
name="flat"
:placeholder="$t('flat')"
:placeholder="trans(ADDRESS_FLAT)"
v-model="flat"
/>
<label for="flat">{{ $t("flat") }}</label>
<label for="flat">{{ trans(ADDRESS_FLAT) }}</label>
</div>
</div>
<div :class="isNoAddress ? 'col-lg-12' : 'col-lg-6'">
@@ -52,10 +52,12 @@
type="text"
name="buildingName"
maxlength="255"
:placeholder="$t('buildingName')"
:placeholder="trans(ADDRESS_BUILDING_NAME)"
v-model="buildingName"
/>
<label for="buildingName">{{ $t("buildingName") }}</label>
<label for="buildingName">{{
trans(ADDRESS_BUILDING_NAME)
}}</label>
</div>
<div class="form-floating my-1">
<input
@@ -63,10 +65,10 @@
type="text"
name="extra"
maxlength="255"
:placeholder="$t('extra')"
:placeholder="trans(ADDRESS_EXTRA)"
v-model="extra"
/>
<label for="extra">{{ $t("extra") }}</label>
<label for="extra">{{ trans(ADDRESS_EXTRA) }}</label>
</div>
<div class="form-floating my-1" v-if="!isNoAddress">
<input
@@ -74,18 +76,48 @@
type="text"
name="distribution"
maxlength="255"
:placeholder="$t('distribution')"
:placeholder="trans(ADDRESS_DISTRIBUTION)"
v-model="distribution"
/>
<label for="distribution">{{ $t("distribution") }}</label>
<label for="distribution">{{
trans(ADDRESS_DISTRIBUTION)
}}</label>
</div>
</div>
</div>
</template>
<script>
import {
ADDRESS_STREET,
ADDRESS_STREET_NUMBER,
ADDRESS_FLOOR,
ADDRESS_CORRIDOR,
ADDRESS_STEPS,
ADDRESS_FLAT,
ADDRESS_BUILDING_NAME,
ADDRESS_DISTRIBUTION,
ADDRESS_EXTRA,
ADDRESS_FILL_AN_ADDRESS,
trans,
} from "translator";
export default {
name: "AddressMore",
setup() {
return {
ADDRESS_STREET,
ADDRESS_STREET_NUMBER,
ADDRESS_FLOOR,
ADDRESS_CORRIDOR,
ADDRESS_STEPS,
ADDRESS_FLAT,
ADDRESS_BUILDING_NAME,
ADDRESS_DISTRIBUTION,
ADDRESS_EXTRA,
ADDRESS_FILL_AN_ADDRESS,
trans,
};
},
props: ["entity", "isNoAddress"],
computed: {
floor: {

View File

@@ -1,16 +1,16 @@
<template>
<div class="my-1">
<label class="col-form-label" for="addressSelector">{{
$t("address")
trans(ADDRESS_ADDRESS)
}}</label>
<VueMultiselect
id="addressSelector"
v-model="value"
:placeholder="$t('select_address')"
:tag-placeholder="$t('create_address')"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('create_address')"
:selected-label="$t('multiselect.selected_label')"
:placeholder="trans(ADDRESS_SELECT_ADDRESS)"
:tag-placeholder="trans(ADDRESS_CREATE_ADDRESS)"
:select-label="trans(MULTISELECT_SELECT_LABEL)"
:deselect-label="trans(ADDRESS_CREATE_ADDRESS)"
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
@search-change="listenInputSearch"
:internal-search="false"
ref="addressSelector"
@@ -42,10 +42,10 @@
class="form-control"
type="text"
name="street"
:placeholder="$t('street')"
:placeholder="trans(ADDRESS_STREET)"
v-model="street"
/>
<label for="street">{{ $t("street") }}</label>
<label for="street">{{ trans(ADDRESS_STREET) }}</label>
</div>
</div>
<div class="col-2">
@@ -54,10 +54,12 @@
class="form-control"
type="text"
name="streetNumber"
:placeholder="$t('streetNumber')"
:placeholder="trans(ADDRESS_STREET_NUMBER)"
v-model="streetNumber"
/>
<label for="streetNumber">{{ $t("streetNumber") }}</label>
<label for="streetNumber">{{
trans(ADDRESS_STREET_NUMBER)
}}</label>
</div>
</div>
</div>
@@ -69,10 +71,32 @@ import {
searchReferenceAddresses,
fetchReferenceAddresses,
} from "../../api.js";
import {
ADDRESS_STREET,
ADDRESS_STREET_NUMBER,
ADDRESS_ADDRESS,
MULTISELECT_SELECTED_LABEL,
MULTISELECT_SELECT_LABEL,
ADDRESS_SELECT_ADDRESS,
ADDRESS_CREATE_ADDRESS,
trans,
} from "translator";
export default {
name: "AddressSelection",
components: { VueMultiselect },
setup() {
return {
ADDRESS_STREET,
ADDRESS_STREET_NUMBER,
ADDRESS_ADDRESS,
MULTISELECT_SELECTED_LABEL,
MULTISELECT_SELECT_LABEL,
ADDRESS_SELECT_ADDRESS,
ADDRESS_CREATE_ADDRESS,
trans,
};
},
props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"],
data() {
return {
@@ -150,7 +174,7 @@ export default {
searchReferenceAddresses(query, this.entity.selected.city)
.then(
(addresses) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.loaded.addresses =
addresses.results;
this.isLoading = false;
@@ -168,7 +192,7 @@ export default {
fetchReferenceAddresses(this.entity.selected.city)
.then(
(addresses) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.loaded.addresses =
addresses.results;
this.isLoading = false;

View File

@@ -1,6 +1,6 @@
<template>
<div class="my-1">
<label class="col-form-label">{{ $t("city") }}</label>
<label class="col-form-label">{{ trans(ADDRESS_CITY) }}</label>
<VueMultiselect
id="citySelector"
v-model="value"
@@ -12,15 +12,15 @@
track-by="id"
label="value"
:custom-label="transName"
:placeholder="$t('select_city')"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('create_postal_code')"
:selected-label="$t('multiselect.selected_label')"
:placeholder="trans(ADDRESS_SELECT_CITY)"
:select-label="trans(MULTISELECT_SELECT_LABEL)"
:deselect-label="trans(ADDRESS_CREATE_POSTAL_CODE)"
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
:taggable="true"
:multiple="false"
:internal-search="false"
@tag="addPostcode"
:tag-placeholder="$t('create_postal_code')"
:tag-placeholder="trans(ADDRESS_CREATE_POSTAL_CODE)"
:loading="isLoading"
:options="cities"
/>
@@ -36,10 +36,10 @@
class="form-control"
type="text"
id="code"
:placeholder="$t('postalCode_code')"
:placeholder="trans(ADDRESS_POSTAL_CODE_CODE)"
v-model="code"
/>
<label for="code">{{ $t("postalCode_code") }}</label>
<label for="code">{{ trans(ADDRESS_POSTAL_CODE_CODE) }}</label>
</div>
</div>
<div class="col-8">
@@ -48,10 +48,10 @@
class="form-control"
type="text"
id="name"
:placeholder="$t('postalCode_name')"
:placeholder="trans(ADDRESS_POSTAL_CODE_NAME)"
v-model="name"
/>
<label for="name">{{ $t("postalCode_name") }}</label>
<label for="name">{{ trans(ADDRESS_POSTAL_CODE_NAME) }}</label>
</div>
</div>
</div>
@@ -60,10 +60,32 @@
<script>
import VueMultiselect from "vue-multiselect";
import { searchCities, fetchCities } from "../../api.js";
import {
MULTISELECT_SELECTED_LABEL,
MULTISELECT_SELECT_LABEL,
ADDRESS_POSTAL_CODE_CODE,
ADDRESS_POSTAL_CODE_NAME,
ADDRESS_CREATE_POSTAL_CODE,
ADDRESS_CITY,
ADDRESS_SELECT_CITY,
trans,
} from "translator";
export default {
name: "CitySelection",
components: { VueMultiselect },
setup() {
return {
MULTISELECT_SELECTED_LABEL,
MULTISELECT_SELECT_LABEL,
ADDRESS_CITY,
ADDRESS_SELECT_CITY,
ADDRESS_POSTAL_CODE_CODE,
ADDRESS_POSTAL_CODE_NAME,
ADDRESS_CREATE_POSTAL_CODE,
trans,
};
},
props: [
"entity",
"context",
@@ -167,7 +189,7 @@ export default {
searchCities(query, this.entity.selected.country)
.then(
(cities) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.loaded.cities =
cities.results.filter(
(c) => c.origin !== 3,
@@ -187,7 +209,7 @@ export default {
fetchCities(this.entity.selected.country)
.then(
(cities) =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
this.entity.loaded.cities =
cities.results.filter(
(c) => c.origin !== 3,

View File

@@ -1,19 +1,19 @@
<template>
<div class="my-1">
<label class="col-form-label" for="countrySelect">{{
$t("country")
trans(ADDRESS_COUNTRY)
}}</label>
<VueMultiselect
id="countrySelect"
label="name"
track-by="id"
:custom-label="transName"
:placeholder="$t('select_country')"
:placeholder="trans(ADDRESS_SELECT_COUNTRY)"
:options="sortedCountries"
v-model="value"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
:select-label="trans(MULTISELECT_SELECT_LABEL)"
:deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
@select="selectCountry"
@remove="remove"
/>
@@ -23,10 +23,28 @@
<script>
import VueMultiselect from "vue-multiselect";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import {
MULTISELECT_SELECTED_LABEL,
MULTISELECT_SELECT_LABEL,
MULTISELECT_DESELECT_LABEL,
ADDRESS_COUNTRY,
ADDRESS_SELECT_COUNTRY,
trans,
} from "translator";
export default {
name: "CountrySelection",
components: { VueMultiselect },
setup() {
return {
MULTISELECT_SELECTED_LABEL,
MULTISELECT_SELECT_LABEL,
MULTISELECT_DESELECT_LABEL,
ADDRESS_COUNTRY,
ADDRESS_SELECT_COUNTRY,
trans,
};
},
props: ["context", "entity", "flag", "checkErrors"],
emits: ["getCities"],
data() {

View File

@@ -18,7 +18,7 @@
</div>
<h4 class="h3">
{{ $t("select_an_address_title") }}
{{ trans(ADDRESS_SELECT_AN_ADDRESS_TITLE) }}
</h4>
<div class="row my-3">
<div class="col-lg-6">
@@ -31,7 +31,7 @@
:value="valueConfidential"
/>
<label class="form-check-label" for="isConfidential">
{{ $t("isConfidential") }}
{{ trans(ADDRESS_IS_CONFIDENTIAL) }}
</label>
</div>
<div class="form-check">
@@ -43,7 +43,7 @@
:value="value"
/>
<label class="form-check-label" for="isNoAddress">
{{ $t("isNoAddress") }}
{{ trans(ADDRESS_IS_NO_ADDRESS) }}
</label>
</div>
@@ -108,6 +108,12 @@ import AddressSelection from "./AddAddress/AddressSelection";
import AddressMap from "./AddAddress/AddressMap";
import AddressMore from "./AddAddress/AddressMore";
import ActionButtons from "./ActionButtons.vue";
import {
ADDRESS_SELECT_AN_ADDRESS_TITLE,
ADDRESS_IS_CONFIDENTIAL,
ADDRESS_IS_NO_ADDRESS,
trans,
} from "translator";
export default {
name: "EditPane",
@@ -119,6 +125,14 @@ export default {
AddressMore,
ActionButtons,
},
setup() {
return {
trans,
ADDRESS_SELECT_AN_ADDRESS_TITLE,
ADDRESS_IS_CONFIDENTIAL,
ADDRESS_IS_NO_ADDRESS,
};
},
props: [
"context",
"options",

View File

@@ -5,7 +5,7 @@
v-if="flag.loading"
class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"
/>
<span class="sr-only">{{ $t("loading") }}</span>
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
</div>
<div v-if="errorMsg && errorMsg.length > 0" class="alert alert-danger">
@@ -13,8 +13,10 @@
</div>
<div v-if="flag.success" class="alert alert-success">
{{ $t(getSuccessText) }}
<span v-if="forceRedirect">{{ $t("wait_redirection") }}</span>
{{ trans(getSuccessText) }}
<span v-if="forceRedirect">{{
trans(ADDRESS_WAIT_REDIRECTION)
}}</span>
</div>
<div
@@ -28,7 +30,7 @@
<div class="no-address-yet">
<i class="fa fa-map-marker" aria-hidden="true" />
<p class="chill-no-data-statement">
{{ $t("not_yet_address") }}
{{ trans(ADDRESS_NOT_YET_ADDRESS) }}
</p>
<action-buttons
@@ -43,10 +45,10 @@
:class="getClassButton"
type="button"
name="button"
:title="$t(getTextButton)"
:title="trans(getTextButton)"
>
<span v-if="displayTextButton">{{
$t(getTextButton)
trans(getTextButton)
}}</span>
</button>
</template>
@@ -71,10 +73,10 @@
:class="getClassButton"
type="button"
name="button"
:title="$t(getTextButton)"
:title="trans(getTextButton)"
>
<span v-if="displayTextButton">{{
$t(getTextButton)
trans(getTextButton)
}}</span>
</button>
</template>
@@ -95,10 +97,10 @@
:class="getClassButton"
type="button"
name="button"
:title="$t(getTextButton)"
:title="trans(getTextButton)"
>
<span v-if="displayTextButton">{{
$t(getTextButton)
trans(getTextButton)
}}</span>
</button>
</template>
@@ -109,13 +111,36 @@
<script>
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
import ActionButtons from "./ActionButtons.vue";
import {
ACTIVITY_CREATE_ADDRESS,
ACTIVITY_EDIT_ADDRESS,
ADDRESS_NOT_YET_ADDRESS,
ADDRESS_WAIT_REDIRECTION,
ADDRESS_LOADING,
ADDRESS_ADDRESS_EDIT_SUCCESS,
ADDRESS_ADDRESS_NEW_SUCCESS,
trans,
} from "translator";
export default {
name: "ShowPane",
methods: {},
components: {
AddressRenderBox,
ActionButtons,
},
setup() {
return {
trans,
ACTIVITY_CREATE_ADDRESS,
ACTIVITY_EDIT_ADDRESS,
ADDRESS_NOT_YET_ADDRESS,
ADDRESS_WAIT_REDIRECTION,
ADDRESS_LOADING,
ADDRESS_ADDRESS_NEW_SUCCESS,
ADDRESS_ADDRESS_EDIT_SUCCESS,
};
},
props: [
"context",
"defaultz",
@@ -156,18 +181,20 @@ export default {
(this.options.button.text.edit !== null ||
this.options.button.text.create !== null)
) {
// console.log('this.options.button.text', this.options.button.text)
return this.context.edit
? this.options.button.text.edit
: this.options.button.text.create;
? ACTIVITY_CREATE_ADDRESS
: ACTIVITY_EDIT_ADDRESS;
}
console.log("defaultz", this.defaultz);
return this.context.edit
? this.defaultz.button.text.edit
: this.defaultz.button.text.create;
},
getSuccessText() {
return this.context.edit
? "address_edit_success"
: "address_new_success";
? ADDRESS_ADDRESS_EDIT_SUCCESS
: ADDRESS_ADDRESS_NEW_SUCCESS;
},
onlyButton() {
return typeof this.options.onlyButton !== "undefined"

View File

@@ -64,3 +64,5 @@ const props = defineProps({
entity: Object,
});
</script>
thirdparty_duplicate: merge: Fussioner find: 'Désigner un tiers doublon'

View File

@@ -44,6 +44,17 @@
{% endif %}
{% endif %}
{% endblock content_view_actions_duplicate_link %}
{% block content_view_actions_merge %}
<li>
<a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
{ 'thirdparty_id': entity.id }) }}"
title="{{ 'Merge'|trans }}"
class="btn btn-misc">
<i class="bi bi-chevron-contract"></i>
{{ 'Merge'|trans }}
</a>
</li>
{% endblock %}
{% block content_view_actions_edit_link %}
{% if chill_crud_action_exists(crud_name, 'edit') %}
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}

View File

@@ -5,7 +5,7 @@
<strong><i class="fa fa-fw fa-filter"></i>Filtrer la liste</strong>
</button>
</h2>
<div class="accordion-collapse collapse" id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion">
<div id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion">
{% set btnSubmit = 0 %}
<div class="accordion-body chill_filter_order container-xxl p-5 py-2">
<div class="row my-2">
@@ -68,10 +68,17 @@
{{ form_label(form.entity_choices[checkbox_name])}}
{% endif %}
<div class="col-sm-8 pt-2">
{% for c in form['entity_choices'][checkbox_name].children %}
{{ form_widget(c) }}
{{ form_label(c) }}
{% endfor %}
{% set field = form['entity_choices'][checkbox_name] %}
{% if field.vars.expanded %}
{# Render expanded checkboxes/radios #}
{% for c in field.children %}
{{ form_widget(c) }}
{{ form_label(c) }}
{% endfor %}
{% else %}
{# Render select dropdown #}
{{ form_widget(field) }}
{% endif %}
</div>
</div>
{% endfor %}

View File

@@ -18,8 +18,9 @@
{%- endif -%}
{%- endblock form_label %}
{# this has been rewritten for chill #}
{% block form_label_class -%}
col-sm-4
{% if 'div_col_width' in label_attr|default({})|keys %}{% if label_attr['div_col_width'] is not same as false %}{{ label_attr['div_col_width'] }}{% endif %}{% else %}col-sm-4{% endif %}
{%- endblock form_label_class %}
{# Rows #}

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