mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-11-19 02:17:45 +00:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1b91ebbfd
|
|||
| 2139b53fb0 | |||
| a43181d60d | |||
| 04bc1c5de8 | |||
| 0a07d68b6d | |||
| fccd29e3c7 | |||
| 274ee94196 | |||
| 799d04142e | |||
| dfe8d8b0bf | |||
| 82f347b93a | |||
| 635efd6f1d | |||
| 869880d8f3 | |||
| f7ea7e4dbf | |||
| 0a58e05230 | |||
| 68c83223dd | |||
| c28bd22560 | |||
| a5ef2475fb | |||
| 86dd9bfb80 | |||
| c28670f0fd | |||
| 9e2c030224 | |||
| a706c6f337 | |||
| bc63b489ee | |||
| a4cfc6a178 | |||
| f75d1da3b1 | |||
|
b8b68e5e5a
|
|||
|
ae5ba67064
|
|||
|
bfe4dd3aec
|
|||
| 3a4c20b53d | |||
| b0c86e238d | |||
| d7614aeab2 | |||
| 671ed21d59 | |||
| 4b9db6ceb6 | |||
| c79c39b562 | |||
| bf768b8e99 | |||
| 2df01833ad | |||
| ffb8183d4d | |||
| 5d45339bf7 | |||
| e87e5cbbaf | |||
| fa8e92ebf5 | |||
| b7a92bf656 | |||
| 3dbbda7b64 | |||
| 769d76a0cc | |||
| 722b37fbcc | |||
| bf38ec22c9 | |||
| 3d99c0f561 | |||
| 2221d17930 | |||
| 9c2abb2dfa | |||
| 94744b9542 | |||
| f42bb498e4 | |||
| 01889ac671 | |||
| 62e5842311 | |||
|
8ad6f397a8
|
|||
| d713704633 | |||
| b1fa9242a0 | |||
| 6ac554f93a | |||
| 372d8e5825 | |||
| 10f05e5559 | |||
| ddb2a65419 | |||
| 8d40a8089f | |||
| e1bf4a24d2 | |||
| 208a378185 | |||
| 9089c8959b | |||
|
1b9b581c31
|
|||
| aa1abe4c88 | |||
| d82c9cc9a7 | |||
| a7e3b1c5d2 | |||
| 84cf11933d | |||
| bc2fbee5c6 | |||
| ebd10ca522 | |||
|
d3a31be412
|
|||
|
d159a82f88
|
|||
| 74c9eb5585 | |||
| f93c7e014f | |||
| e6a799abc4 | |||
| 68a0ef7115 | |||
| 1675c56f3d | |||
| 675e8450fc | |||
| 4ffd7034d0 |
14
.changes/v4.6.0.md
Normal file
14
.changes/v4.6.0.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
## v4.6.0 - 2025-10-15
|
||||||
|
### Feature
|
||||||
|
* ([#423](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/423)) Create environment banner that can be activated and configured depending on the image deployed
|
||||||
|
* ([#394](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/394)) Only show active workflow on the page "my tracked workflow"
|
||||||
|
### Fixed
|
||||||
|
* Fix loading of classLists in SocialIssuesAcc.vue, ensure elements are present
|
||||||
|
* Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted
|
||||||
|
* ([#434](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/434)) Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists
|
||||||
|
* Fix loading of social issues and social actions within vue component
|
||||||
|
* ([#446](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/446)) Add unique condition on stored object filename, with cleaning step on existing duplicate filenames
|
||||||
|
|
||||||
|
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
|
||||||
|
* [workflow] take permissions into account to delete the workflow attachment
|
||||||
|
* ([#448](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/448)) Fix the execution of daily cronjob notification, when the previous last execution storage was invalid
|
||||||
3
.changes/v4.6.1.md
Normal file
3
.changes/v4.6.1.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## v4.6.1 - 2025-10-27
|
||||||
|
### Fixed
|
||||||
|
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php
|
||||||
21
.changes/v4.7.0.md
Normal file
21
.changes/v4.7.0.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## v4.7.0 - 2025-11-10
|
||||||
|
### Feature
|
||||||
|
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
|
||||||
|
* ([#404](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/404)) Add columns for comments linked to an activity in the activity list export
|
||||||
|
### Fixed
|
||||||
|
* ([#451](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/451)) Fix: display also social actions linked to parents of the selected social issue
|
||||||
|
* ([#453](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/453)) Fix: export actions and their results in csv even when action does not have any goals attached to it.
|
||||||
|
* Fix the possibility to delete a workflow
|
||||||
|
|
||||||
|
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
|
||||||
|
* ([#457](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/457)) Fix the fusion of thirdparty properties that are located in another schema than public for TO_ONE relations + add extra loop for MANY_TO_MANY relations where thirdparty is the source instead of the target
|
||||||
|
* ([#428](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/428)) Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
|
||||||
|
### DX
|
||||||
|
* Send notifications log to dedicated channel, if it exists
|
||||||
|
|
||||||
|
### UX
|
||||||
|
* ([#425](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/425)) Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
|
||||||
|
* ([#542](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/542)) Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
|
||||||
|
* Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
|
||||||
|
* ([#455](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/455)) Change the order of display for results and objectives in the social work/action form
|
||||||
|
* Wrap text when it is too long within badges
|
||||||
9
.changes/v4.8.0.md
Normal file
9
.changes/v4.8.0.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## v4.8.0 - 2025-11-17
|
||||||
|
### Feature
|
||||||
|
* ([#461](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/461)) Make a calendar item on the 'mes rendez-vous' page clickable. Clicking will navigate to the edit page of the calendar item.
|
||||||
|
### Fixed
|
||||||
|
* ([#463](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/463)) Display calendar items for which an invite was accepted on the mes rendez-vous page
|
||||||
|
* Improve accessibility on login page
|
||||||
|
|
||||||
|
### UX
|
||||||
|
* ([#449](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/449)) Remove the label if there is only one scope and no scope picking field is displayed.
|
||||||
@@ -240,9 +240,6 @@ The tests are run from the project's root (not from the bundle's root).
|
|||||||
# Run all tests
|
# Run all tests
|
||||||
vendor/bin/phpunit
|
vendor/bin/phpunit
|
||||||
|
|
||||||
# Run tests for a specific bundle
|
|
||||||
vendor/bin/phpunit --testsuite NameBundle
|
|
||||||
|
|
||||||
# Run a specific test file
|
# Run a specific test file
|
||||||
vendor/bin/phpunit path/to/TestFile.php
|
vendor/bin/phpunit path/to/TestFile.php
|
||||||
|
|
||||||
@@ -250,6 +247,9 @@ vendor/bin/phpunit path/to/TestFile.php
|
|||||||
vendor/bin/phpunit --filter methodName path/to/TestFile.php
|
vendor/bin/phpunit --filter methodName path/to/TestFile.php
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When writing tests, only test specific files. Do not run all tests or the full
|
||||||
|
test suite.
|
||||||
|
|
||||||
#### Test Structure
|
#### Test Structure
|
||||||
|
|
||||||
Tests are organized by bundle and follow the same structure as the bundle itself:
|
Tests are organized by bundle and follow the same structure as the bundle itself:
|
||||||
|
|||||||
51
CHANGELOG.md
51
CHANGELOG.md
@@ -6,6 +6,57 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||||
|
|
||||||
|
|
||||||
|
## v4.8.0 - 2025-11-17
|
||||||
|
### Feature
|
||||||
|
* ([#461](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/461)) Make a calendar item on the 'mes rendez-vous' page clickable. Clicking will navigate to the edit page of the calendar item.
|
||||||
|
### Fixed
|
||||||
|
* ([#463](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/463)) Display calendar items for which an invite was accepted on the mes rendez-vous page
|
||||||
|
* Improve accessibility on login page
|
||||||
|
|
||||||
|
### UX
|
||||||
|
* ([#449](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/449)) Remove the label if there is only one scope and no scope picking field is displayed.
|
||||||
|
|
||||||
|
## v4.7.0 - 2025-11-10
|
||||||
|
### Feature
|
||||||
|
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
|
||||||
|
* ([#404](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/404)) Add columns for comments linked to an activity in the activity list export
|
||||||
|
### Fixed
|
||||||
|
* ([#451](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/451)) Fix: display also social actions linked to parents of the selected social issue
|
||||||
|
* ([#453](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/453)) Fix: export actions and their results in csv even when action does not have any goals attached to it.
|
||||||
|
* Fix the possibility to delete a workflow
|
||||||
|
|
||||||
|
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
|
||||||
|
* ([#457](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/457)) Fix the fusion of thirdparty properties that are located in another schema than public for TO_ONE relations + add extra loop for MANY_TO_MANY relations where thirdparty is the source instead of the target
|
||||||
|
* ([#428](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/428)) Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
|
||||||
|
### DX
|
||||||
|
* Send notifications log to dedicated channel, if it exists
|
||||||
|
|
||||||
|
### UX
|
||||||
|
* ([#425](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/425)) Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
|
||||||
|
* ([#542](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/542)) Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
|
||||||
|
* Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
|
||||||
|
* ([#455](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/455)) Change the order of display for results and objectives in the social work/action form
|
||||||
|
* Wrap text when it is too long within badges
|
||||||
|
|
||||||
|
## v4.6.1 - 2025-10-27
|
||||||
|
### Fixed
|
||||||
|
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php
|
||||||
|
|
||||||
|
## v4.6.0 - 2025-10-15
|
||||||
|
### Feature
|
||||||
|
* ([#423](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/423)) Create environment banner that can be activated and configured depending on the image deployed
|
||||||
|
* ([#394](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/394)) Only show active workflow on the page "my tracked workflow"
|
||||||
|
### Fixed
|
||||||
|
* Fix loading of classLists in SocialIssuesAcc.vue, ensure elements are present
|
||||||
|
* Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted
|
||||||
|
* ([#434](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/434)) Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists
|
||||||
|
* Fix loading of social issues and social actions within vue component
|
||||||
|
* ([#446](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/446)) Add unique condition on stored object filename, with cleaning step on existing duplicate filenames
|
||||||
|
|
||||||
|
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
|
||||||
|
* [workflow] take permissions into account to delete the workflow attachment
|
||||||
|
* ([#448](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/448)) Fix the execution of daily cronjob notification, when the previous last execution storage was invalid
|
||||||
|
|
||||||
## v4.5.1 - 2025-10-03
|
## v4.5.1 - 2025-10-03
|
||||||
### Fixed
|
### Fixed
|
||||||
* Add missing javascript dependency
|
* Add missing javascript dependency
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-redis": "*",
|
"ext-redis": "*",
|
||||||
"ext-zlib": "*",
|
"ext-zlib": "*",
|
||||||
"champs-libres/wopi-bundle": "dev-master@dev",
|
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
||||||
"champs-libres/wopi-lib": "dev-master@dev",
|
"champs-libres/wopi-lib": "dev-master@dev",
|
||||||
"doctrine/data-fixtures": "^1.8",
|
"doctrine/data-fixtures": "^1.8",
|
||||||
"doctrine/doctrine-bundle": "^2.1",
|
"doctrine/doctrine-bundle": "^2.1",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
|
|
||||||
ChampsLibres\WopiBundle\WopiBundle::class => ['all' => true],
|
ChampsLibres\WopiBundle\WopiBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||||
@@ -37,4 +36,5 @@ return [
|
|||||||
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
|
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||||
|
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
chill_main:
|
chill_main:
|
||||||
available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
|
available_languages: [ '%env(resolve:LOCALE)%', 'en', 'nl' ]
|
||||||
available_countries: ['BE', 'FR']
|
available_countries: ['BE', 'FR']
|
||||||
|
top_banner:
|
||||||
|
visible: false
|
||||||
|
text:
|
||||||
|
fr: 'Vous travaillez actuellement avec la version de PRÉ-PRODUCTION.'
|
||||||
|
nl: 'Je werkt momenteel in de PRE-PRODUCTIE versie'
|
||||||
|
color: '#353535'
|
||||||
|
background_color: '#d8bb48'
|
||||||
notifications:
|
notifications:
|
||||||
from_email: '%env(resolve:NOTIFICATION_FROM_EMAIL)%'
|
from_email: '%env(resolve:NOTIFICATION_FROM_EMAIL)%'
|
||||||
from_name: '%env(resolve:NOTIFICATION_FROM_NAME)%'
|
from_name: '%env(resolve:NOTIFICATION_FROM_NAME)%'
|
||||||
|
|||||||
2
config/packages/chill_aside_activity.yaml
Normal file
2
config/packages/chill_aside_activity.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
chill_aside_activity:
|
||||||
|
show_concerned_persons_count: hidden
|
||||||
@@ -23,8 +23,8 @@ class "Document" {
|
|||||||
- text description
|
- text description
|
||||||
- ArrayCollection_DocumentCategory categories
|
- ArrayCollection_DocumentCategory categories
|
||||||
- varchar_150 content #link to openstack
|
- varchar_150 content #link to openstack
|
||||||
- Center center
|
- Territoire territoire
|
||||||
- Cercle cercle
|
- Service service
|
||||||
- User user
|
- User user
|
||||||
- DateTime date # Creation date
|
- DateTime date # Creation date
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Certaines données sont historisées:
|
|||||||
|
|
||||||
- les référents d'un parcours;
|
- les référents d'un parcours;
|
||||||
- les statuts d'un parcours;
|
- les statuts d'un parcours;
|
||||||
- la liaison entre les centres et les usagers;
|
- la liaison entre les territoires et les usagers;
|
||||||
- etc.
|
- etc.
|
||||||
|
|
||||||
Dans ces cas-là, Chill crée généralement deux colonnes, qui sont habituellement nommées :code:`startDate` et :code:`endDate`. Lorsque la colonne :code:`endDate` est à :code:`NULL`, cela signifie que la période n'est pas "fermée". La colonne :code:`startDate` n'est pas nullable.
|
Dans ces cas-là, Chill crée généralement deux colonnes, qui sont habituellement nommées :code:`startDate` et :code:`endDate`. Lorsque la colonne :code:`endDate` est à :code:`NULL`, cela signifie que la période n'est pas "fermée". La colonne :code:`startDate` n'est pas nullable.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
order,table_schema,table_name,commentaire
|
order,table_schema,table_name,commentaire
|
||||||
1,chill_3party,party_category,Catégorie de tiers
|
1,chill_3party,party_category,Catégorie de tiers
|
||||||
2,chill_3party,party_center,Association entre les tiers et les centres (déprécié)
|
2,chill_3party,party_center,Association entre les tiers et les territoires (déprécié)
|
||||||
3,chill_3party,party_profession,Profession du tiers (déprécié)
|
3,chill_3party,party_profession,Profession du tiers (déprécié)
|
||||||
4,chill_3party,third_party,Tiers
|
4,chill_3party,third_party,Tiers
|
||||||
5,chill_3party,thirdparty_category,association tiers - catégories
|
5,chill_3party,thirdparty_category,association tiers - catégories
|
||||||
@@ -54,7 +54,7 @@ order,table_schema,table_name,commentaire
|
|||||||
53,public,activitytpresence,Présence aux échanges
|
53,public,activitytpresence,Présence aux échanges
|
||||||
54,public,activitytype,Types d'échanges
|
54,public,activitytype,Types d'échanges
|
||||||
55,public,activitytypecategory,Catégories de types d'échanges
|
55,public,activitytypecategory,Catégories de types d'échanges
|
||||||
56,public,centers,"Centres (territoires, agences, etc.)"
|
56,public,centers,"Territoires (territoires, agences, etc.)"
|
||||||
57,public,chill_activity_activity_chill_person_socialaction,
|
57,public,chill_activity_activity_chill_person_socialaction,
|
||||||
58,public,chill_activity_activity_chill_person_socialissue
|
58,public,chill_activity_activity_chill_person_socialissue
|
||||||
59,public,chill_docgen_template,Gabarits de documents
|
59,public,chill_docgen_template,Gabarits de documents
|
||||||
@@ -111,7 +111,7 @@ order,table_schema,table_name,commentaire
|
|||||||
110,public,chill_person_marital_status,Etats civils
|
110,public,chill_person_marital_status,Etats civils
|
||||||
111,public,chill_person_not_duplicate,
|
111,public,chill_person_not_duplicate,
|
||||||
112,public,chill_person_person,Usagers
|
112,public,chill_person_person,Usagers
|
||||||
113,public,chill_person_person_center_history,Historique des centres d'un usagers
|
113,public,chill_person_person_center_history,Historique des territoires d'un usagers
|
||||||
114,public,chill_person_persons_to_addresses,Déprécié
|
114,public,chill_person_persons_to_addresses,Déprécié
|
||||||
115,public,chill_person_phone,Numéros d etéléphone supplémentaires d'un usager
|
115,public,chill_person_phone,Numéros d etéléphone supplémentaires d'un usager
|
||||||
116,public,chill_person_relations,Types de relations de filiation
|
116,public,chill_person_relations,Types de relations de filiation
|
||||||
@@ -142,7 +142,7 @@ order,table_schema,table_name,commentaire
|
|||||||
141,public,permission_groups
|
141,public,permission_groups
|
||||||
142,public,permissionsgroup_rolescope
|
142,public,permissionsgroup_rolescope
|
||||||
143,public,persons_spoken_languages
|
143,public,persons_spoken_languages
|
||||||
144,public,regroupment,Regroupement de centres
|
144,public,regroupment,Regroupement de territoires
|
||||||
145,public,regroupment_center,
|
145,public,regroupment_center,
|
||||||
146,public,role_scopes,
|
146,public,role_scopes,
|
||||||
147,public,scopes,Services
|
147,public,scopes,Services
|
||||||
|
|||||||
|
@@ -66,6 +66,9 @@ class ListActivityHelper
|
|||||||
->leftJoin('activity.location', 'location')
|
->leftJoin('activity.location', 'location')
|
||||||
->addSelect('location.name AS locationName')
|
->addSelect('location.name AS locationName')
|
||||||
->addSelect('activity.sentReceived')
|
->addSelect('activity.sentReceived')
|
||||||
|
->addSelect('activity.comment.comment AS commentText')
|
||||||
|
->addSelect('activity.comment.date AS commentDate')
|
||||||
|
->addSelect('JSON_BUILD_OBJECT(\'uid\', activity.comment.userId, \'d\', activity.comment.date) AS commentUser')
|
||||||
->addSelect('JSON_BUILD_OBJECT(\'uid\', IDENTITY(activity.createdBy), \'d\', activity.createdAt) AS createdBy')
|
->addSelect('JSON_BUILD_OBJECT(\'uid\', IDENTITY(activity.createdBy), \'d\', activity.createdAt) AS createdBy')
|
||||||
->addSelect('activity.createdAt')
|
->addSelect('activity.createdAt')
|
||||||
->addSelect('JSON_BUILD_OBJECT(\'uid\', IDENTITY(activity.updatedBy), \'d\', activity.updatedAt) AS updatedBy')
|
->addSelect('JSON_BUILD_OBJECT(\'uid\', IDENTITY(activity.updatedBy), \'d\', activity.updatedAt) AS updatedBy')
|
||||||
@@ -87,6 +90,8 @@ class ListActivityHelper
|
|||||||
'createdAt', 'updatedAt' => $this->dateTimeHelper->getLabel($key),
|
'createdAt', 'updatedAt' => $this->dateTimeHelper->getLabel($key),
|
||||||
'createdBy', 'updatedBy' => $this->userHelper->getLabel($key, $values, $key),
|
'createdBy', 'updatedBy' => $this->userHelper->getLabel($key, $values, $key),
|
||||||
'date' => $this->dateTimeHelper->getLabel(self::MSG_KEY.$key),
|
'date' => $this->dateTimeHelper->getLabel(self::MSG_KEY.$key),
|
||||||
|
'commentDate' => $this->dateTimeHelper->getLabel(self::MSG_KEY.'comment_date'),
|
||||||
|
'commentUser' => $this->userHelper->getLabel($key, $values, self::MSG_KEY.'comment_user'),
|
||||||
'attendeeName' => function ($value) {
|
'attendeeName' => function ($value) {
|
||||||
if ('_header' === $value) {
|
if ('_header' === $value) {
|
||||||
return 'Attendee';
|
return 'Attendee';
|
||||||
@@ -176,6 +181,9 @@ class ListActivityHelper
|
|||||||
'usersNames',
|
'usersNames',
|
||||||
'thirdPartiesIds',
|
'thirdPartiesIds',
|
||||||
'thirdPartiesNames',
|
'thirdPartiesNames',
|
||||||
|
'commentText',
|
||||||
|
'commentDate',
|
||||||
|
'commentUser',
|
||||||
'createdBy',
|
'createdBy',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedBy',
|
'updatedBy',
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt
|
|||||||
|
|
||||||
public function getFormDefaultData(): array
|
public function getFormDefaultData(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [
|
||||||
|
'reasons' => [],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array
|
public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
|
|
||||||
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
|
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
|
||||||
{
|
{
|
||||||
|
error_log('alterQuery called with data: '.json_encode(array_keys($data)));
|
||||||
|
|
||||||
// create a subquery for activity
|
// create a subquery for activity
|
||||||
$sqb = $qb->getEntityManager()->createQueryBuilder();
|
$sqb = $qb->getEntityManager()->createQueryBuilder();
|
||||||
$sqb->select('1')
|
$sqb->select('1')
|
||||||
@@ -59,7 +61,6 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
if (\in_array('activity', $qb->getAllAliases(), true)) {
|
if (\in_array('activity', $qb->getAllAliases(), true)) {
|
||||||
$sqb->andWhere('activity_person_having_activity.id = activity.id');
|
$sqb->andWhere('activity_person_having_activity.id = activity.id');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['reasons']) && [] !== $data['reasons']) {
|
if (isset($data['reasons']) && [] !== $data['reasons']) {
|
||||||
// add clause activity reason
|
// add clause activity reason
|
||||||
$sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');
|
$sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');
|
||||||
@@ -124,12 +125,38 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
|
|
||||||
public function normalizeFormData(array $formData): array
|
public function normalizeFormData(array $formData): array
|
||||||
{
|
{
|
||||||
return ['date_from_rolling' => $formData['date_from_rolling']->normalize(), 'date_to_rolling' => $formData['date_to_rolling']->normalize()];
|
$normalized = [
|
||||||
|
'date_from_rolling' => $formData['date_from_rolling']->normalize(),
|
||||||
|
'date_to_rolling' => $formData['date_to_rolling']->normalize(),
|
||||||
|
'reasons' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($formData['reasons']) && [] !== $formData['reasons']) {
|
||||||
|
$normalized['reasons'] = array_map(
|
||||||
|
fn (ActivityReason $reason) => $reason->getId(),
|
||||||
|
$formData['reasons']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||||
{
|
{
|
||||||
return ['date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']), 'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling'])];
|
$denormalized = [
|
||||||
|
'date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']),
|
||||||
|
'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling']),
|
||||||
|
'reasons' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($formData['reasons']) && [] !== $formData['reasons']) {
|
||||||
|
$denormalized['reasons'] = array_map(
|
||||||
|
fn ($id) => $this->activityReasonRepository->find($id),
|
||||||
|
$formData['reasons']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $denormalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFormDefaultData(): array
|
public function getFormDefaultData(): array
|
||||||
@@ -143,10 +170,12 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
|
|
||||||
public function describeAction($data, ExportGenerationContext $context): array
|
public function describeAction($data, ExportGenerationContext $context): array
|
||||||
{
|
{
|
||||||
|
$reasons = $data['reasons'] ?? [];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[] === $data['reasons'] ?
|
[] === $reasons ?
|
||||||
'export.filter.person_between_dates.describe_action_with_no_subject'
|
'export.filter.activity.describe_action_with_no_subject'
|
||||||
: 'export.filter.person_between_dates.describe_action_with_subject',
|
: 'export.filter.activity.describe_action_with_subject',
|
||||||
[
|
[
|
||||||
'date_from' => $this->rollingDateConverter->convert($data['date_from_rolling']),
|
'date_from' => $this->rollingDateConverter->convert($data['date_from_rolling']),
|
||||||
'date_to' => $this->rollingDateConverter->convert($data['date_to_rolling']),
|
'date_to' => $this->rollingDateConverter->convert($data['date_to_rolling']),
|
||||||
@@ -154,7 +183,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
', ',
|
', ',
|
||||||
array_map(
|
array_map(
|
||||||
fn (ActivityReason $r): string => '"'.$this->translatableStringHelper->localize($r->getName()).'"',
|
fn (ActivityReason $r): string => '"'.$this->translatableStringHelper->localize($r->getName()).'"',
|
||||||
$data['reasons']
|
$reasons
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -168,6 +197,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
|
|
||||||
public function validateForm($data, ExecutionContextInterface $context): void
|
public function validateForm($data, ExecutionContextInterface $context): void
|
||||||
{
|
{
|
||||||
|
error_log('validateForm called with data: '.json_encode(array_keys($data)));
|
||||||
if ($this->rollingDateConverter->convert($data['date_from_rolling'])
|
if ($this->rollingDateConverter->convert($data['date_from_rolling'])
|
||||||
>= $this->rollingDateConverter->convert($data['date_to_rolling'])) {
|
>= $this->rollingDateConverter->convert($data['date_to_rolling'])) {
|
||||||
$context->buildViolation('export.filter.activity.person_between_dates.date mismatch')
|
$context->buildViolation('export.filter.activity.person_between_dates.date mismatch')
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ class ActivityType extends AbstractType
|
|||||||
|
|
||||||
if (null !== $options['data']->getPerson()) {
|
if (null !== $options['data']->getPerson()) {
|
||||||
$builder->add('scope', ScopePickerType::class, [
|
$builder->add('scope', ScopePickerType::class, [
|
||||||
'center' => $options['center'],
|
|
||||||
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
|
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
|
||||||
|
'center' => $options['center'],
|
||||||
'required' => true,
|
'required' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,8 +136,14 @@ export default {
|
|||||||
issueIsLoading: false,
|
issueIsLoading: false,
|
||||||
actionIsLoading: false,
|
actionIsLoading: false,
|
||||||
actionAreLoaded: false,
|
actionAreLoaded: false,
|
||||||
socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`,
|
socialIssuesClassList: {
|
||||||
socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`,
|
"col-form-label": true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
socialActionsClassList: {
|
||||||
|
"col-form-label": true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -158,6 +164,21 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
/* Load classNames after element is present */
|
||||||
|
const socialActionsEl = document.querySelector(
|
||||||
|
"input#chill_activitybundle_activity_socialActions",
|
||||||
|
);
|
||||||
|
if (socialActionsEl && socialActionsEl.hasAttribute("required")) {
|
||||||
|
this.socialActionsClassList.required = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const socialIssuesEl = document.querySelector(
|
||||||
|
"input#chill_activitybundle_activity_socialIssues",
|
||||||
|
);
|
||||||
|
if (socialIssuesEl && socialIssuesEl.hasAttribute("required")) {
|
||||||
|
this.socialIssuesClassList.required = true;
|
||||||
|
}
|
||||||
|
|
||||||
/* Load other issues in multiselect */
|
/* Load other issues in multiselect */
|
||||||
this.issueIsLoading = true;
|
this.issueIsLoading = true;
|
||||||
this.actionAreLoaded = false;
|
this.actionAreLoaded = false;
|
||||||
|
|||||||
@@ -43,11 +43,23 @@ export default {
|
|||||||
span.badge {
|
span.badge {
|
||||||
@include badge_social($social-action-color);
|
@include badge_social($social-action-color);
|
||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
max-width: 100%; /* Adjust as needed */
|
text-align: left;
|
||||||
overflow: hidden;
|
line-height: 1.2em;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 11px;
|
||||||
|
top: 0;
|
||||||
|
margin: 0 0.3em 0 -0.75em;
|
||||||
|
}
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.5em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,7 +43,22 @@ export default {
|
|||||||
span.badge {
|
span.badge {
|
||||||
@include badge_social($social-issue-color);
|
@include badge_social($social-issue-color);
|
||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 11px;
|
||||||
|
top: 0;
|
||||||
|
margin: 0 0.3em 0 -0.75em;
|
||||||
|
}
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.5em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Attendee: Présence de l'usager
|
|||||||
attendee: présence de l'usager
|
attendee: présence de l'usager
|
||||||
list_reasons: liste des sujets
|
list_reasons: liste des sujets
|
||||||
user_username: nom de l'utilisateur
|
user_username: nom de l'utilisateur
|
||||||
circle_name: nom du cercle
|
circle_name: nom du service
|
||||||
Remark: Commentaire
|
Remark: Commentaire
|
||||||
No comments: Aucun commentaire
|
No comments: Aucun commentaire
|
||||||
Add a new activity: Ajouter une nouvel échange
|
Add a new activity: Ajouter une nouvel échange
|
||||||
@@ -20,7 +20,7 @@ not present: absent
|
|||||||
Delete: Supprimer
|
Delete: Supprimer
|
||||||
Update: Mettre à jour
|
Update: Mettre à jour
|
||||||
Update activity: Modifier l'échange
|
Update activity: Modifier l'échange
|
||||||
Scope: Cercle
|
Scope: Service
|
||||||
Activity data: Données de l'échange
|
Activity data: Données de l'échange
|
||||||
Activity location: Localisation de l'échange
|
Activity location: Localisation de l'échange
|
||||||
No reason associated: Aucun sujet
|
No reason associated: Aucun sujet
|
||||||
@@ -398,13 +398,15 @@ export:
|
|||||||
sent received: Envoyé ou reçu
|
sent received: Envoyé ou reçu
|
||||||
emergency: Urgence
|
emergency: Urgence
|
||||||
accompanying course id: Identifiant du parcours
|
accompanying course id: Identifiant du parcours
|
||||||
course circles: Cercles du parcours
|
course circles: Services du parcours
|
||||||
travelTime: Durée de déplacement
|
travelTime: Durée de déplacement
|
||||||
durationTime: Durée
|
durationTime: Durée
|
||||||
id: Identifiant
|
id: Identifiant
|
||||||
List activities linked to an accompanying course: Liste les échanges liés à un parcours en fonction de différents filtres.
|
List activities linked to an accompanying course: Liste les échanges liés à un parcours en fonction de différents filtres.
|
||||||
List activity linked to a course: Liste des échanges liés à un parcours
|
List activity linked to a course: Liste des échanges liés à un parcours
|
||||||
|
commentText: Commentaire
|
||||||
|
comment_date: Date de la dernière édition du commentaire
|
||||||
|
comment_user: Dernière édition par
|
||||||
|
|
||||||
filter:
|
filter:
|
||||||
activity:
|
activity:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
|
|||||||
$config = $this->processConfiguration($configuration, $configs);
|
$config = $this->processConfiguration($configuration, $configs);
|
||||||
|
|
||||||
$container->setParameter('chill_aside_activity.form.time_duration', $config['form']['time_duration']);
|
$container->setParameter('chill_aside_activity.form.time_duration', $config['form']['time_duration']);
|
||||||
|
$container->setParameter('chill_aside_activity.show_concerned_persons_count', 'visible' === $config['show_concerned_persons_count']);
|
||||||
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||||
$loader->load('services.yaml');
|
$loader->load('services.yaml');
|
||||||
@@ -38,6 +39,24 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
|
|||||||
{
|
{
|
||||||
$this->prependRoute($container);
|
$this->prependRoute($container);
|
||||||
$this->prependCruds($container);
|
$this->prependCruds($container);
|
||||||
|
$this->prependTwigConfig($container);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prependTwigConfig(ContainerBuilder $container)
|
||||||
|
{
|
||||||
|
// Get the configuration for this bundle
|
||||||
|
$chillAsideActivityConfig = $container->getExtensionConfig($this->getAlias());
|
||||||
|
$config = $this->processConfiguration($this->getConfiguration($chillAsideActivityConfig, $container), $chillAsideActivityConfig);
|
||||||
|
|
||||||
|
// Add configuration to twig globals
|
||||||
|
$twigConfig = [
|
||||||
|
'globals' => [
|
||||||
|
'chill_aside_activity_config' => [
|
||||||
|
'show_concerned_persons_count' => 'visible' === $config['show_concerned_persons_count'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$container->prependExtensionConfig('twig', $twigConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function prependCruds(ContainerBuilder $container)
|
protected function prependCruds(ContainerBuilder $container)
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
->end()
|
||||||
|
->enumNode('show_concerned_persons_count')
|
||||||
|
->values(['hidden', 'visible'])
|
||||||
|
->defaultValue('hidden')
|
||||||
|
->info('Show the concerned persons count field in aside activity forms and views')
|
||||||
|
->end()
|
||||||
->end();
|
->end();
|
||||||
|
|
||||||
return $treeBuilder;
|
return $treeBuilder;
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
private User $updatedBy;
|
private User $updatedBy;
|
||||||
|
|
||||||
|
#[Assert\GreaterThanOrEqual(0)]
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true)]
|
||||||
|
private ?int $concernedPersonsCount = 0;
|
||||||
|
|
||||||
public function getAgent(): ?User
|
public function getAgent(): ?User
|
||||||
{
|
{
|
||||||
return $this->agent;
|
return $this->agent;
|
||||||
@@ -186,4 +190,16 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getConcernedPersonsCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->concernedPersonsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConcernedPersonsCount(?int $concernedPersonsCount): self
|
||||||
|
{
|
||||||
|
$this->concernedPersonsCount = $concernedPersonsCount;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?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\AsideActivityBundle\Export\Aggregator;
|
||||||
|
|
||||||
|
use Chill\AsideActivityBundle\Export\Declarations;
|
||||||
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
class ByConcernedPersonsCountAggregator implements AggregatorInterface
|
||||||
|
{
|
||||||
|
public function addRole(): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
||||||
|
{
|
||||||
|
$qb->addSelect('aside.concernedPersonsCount AS by_concerned_persons_count_aggregator')
|
||||||
|
->addGroupBy('by_concerned_persons_count_aggregator');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyOn(): string
|
||||||
|
{
|
||||||
|
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder): void
|
||||||
|
{
|
||||||
|
// No form needed
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNormalizationVersion(): int
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeFormData(array $formData): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels($key, array $values, $data): callable
|
||||||
|
{
|
||||||
|
return function ($value): string {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.aggregator.Concerned persons count';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value) {
|
||||||
|
return 'export.aggregator.No concerned persons count specified';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryKeys($data): array
|
||||||
|
{
|
||||||
|
return ['by_concerned_persons_count_aggregator'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'export.aggregator.Group by concerned persons count';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?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\AsideActivityBundle\Export\Export;
|
||||||
|
|
||||||
|
use Chill\AsideActivityBundle\Export\Declarations;
|
||||||
|
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
|
||||||
|
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
|
||||||
|
use Chill\MainBundle\Export\ExportInterface;
|
||||||
|
use Chill\MainBundle\Export\FormatterInterface;
|
||||||
|
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||||
|
use Doctrine\ORM\Query;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
class SumConcernedPersonsCountAsideActivity implements ExportInterface, GroupedExportInterface
|
||||||
|
{
|
||||||
|
public function __construct(private readonly AsideActivityRepository $repository) {}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder) {}
|
||||||
|
|
||||||
|
public function getNormalizationVersion(): int
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeFormData(array $formData): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllowedFormattersTypes(): array
|
||||||
|
{
|
||||||
|
return [FormatterInterface::TYPE_TABULAR];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'export.Sum concerned persons count for aside activities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGroup(): string
|
||||||
|
{
|
||||||
|
return 'export.Exports of aside activities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels($key, array $values, $data)
|
||||||
|
{
|
||||||
|
if ('export_sum_concerned_persons_count' !== $key) {
|
||||||
|
throw new \LogicException("the key {$key} is not used by this export");
|
||||||
|
}
|
||||||
|
|
||||||
|
$labels = array_combine($values, $values);
|
||||||
|
$labels['_header'] = $this->getTitle();
|
||||||
|
|
||||||
|
return static fn ($value) => $labels[$value];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryKeys($data): array
|
||||||
|
{
|
||||||
|
return ['export_sum_concerned_persons_count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array
|
||||||
|
{
|
||||||
|
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'export.Sum concerned persons count for aside activities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->repository->createQueryBuilder('aside');
|
||||||
|
|
||||||
|
$qb->select('SUM(COALESCE(aside.concernedPersonsCount, 0)) as export_sum_concerned_persons_count');
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requiredRole(): string
|
||||||
|
{
|
||||||
|
return AsideActivityVoter::STATS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsModifiers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Declarations::ASIDE_ACTIVITY_TYPE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
|||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
|
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\Form\FormEvent;
|
use Symfony\Component\Form\FormEvent;
|
||||||
use Symfony\Component\Form\FormEvents;
|
use Symfony\Component\Form\FormEvents;
|
||||||
@@ -29,11 +30,13 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
final class AsideActivityFormType extends AbstractType
|
final class AsideActivityFormType extends AbstractType
|
||||||
{
|
{
|
||||||
private readonly array $timeChoices;
|
private readonly array $timeChoices;
|
||||||
|
private readonly bool $showConcernedPersonsCount;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ParameterBagInterface $parameterBag,
|
ParameterBagInterface $parameterBag,
|
||||||
) {
|
) {
|
||||||
$this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration');
|
$this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration');
|
||||||
|
$this->showConcernedPersonsCount = $parameterBag->get('chill_aside_activity.show_concerned_persons_count');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
@@ -76,6 +79,16 @@ final class AsideActivityFormType extends AbstractType
|
|||||||
->add('location', PickUserLocationType::class)
|
->add('location', PickUserLocationType::class)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
if ($this->showConcernedPersonsCount) {
|
||||||
|
$builder->add('concernedPersonsCount', IntegerType::class, [
|
||||||
|
'label' => 'Concerned persons count',
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'min' => 0,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (['duration'] as $fieldName) {
|
foreach (['duration'] as $fieldName) {
|
||||||
$builder->get($fieldName)
|
$builder->get($fieldName)
|
||||||
->addModelTransformer($durationTimeTransformer);
|
->addModelTransformer($durationTimeTransformer);
|
||||||
|
|||||||
@@ -42,6 +42,11 @@
|
|||||||
{%- if entity.location.name is defined -%}
|
{%- if entity.location.name is defined -%}
|
||||||
<div><i class="fa fa-fw fa-map-marker"></i>{{ entity.location.name }}</div>
|
<div><i class="fa fa-fw fa-map-marker"></i>{{ entity.location.name }}</div>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- if entity.concernedPersonsCount > 0 -%}
|
||||||
|
<div><i class="fa fa-fw fa-user"></i>{{ entity.concernedPersonsCount }}</div>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item-col" style="justify-content: flex-end;">
|
<div class="item-col" style="justify-content: flex-end;">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
<dt class="inline">{{ 'Duration'|trans }}</dt>
|
<dt class="inline">{{ 'Duration'|trans }}</dt>
|
||||||
<dd>{{ entity.duration|date('H:i') }}</dd>
|
<dd>{{ entity.duration|date('H:i') }}</dd>
|
||||||
|
|
||||||
|
{% if chill_aside_activity_config.show_concerned_persons_count == 'visible' %}
|
||||||
|
<dt class="inline">{{ 'Concerned persons count'|trans }}</dt>
|
||||||
|
<dd>{{ entity.concernedPersonsCount }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<dt class="inline">{{ 'Remark'|trans }}</dt>
|
<dt class="inline">{{ 'Remark'|trans }}</dt>
|
||||||
{%- if entity.note is empty -%}
|
{%- if entity.note is empty -%}
|
||||||
<dd>
|
<dd>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?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\AsideActivityBundle\Tests\Export\Aggregator;
|
||||||
|
|
||||||
|
use Chill\AsideActivityBundle\Entity\AsideActivity;
|
||||||
|
use Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator;
|
||||||
|
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class ByConcernedPersonsCountAggregatorTest extends AbstractAggregatorTest
|
||||||
|
{
|
||||||
|
public function getAggregator()
|
||||||
|
{
|
||||||
|
return new ByConcernedPersonsCountAggregator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getFormData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getQueryBuilders(): iterable
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$em = self::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
|
||||||
|
return [
|
||||||
|
$em->createQueryBuilder()
|
||||||
|
->select('count(aside.id)')
|
||||||
|
->from(AsideActivity::class, 'aside'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?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\AsideActivityBundle\Tests\Export\Export;
|
||||||
|
|
||||||
|
use Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity;
|
||||||
|
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
|
||||||
|
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class SumConcernedPersonsCountAsideActivityTest extends AbstractExportTest
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExport()
|
||||||
|
{
|
||||||
|
$repository = self::getContainer()->get(AsideActivityRepository::class);
|
||||||
|
|
||||||
|
yield new SumConcernedPersonsCountAsideActivity($repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getFormData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModifiersCombination(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['aside_activity'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: chill.export, alias: 'avg_aside_activity_duration' }
|
- { name: chill.export, alias: 'avg_aside_activity_duration' }
|
||||||
|
|
||||||
|
Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export, alias: 'sum_aside_activity_concerned_persons_count' }
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
chill.aside_activity.export.date_filter:
|
chill.aside_activity.export.date_filter:
|
||||||
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
|
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
|
||||||
@@ -70,3 +74,7 @@ services:
|
|||||||
Chill\AsideActivityBundle\Export\Aggregator\ByLocationAggregator:
|
Chill\AsideActivityBundle\Export\Aggregator\ByLocationAggregator:
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export_aggregator, alias: 'aside_activity_location_aggregator' }
|
- { name: chill.export_aggregator, alias: 'aside_activity_location_aggregator' }
|
||||||
|
|
||||||
|
Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export_aggregator, alias: 'aside_activity_concerned_persons_count_aggregator' }
|
||||||
|
|||||||
@@ -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\Migrations\AsideActivity;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20251006113048 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add concernedPersonsCount property to AsideActivity entity';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_asideactivity.asideactivity ADD concernedPersonsCount INT DEFAULT 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_asideactivity.AsideActivity DROP concernedPersonsCount');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ Emergency: Urgent
|
|||||||
by: "Par "
|
by: "Par "
|
||||||
location: Lieu
|
location: Lieu
|
||||||
Asideactivity location: Localisation de l'activité
|
Asideactivity location: Localisation de l'activité
|
||||||
|
Concerned persons count: Nombre d'usager concernés
|
||||||
|
|
||||||
# Crud
|
# Crud
|
||||||
crud:
|
crud:
|
||||||
@@ -177,7 +178,7 @@ export:
|
|||||||
agent_id: Utilisateur
|
agent_id: Utilisateur
|
||||||
creator_id: Créateur
|
creator_id: Créateur
|
||||||
main_scope: Service principal de l'utilisateur
|
main_scope: Service principal de l'utilisateur
|
||||||
main_center: Centre principal de l'utilisateur
|
main_center: Territoire principal de l'utilisateur
|
||||||
aside_activity_type: Catégorie d'activité annexe
|
aside_activity_type: Catégorie d'activité annexe
|
||||||
date: Date
|
date: Date
|
||||||
duration: Durée
|
duration: Durée
|
||||||
@@ -190,6 +191,7 @@ export:
|
|||||||
Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
|
Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
|
||||||
Average aside activities duration: Durée moyenne des activités annexes
|
Average aside activities duration: Durée moyenne des activités annexes
|
||||||
Sum aside activities duration: Durée des activités annexes
|
Sum aside activities duration: Durée des activités annexes
|
||||||
|
Sum concerned persons count for aside activities: Nombre d'usager concernés par les activités annexes
|
||||||
filter:
|
filter:
|
||||||
Filter by aside activity date: Filtrer les activités annexes par date
|
Filter by aside activity date: Filtrer les activités annexes par date
|
||||||
Filter by aside activity type: Filtrer les activités annexes par type d'activité
|
Filter by aside activity type: Filtrer les activités annexes par type d'activité
|
||||||
@@ -210,6 +212,8 @@ export:
|
|||||||
'Filtered by aside activity location: only %location%': "Filtré par localisation: uniquement %location%"
|
'Filtered by aside activity location: only %location%': "Filtré par localisation: uniquement %location%"
|
||||||
aggregator:
|
aggregator:
|
||||||
Group by aside activity type: Grouper les activités annexes par type d'activité
|
Group by aside activity type: Grouper les activités annexes par type d'activité
|
||||||
|
Group by concerned persons count: Grouper les activités annexes par nombre d'usagers conernés
|
||||||
|
Concerned persons count: Nombre d'usagers concernés
|
||||||
Aside activity type: Type d'activité annexe
|
Aside activity type: Type d'activité annexe
|
||||||
by_user_job:
|
by_user_job:
|
||||||
Aggregate by user job: Grouper les activités annexes par métier des utilisateurs
|
Aggregate by user job: Grouper les activités annexes par métier des utilisateurs
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\CalendarBundle\Controller;
|
namespace Chill\CalendarBundle\Controller;
|
||||||
|
|
||||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||||
|
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Serializer\Model\Collection;
|
use Chill\MainBundle\Serializer\Model\Collection;
|
||||||
@@ -23,7 +24,10 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
|
|
||||||
class CalendarAPIController extends ApiController
|
class CalendarAPIController extends ApiController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly CalendarRepository $calendarRepository) {}
|
public function __construct(
|
||||||
|
private readonly CalendarRepository $calendarRepository,
|
||||||
|
private readonly InviteRepository $inviteRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
#[Route(path: '/api/1.0/calendar/calendar/by-user/{id}.{_format}', name: 'chill_api_single_calendar_list_by-user', requirements: ['_format' => 'json'])]
|
#[Route(path: '/api/1.0/calendar/calendar/by-user/{id}.{_format}', name: 'chill_api_single_calendar_list_by-user', requirements: ['_format' => 'json'])]
|
||||||
public function listByUser(User $user, Request $request, string $_format): JsonResponse
|
public function listByUser(User $user, Request $request, string $_format): JsonResponse
|
||||||
@@ -52,16 +56,37 @@ class CalendarAPIController extends ApiController
|
|||||||
throw new BadRequestHttpException('dateTo not parsable');
|
throw new BadRequestHttpException('dateTo not parsable');
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = $this->calendarRepository->countByUser($user, $dateFrom, $dateTo);
|
// Get calendar items where user is the main user
|
||||||
$paginator = $this->getPaginatorFactory()->create($total);
|
$ownCalendars = $this->calendarRepository->findByUser(
|
||||||
$ranges = $this->calendarRepository->findByUser(
|
|
||||||
$user,
|
$user,
|
||||||
$dateFrom,
|
$dateFrom,
|
||||||
$dateTo,
|
$dateTo
|
||||||
$paginator->getItemsPerPage(),
|
|
||||||
$paginator->getCurrentPageFirstItemNumber()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get calendar items from accepted invites
|
||||||
|
$acceptedInvites = $this->inviteRepository->findAcceptedInvitesByUserAndDateRange($user, $dateFrom, $dateTo);
|
||||||
|
$inviteCalendars = array_map(fn ($invite) => $invite->getCalendar(), $acceptedInvites);
|
||||||
|
|
||||||
|
// Merge
|
||||||
|
$allCalendars = array_merge($ownCalendars, $inviteCalendars);
|
||||||
|
$uniqueCalendars = [];
|
||||||
|
$seenIds = [];
|
||||||
|
|
||||||
|
foreach ($allCalendars as $calendar) {
|
||||||
|
$id = $calendar->getId();
|
||||||
|
if (!in_array($id, $seenIds, true)) {
|
||||||
|
$seenIds[] = $id;
|
||||||
|
$uniqueCalendars[] = $calendar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = count($uniqueCalendars);
|
||||||
|
$paginator = $this->getPaginatorFactory()->create($total);
|
||||||
|
|
||||||
|
$offset = $paginator->getCurrentPageFirstItemNumber();
|
||||||
|
$limit = $paginator->getItemsPerPage();
|
||||||
|
$ranges = array_slice($uniqueCalendars, $offset, $limit);
|
||||||
|
|
||||||
$collection = new Collection($ranges, $paginator);
|
$collection = new Collection($ranges, $paginator);
|
||||||
|
|
||||||
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]);
|
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\Controller;
|
|||||||
|
|
||||||
use Chill\CalendarBundle\Entity\Calendar;
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
use Chill\CalendarBundle\Form\CalendarType;
|
use Chill\CalendarBundle\Form\CalendarType;
|
||||||
|
use Chill\CalendarBundle\Form\CancelType;
|
||||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||||
use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface;
|
use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface;
|
||||||
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
|
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
|
||||||
@@ -30,6 +31,7 @@ use Chill\PersonBundle\Repository\PersonRepository;
|
|||||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use http\Exception\UnexpectedValueException;
|
use http\Exception\UnexpectedValueException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -60,6 +62,7 @@ class CalendarController extends AbstractController
|
|||||||
private readonly UserRepositoryInterface $userRepository,
|
private readonly UserRepositoryInterface $userRepository,
|
||||||
private readonly TranslatorInterface $translator,
|
private readonly TranslatorInterface $translator,
|
||||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,6 +114,55 @@ class CalendarController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/{_locale}/calendar/calendar/{id}/cancel', name: 'chill_calendar_calendar_cancel')]
|
||||||
|
public function cancelAction(Calendar $calendar, Request $request): Response
|
||||||
|
{
|
||||||
|
// Deal with sms being sent or not
|
||||||
|
// Communicate cancellation with the remote calendar.
|
||||||
|
|
||||||
|
$this->denyAccessUnlessGranted(CalendarVoter::EDIT, $calendar);
|
||||||
|
|
||||||
|
[$person, $accompanyingPeriod] = [$calendar->getPerson(), $calendar->getAccompanyingPeriod()];
|
||||||
|
|
||||||
|
$form = $this->createForm(CancelType::class, $calendar);
|
||||||
|
$form->add('submit', SubmitType::class);
|
||||||
|
|
||||||
|
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||||
|
$view = '@ChillCalendar/Calendar/cancelCalendarByAccompanyingCourse.html.twig';
|
||||||
|
$redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]);
|
||||||
|
} elseif ($person instanceof Person) {
|
||||||
|
$view = '@ChillCalendar/Calendar/cancelCalendarByPerson.html.twig';
|
||||||
|
$redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]);
|
||||||
|
} else {
|
||||||
|
throw new \RuntimeException('nor person or accompanying period');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
|
||||||
|
$this->logger->notice('A calendar event has been cancelled', [
|
||||||
|
'by_user' => $this->getUser()->getUsername(),
|
||||||
|
'calendar_id' => $calendar->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$calendar->setStatus($calendar::STATUS_CANCELED);
|
||||||
|
$calendar->setSmsStatus($calendar::SMS_CANCEL_PENDING);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', $this->translator->trans('chill_calendar.calendar_canceled'));
|
||||||
|
|
||||||
|
return new RedirectResponse($redirectRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render($view, [
|
||||||
|
'calendar' => $calendar,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'accompanyingCourse' => $accompanyingPeriod,
|
||||||
|
'person' => $person,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit a calendar item.
|
* Edit a calendar item.
|
||||||
*/
|
*/
|
||||||
@@ -266,7 +318,7 @@ class CalendarController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->getUser() instanceof User) {
|
if (!$this->getUser() instanceof User) {
|
||||||
throw new UnauthorizedHttpException('you are not an user');
|
throw new UnauthorizedHttpException('you are not a user');
|
||||||
}
|
}
|
||||||
|
|
||||||
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?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\CalendarBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
|
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||||
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class MyInvitationsController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly InviteRepository $inviteRepository, private readonly PaginatorFactory $paginator, private readonly DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository) {}
|
||||||
|
|
||||||
|
#[Route(path: '/{_locale}/calendar/invitations/my', name: 'chill_calendar_invitations_list_my')]
|
||||||
|
public function myInvitations(Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
|
|
||||||
|
$user = $this->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new UnauthorizedHttpException('you are not a user');
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = count($this->inviteRepository->findBy(['user' => $user]));
|
||||||
|
$paginator = $this->paginator->create($total);
|
||||||
|
|
||||||
|
$invitations = $this->inviteRepository->findBy(
|
||||||
|
['user' => $user],
|
||||||
|
['createdAt' => 'DESC'],
|
||||||
|
$paginator->getItemsPerPage(),
|
||||||
|
$paginator->getCurrentPageFirstItemNumber()
|
||||||
|
);
|
||||||
|
|
||||||
|
$view = '@ChillCalendar/Invitations/listByUser.html.twig';
|
||||||
|
|
||||||
|
return $this->render($view, [
|
||||||
|
'invitations' => $invitations,
|
||||||
|
'paginator' => $paginator,
|
||||||
|
'templates' => $this->docGeneratorTemplateRepository->findByEntity(Calendar::class),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ class LoadCancelReason extends Fixture implements FixtureGroupInterface
|
|||||||
$arr = [
|
$arr = [
|
||||||
['name' => CancelReason::CANCELEDBY_USER],
|
['name' => CancelReason::CANCELEDBY_USER],
|
||||||
['name' => CancelReason::CANCELEDBY_PERSON],
|
['name' => CancelReason::CANCELEDBY_PERSON],
|
||||||
['name' => CancelReason::CANCELEDBY_DONOTCOUNT],
|
['name' => CancelReason::CANCELEDBY_OTHER],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($arr as $a) {
|
foreach ($arr as $a) {
|
||||||
|
|||||||
@@ -269,6 +269,11 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
|||||||
return $this->cancelReason;
|
return $this->cancelReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isCanceled(): bool
|
||||||
|
{
|
||||||
|
return null !== $this->cancelReason;
|
||||||
|
}
|
||||||
|
|
||||||
public function getCenters(): ?iterable
|
public function getCenters(): ?iterable
|
||||||
{
|
{
|
||||||
return match ($this->getContext()) {
|
return match ($this->getContext()) {
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
#[ORM\Table(name: 'chill_calendar.cancel_reason')]
|
#[ORM\Table(name: 'chill_calendar.cancel_reason')]
|
||||||
class CancelReason
|
class CancelReason
|
||||||
{
|
{
|
||||||
final public const CANCELEDBY_DONOTCOUNT = 'CANCELEDBY_DONOTCOUNT';
|
final public const CANCELEDBY_OTHER = 'CANCELEDBY_OTHER';
|
||||||
|
|
||||||
final public const CANCELEDBY_PERSON = 'CANCELEDBY_PERSON';
|
final public const CANCELEDBY_PERSON = 'CANCELEDBY_PERSON';
|
||||||
|
|
||||||
final public const CANCELEDBY_USER = 'CANCELEDBY_USER';
|
final public const CANCELEDBY_USER = 'CANCELEDBY_USER';
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
|
||||||
private ?bool $active = null;
|
private bool $active = true;
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
||||||
private ?string $canceledBy = null;
|
private ?string $canceledBy = null;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use Chill\CalendarBundle\Entity\CancelReason;
|
|||||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
@@ -28,7 +28,14 @@ class CancelReasonType extends AbstractType
|
|||||||
->add('active', CheckboxType::class, [
|
->add('active', CheckboxType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
->add('canceledBy', TextType::class);
|
->add('canceledBy', ChoiceType::class, [
|
||||||
|
'choices' => [
|
||||||
|
'chill_calendar.canceled_by.user' => CancelReason::CANCELEDBY_USER,
|
||||||
|
'chill_calendar.canceled_by.person' => CancelReason::CANCELEDBY_PERSON,
|
||||||
|
'chill_calendar.canceled_by.other' => CancelReason::CANCELEDBY_OTHER,
|
||||||
|
],
|
||||||
|
'required' => true,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
|||||||
42
src/Bundle/ChillCalendarBundle/Form/CancelType.php
Normal file
42
src/Bundle/ChillCalendarBundle/Form/CancelType.php
Normal 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\CalendarBundle\Form;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
|
use Chill\CalendarBundle\Entity\CancelReason;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class CancelType extends AbstractType
|
||||||
|
{
|
||||||
|
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder->add('cancelReason', EntityType::class, [
|
||||||
|
'class' => CancelReason::class,
|
||||||
|
'required' => true,
|
||||||
|
'choice_label' => fn (CancelReason $cancelReason) => $this->translatableStringHelper->localize($cancelReason->getName()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Calendar::class,
|
||||||
|
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,13 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
|||||||
if ($this->security->isGranted('ROLE_USER')) {
|
if ($this->security->isGranted('ROLE_USER')) {
|
||||||
$menu->addChild('My calendar list', [
|
$menu->addChild('My calendar list', [
|
||||||
'route' => 'chill_calendar_calendar_list_my',
|
'route' => 'chill_calendar_calendar_list_my',
|
||||||
|
])
|
||||||
|
->setExtras([
|
||||||
|
'order' => 8,
|
||||||
|
'icon' => 'tasks',
|
||||||
|
]);
|
||||||
|
$menu->addChild('invite.list.title', [
|
||||||
|
'route' => 'chill_calendar_invitations_list_my',
|
||||||
])
|
])
|
||||||
->setExtras([
|
->setExtras([
|
||||||
'order' => 9,
|
'order' => 9,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Chill\CalendarBundle\Messenger\Doctrine;
|
|||||||
use Chill\CalendarBundle\Entity\Calendar;
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
|
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
|
||||||
use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
|
use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||||
use Doctrine\ORM\Event\PostRemoveEventArgs;
|
use Doctrine\ORM\Event\PostRemoveEventArgs;
|
||||||
use Doctrine\ORM\Event\PostUpdateEventArgs;
|
use Doctrine\ORM\Event\PostUpdateEventArgs;
|
||||||
@@ -31,6 +32,17 @@ class CalendarEntityListener
|
|||||||
{
|
{
|
||||||
public function __construct(private readonly MessageBusInterface $messageBus, private readonly Security $security) {}
|
public function __construct(private readonly MessageBusInterface $messageBus, private readonly Security $security) {}
|
||||||
|
|
||||||
|
private function getAuthenticatedUser(): User
|
||||||
|
{
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new \LogicException('Expected an instance of User.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
public function postPersist(Calendar $calendar, PostPersistEventArgs $args): void
|
public function postPersist(Calendar $calendar, PostPersistEventArgs $args): void
|
||||||
{
|
{
|
||||||
if (!$calendar->preventEnqueueChanges) {
|
if (!$calendar->preventEnqueueChanges) {
|
||||||
@@ -38,7 +50,7 @@ class CalendarEntityListener
|
|||||||
new CalendarMessage(
|
new CalendarMessage(
|
||||||
$calendar,
|
$calendar,
|
||||||
CalendarMessage::CALENDAR_PERSIST,
|
CalendarMessage::CALENDAR_PERSIST,
|
||||||
$this->security->getUser()
|
$this->getAuthenticatedUser()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,7 +62,7 @@ class CalendarEntityListener
|
|||||||
$this->messageBus->dispatch(
|
$this->messageBus->dispatch(
|
||||||
new CalendarRemovedMessage(
|
new CalendarRemovedMessage(
|
||||||
$calendar,
|
$calendar,
|
||||||
$this->security->getUser()
|
$this->getAuthenticatedUser()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -58,12 +70,19 @@ class CalendarEntityListener
|
|||||||
|
|
||||||
public function postUpdate(Calendar $calendar, PostUpdateEventArgs $args): void
|
public function postUpdate(Calendar $calendar, PostUpdateEventArgs $args): void
|
||||||
{
|
{
|
||||||
if (!$calendar->preventEnqueueChanges) {
|
if ($calendar->getStatus() === $calendar::STATUS_CANCELED) {
|
||||||
|
$this->messageBus->dispatch(
|
||||||
|
new CalendarRemovedMessage(
|
||||||
|
$calendar,
|
||||||
|
$this->getAuthenticatedUser()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} elseif (!$calendar->preventEnqueueChanges) {
|
||||||
$this->messageBus->dispatch(
|
$this->messageBus->dispatch(
|
||||||
new CalendarMessage(
|
new CalendarMessage(
|
||||||
$calendar,
|
$calendar,
|
||||||
CalendarMessage::CALENDAR_UPDATE,
|
CalendarMessage::CALENDAR_UPDATE,
|
||||||
$this->security->getUser()
|
$this->getAuthenticatedUser()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ class CalendarRemovedMessage
|
|||||||
|
|
||||||
public function getRemoteId(): string
|
public function getRemoteId(): string
|
||||||
{
|
{
|
||||||
|
dump($this->remoteId);
|
||||||
|
|
||||||
return $this->remoteId;
|
return $this->remoteId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ class CalendarRepository implements ObjectRepository
|
|||||||
$qb->expr()->eq('c.mainUser', ':user'),
|
$qb->expr()->eq('c.mainUser', ':user'),
|
||||||
$qb->expr()->gte('c.startDate', ':startDate'),
|
$qb->expr()->gte('c.startDate', ':startDate'),
|
||||||
$qb->expr()->lte('c.endDate', ':endDate'),
|
$qb->expr()->lte('c.endDate', ':endDate'),
|
||||||
|
$qb->expr()->isNull('c.cancelReason'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
->setParameters([
|
->setParameters([
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\CalendarBundle\Repository;
|
namespace Chill\CalendarBundle\Repository;
|
||||||
|
|
||||||
use Chill\CalendarBundle\Entity\Invite;
|
use Chill\CalendarBundle\Entity\Invite;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
@@ -41,7 +42,7 @@ class InviteRepository implements ObjectRepository
|
|||||||
/**
|
/**
|
||||||
* @return array|Invite[]
|
* @return array|Invite[]
|
||||||
*/
|
*/
|
||||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
|
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||||
{
|
{
|
||||||
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
|
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,52 @@ class InviteRepository implements ObjectRepository
|
|||||||
return $this->entityRepository->findOneBy($criteria);
|
return $this->entityRepository->findOneBy($criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find accepted invites for a user within a date range.
|
||||||
|
*
|
||||||
|
* @return array|Invite[]
|
||||||
|
*/
|
||||||
|
public function findAcceptedInvitesByUserAndDateRange(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||||
|
{
|
||||||
|
return $this->buildAcceptedInviteByUserAndDateRangeQuery($user, $from, $to)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count accepted invites for a user within a date range.
|
||||||
|
*/
|
||||||
|
public function countAcceptedInvitesByUserAndDateRange(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to): int
|
||||||
|
{
|
||||||
|
return $this->buildAcceptedInviteByUserAndDateRangeQuery($user, $from, $to)
|
||||||
|
->select('COUNT(c)')
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildAcceptedInviteByUserAndDateRangeQuery(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to)
|
||||||
|
{
|
||||||
|
$qb = $this->entityRepository->createQueryBuilder('i');
|
||||||
|
|
||||||
|
return $qb
|
||||||
|
->join('i.calendar', 'c')
|
||||||
|
->where(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('i.user', ':user'),
|
||||||
|
$qb->expr()->eq('i.status', ':status'),
|
||||||
|
$qb->expr()->gte('c.startDate', ':startDate'),
|
||||||
|
$qb->expr()->lte('c.endDate', ':endDate'),
|
||||||
|
$qb->expr()->isNull('c.cancelReason')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->setParameters([
|
||||||
|
'user' => $user,
|
||||||
|
'status' => Invite::ACCEPTED,
|
||||||
|
'startDate' => $from,
|
||||||
|
'endDate' => $to,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function getClassName(): string
|
public function getClassName(): string
|
||||||
{
|
{
|
||||||
return Invite::class;
|
return Invite::class;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
Chill\CalendarBundle\Controller\:
|
Chill\CalendarBundle\Controller\:
|
||||||
autowire: true
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
resource: '../../../Controller'
|
resource: '../../../Controller'
|
||||||
tags: ['controller.service_arguments']
|
tags: ['controller.service_arguments']
|
||||||
|
|||||||
@@ -108,9 +108,12 @@
|
|||||||
{{ formatDate(event.endStr, "time") }}:
|
{{ formatDate(event.endStr, "time") }}:
|
||||||
{{ event.extendedProps.locationName }}</b
|
{{ event.extendedProps.locationName }}</b
|
||||||
>
|
>
|
||||||
<b v-else-if="event.extendedProps.is === 'local'">{{
|
<a
|
||||||
event.title
|
:href="calendarLink(event.id)"
|
||||||
}}</b>
|
v-else-if="event.extendedProps.is === 'local'"
|
||||||
|
>
|
||||||
|
<b>{{ event.title }}</b>
|
||||||
|
</a>
|
||||||
<b v-else>no 'is'</b>
|
<b v-else>no 'is'</b>
|
||||||
<a
|
<a
|
||||||
v-if="event.extendedProps.is === 'range'"
|
v-if="event.extendedProps.is === 'range'"
|
||||||
@@ -486,6 +489,12 @@ function copyWeek() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calendarLink = (calendarId: string) => {
|
||||||
|
const idStr = calendarId.match(/_(\d+)$/)?.[1];
|
||||||
|
|
||||||
|
return `/fr/calendar/calendar/${idStr}/edit`;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
copyFromWeek.value = dateToISO(getMonday(0));
|
copyFromWeek.value = dateToISO(getMonday(0));
|
||||||
copyToWeek.value = dateToISO(getMonday(1));
|
copyToWeek.value = dateToISO(getMonday(1));
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
{# list used in context of person or accompanyingPeriod #}
|
{# list used in context of person, accompanyingPeriod or user #}
|
||||||
|
|
||||||
{% if calendarItems|length > 0 %}
|
<div class="item-bloc">
|
||||||
<div class="flex-table list-records context-accompanyingCourse">
|
<div class="item-row main">
|
||||||
|
<div class="item-col">
|
||||||
{% for calendar in calendarItems %}
|
<div class="wrap-header">
|
||||||
|
<div class="wl-row">
|
||||||
<div class="item-bloc">
|
{% if calendar.status == 'canceled' %}
|
||||||
<div class="item-row main">
|
<div class="badge rounded-pill bg-danger">
|
||||||
<div class="item-col">
|
<span>{{ 'chill_calendar.canceled'|trans }}: </span>
|
||||||
<div class="wrap-header">
|
<span>{{ calendar.cancelReason.name|localize_translatable_string }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="wl-row">
|
<div class="wl-row">
|
||||||
<div class="wl-col title">
|
<div class="wl-col title">
|
||||||
<p class="date-label">
|
<p class="date-label">
|
||||||
|
{% if calendar.status == 'canceled' %}
|
||||||
|
<del>
|
||||||
|
{% endif %}
|
||||||
{% if context == 'person' and calendar.context == 'accompanying_period' %}
|
{% if context == 'person' and calendar.context == 'accompanying_period' %}
|
||||||
<a href="{{ chill_path_add_return_path('chill_person_accompanying_course_index', {'accompanying_period_id': calendar.accompanyingPeriod.id}) }}" style="text-decoration: none;">
|
<a href="{{ chill_path_add_return_path('chill_person_accompanying_course_index', {'accompanying_period_id': calendar.accompanyingPeriod.id}) }}" style="text-decoration: none;">
|
||||||
<span class="badge bg-primary">
|
<span class="badge bg-primary">
|
||||||
@@ -19,6 +25,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if calendar.status == 'canceled' %}
|
||||||
|
<del>
|
||||||
|
{% endif %}
|
||||||
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
|
||||||
{{ calendar.startDate|format_datetime('short', 'short') }}
|
{{ calendar.startDate|format_datetime('short', 'short') }}
|
||||||
- {{ calendar.endDate|format_datetime('short', 'short') }}
|
- {{ calendar.endDate|format_datetime('short', 'short') }}
|
||||||
@@ -26,44 +35,46 @@
|
|||||||
{{ calendar.startDate|format_datetime('short', 'short') }}
|
{{ calendar.startDate|format_datetime('short', 'short') }}
|
||||||
- {{ calendar.endDate|format_datetime('none', 'short') }}
|
- {{ calendar.endDate|format_datetime('none', 'short') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
{% if calendar.status == 'canceled' %}
|
||||||
|
</del>
|
||||||
<div class="duration short-message">
|
|
||||||
<i class="fa fa-fw fa-hourglass-end"></i>
|
|
||||||
{{ calendar.duration|date('%H:%I') }}
|
|
||||||
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
|
|
||||||
<!-- no sms will be send -->
|
|
||||||
{% else %}
|
|
||||||
{% if calendar.smsStatus == 'sms_sent' %}
|
|
||||||
<span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
|
|
||||||
<i class="fa fa-check "></i>
|
|
||||||
<i class="fa fa-envelope "></i>
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span title="{{ 'Will send SMS'|trans }}" class="badge bg-info">
|
|
||||||
<i class="fa fa-envelope "></i>
|
|
||||||
<i class="fa fa-hourglass-end "></i>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<div class="duration short-message">
|
||||||
</div>
|
<i class="fa fa-fw fa-hourglass-end"></i>
|
||||||
</div>
|
{{ calendar.duration|date('%H:%I') }}
|
||||||
|
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
|
||||||
<div class="item-col">
|
<!-- no sms will be sent -->
|
||||||
<ul class="list-content">
|
{% else %}
|
||||||
{% if calendar.mainUser is not empty %}
|
{% if calendar.smsStatus == 'sms_sent' %}
|
||||||
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
|
<span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
|
||||||
|
<i class="fa fa-check "></i>
|
||||||
|
<i class="fa fa-envelope "></i>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span title="{{ 'Will send SMS'|trans }}" class="badge bg-info">
|
||||||
|
<i class="fa fa-envelope "></i>
|
||||||
|
<i class="fa fa-hourglass-end "></i>
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if calendar.comment.comment is not empty
|
<div class="item-col">
|
||||||
|
<ul class="list-content">
|
||||||
|
{% if calendar.mainUser is not empty %}
|
||||||
|
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if calendar.comment.comment is not empty
|
||||||
or calendar.users|length > 0
|
or calendar.users|length > 0
|
||||||
or calendar.thirdParties|length > 0
|
or calendar.thirdParties|length > 0
|
||||||
or calendar.users|length > 0 %}
|
or calendar.users|length > 0 %}
|
||||||
@@ -76,131 +87,133 @@
|
|||||||
} %}
|
} %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if calendar.comment.comment is not empty %}
|
||||||
|
<div class="item-row details separator">
|
||||||
|
<div class="item-col comment">
|
||||||
|
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if calendar.location is not empty %}
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div>
|
||||||
|
{% if calendar.location.address is not same as(null) and calendar.location.name is not empty %}
|
||||||
|
<i class="fa fa-map-marker"></i>{% endif %}
|
||||||
|
{% if calendar.location.name is not empty %}{{ calendar.location.name }}{% endif %}
|
||||||
|
{% if calendar.location.address is not same as(null) %}{{ calendar.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (calendar.location.name is empty)}) }}{% else %}
|
||||||
|
<i class="fa fa-map-marker"></i>{% endif %}
|
||||||
|
{% if calendar.location.phonenumber1 is not empty %}<i
|
||||||
|
class="fa fa-phone"></i> {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %}
|
||||||
|
{% if calendar.location.phonenumber2 is not empty %}<i
|
||||||
|
class="fa fa-phone"></i> {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if calendar.documents is not empty %}
|
||||||
|
<div class="item-row separator column">
|
||||||
|
<div>
|
||||||
|
{{ include('@ChillCalendar/Calendar/_documents.twig.html') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if calendar.activity is not null %}
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="item-col">
|
||||||
|
<div class="wrap-list">
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title"><h3>{{ 'Activity'|trans }}</h3></div>
|
||||||
|
<div class="wl-col list activity-linked">
|
||||||
|
<h2 class="badge-title">
|
||||||
|
<span class="title_label"></span>
|
||||||
|
<span class="title_action">
|
||||||
|
{{ calendar.activity.type.name | localize_translatable_string }}
|
||||||
|
|
||||||
|
{% if calendar.activity.emergency %}
|
||||||
|
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li class="cancel">
|
||||||
|
<span class="createdBy">
|
||||||
|
{{ 'Created by'|trans }}
|
||||||
|
<b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': calendar.activity.id}) }}" class="btn btn-sm btn-show" ></a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
{% if calendar.comment.comment is not empty %}
|
|
||||||
<div class="item-row details separator">
|
|
||||||
<div class="item-col comment">
|
|
||||||
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if calendar.location is not empty %}
|
|
||||||
<div class="item-row separator">
|
|
||||||
<div>
|
|
||||||
{% if calendar.location.address is not same as(null) and calendar.location.name is not empty %}
|
|
||||||
<i class="fa fa-map-marker"></i>{% endif %}
|
|
||||||
{% if calendar.location.name is not empty %}{{ calendar.location.name }}{% endif %}
|
|
||||||
{% if calendar.location.address is not same as(null) %}{{ calendar.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (calendar.location.name is empty)}) }}{% else %}
|
|
||||||
<i class="fa fa-map-marker"></i>{% endif %}
|
|
||||||
{% if calendar.location.phonenumber1 is not empty %}<i
|
|
||||||
class="fa fa-phone"></i> {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %}
|
|
||||||
{% if calendar.location.phonenumber2 is not empty %}<i
|
|
||||||
class="fa fa-phone"></i> {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="item-row separator column">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
{{ include('@ChillCalendar/Calendar/_documents.twig.html') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if calendar.activity is not null %}
|
<div class="item-row separator">
|
||||||
<div class="item-row separator">
|
<ul class="record_actions">
|
||||||
<div class="item-col">
|
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', calendar) and calendar.status is not constant('STATUS_CANCELED', calendar) %}
|
||||||
<div class="wrap-list">
|
{% if templates|length == 0 %}
|
||||||
<div class="wl-row">
|
<li>
|
||||||
<div class="wl-col title"><h3>{{ 'Activity'|trans }}</h3></div>
|
<a class="btn btn-create"
|
||||||
<div class="wl-col list activity-linked">
|
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
|
||||||
<h2 class="badge-title">
|
{{ 'chill_calendar.Add a document'|trans }}
|
||||||
<span class="title_label"></span>
|
</a>
|
||||||
<span class="title_action">
|
</li>
|
||||||
{{ calendar.activity.type.name | localize_translatable_string }}
|
{% else %}
|
||||||
|
<li>
|
||||||
{% if calendar.activity.emergency %}
|
<div class="dropdown">
|
||||||
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
|
<button class="btn btn-create dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
{% endif %}
|
{{ 'chill_calendar.Add a document'|trans }}
|
||||||
</span>
|
</button>
|
||||||
</h2>
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
<ul class="record_actions">
|
<a class="dropdown-item"
|
||||||
<li class="cancel">
|
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
|
||||||
<span class="createdBy">
|
{{ 'chill_calendar.Upload a document'|trans }}
|
||||||
{{ 'Created by'|trans }}
|
</a>
|
||||||
<b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
|
</li>
|
||||||
</span>
|
{% for template in templates %}
|
||||||
</li>
|
<li>
|
||||||
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}
|
<a class="dropdown-item"
|
||||||
<li>
|
href="{{ chill_path_add_return_path('chill_docgenerator_generate_from_template', {'template': template.id, 'entityClassName': 'Chill\\CalendarBundle\\Entity\\Calendar', 'entityId': calendar.id}) }}"
|
||||||
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': calendar.activity.id}) }}" class="btn btn-sm btn-show" ></a>
|
>
|
||||||
</li>
|
{{ template.name|localize_translatable_string }}
|
||||||
{% endif %}
|
</a>
|
||||||
</ul>
|
</li>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if calendar.activity is null and (
|
||||||
|
(calendar.context == 'accompanying_period' and is_granted('CHILL_ACTIVITY_CREATE', calendar.accompanyingPeriod))
|
||||||
|
or
|
||||||
|
(calendar.context == 'person' and is_granted('CHILL_ACTIVITY_CREATE', calendar.person))
|
||||||
|
)
|
||||||
|
and calendar.status is not constant('STATUS_CANCELED', calendar)
|
||||||
|
%}
|
||||||
|
<li>
|
||||||
|
<a class="btn btn-create"
|
||||||
|
href="{{ chill_path_add_return_path('chill_calendar_calendar_to_activity', { 'id': calendar.id }) }}">
|
||||||
|
{{ 'Transform to activity'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="item-row separator">
|
{% if calendar.isInvited(app.user) and not calendar.isCanceled %}
|
||||||
<ul class="record_actions">
|
|
||||||
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', calendar) %}
|
|
||||||
{% if templates|length == 0 %}
|
|
||||||
<li>
|
|
||||||
<a class="btn btn-create"
|
|
||||||
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
|
|
||||||
{{ 'chill_calendar.Add a document'|trans }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li>
|
|
||||||
<div class="dropdown">
|
|
||||||
<button class="btn btn-create dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
{{ 'chill_calendar.Add a document'|trans }}
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item"
|
|
||||||
href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
|
|
||||||
{{ 'chill_calendar.Upload a document'|trans }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% for template in templates %}
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item"
|
|
||||||
href="{{ chill_path_add_return_path('chill_docgenerator_generate_from_template', {'template': template.id, 'entityClassName': 'Chill\\CalendarBundle\\Entity\\Calendar', 'entityId': calendar.id}) }}"
|
|
||||||
>
|
|
||||||
{{ template.name|localize_translatable_string }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if calendar.activity is null and (
|
|
||||||
(calendar.context == 'accompanying_period' and is_granted('CHILL_ACTIVITY_CREATE', calendar.accompanyingPeriod))
|
|
||||||
or
|
|
||||||
(calendar.context == 'person' and is_granted('CHILL_ACTIVITY_CREATE', calendar.person))
|
|
||||||
)
|
|
||||||
%}
|
|
||||||
<li>
|
|
||||||
<a class="btn btn-create"
|
|
||||||
href="{{ chill_path_add_return_path('chill_calendar_calendar_to_activity', { 'id': calendar.id }) }}">
|
|
||||||
{{ 'Transform to activity'|trans }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if (calendar.isInvited(app.user)) %}
|
|
||||||
{% set invite = calendar.inviteForUser(app.user) %}
|
{% set invite = calendar.inviteForUser(app.user) %}
|
||||||
<li>
|
<li>
|
||||||
<div invite-answer data-status="{{ invite.status|e('html_attr') }}"
|
<div invite-answer data-status="{{ invite.status|e('html_attr') }}"
|
||||||
@@ -213,12 +226,18 @@
|
|||||||
class="btn btn-show "></a>
|
class="btn btn-show "></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) %}
|
|
||||||
|
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) and calendar.status is not constant('STATUS_CANCELED', calendar) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { 'id': calendar.id }) }}"
|
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { 'id': calendar.id }) }}"
|
||||||
class="btn btn-update "></a>
|
class="btn btn-update "></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_cancel', { 'id': calendar.id } ) }}"
|
||||||
|
class="btn btn-action"><i class="bi bi-x-circle"></i> {{ 'Cancel'|trans }}</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
|
{% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_delete', { 'id': calendar.id } ) }}"
|
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_delete', { 'id': calendar.id } ) }}"
|
||||||
@@ -227,14 +246,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if calendarItems|length < paginator.getTotalItems %}
|
|
||||||
{{ chill_pagination(paginator) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
|
||||||
|
|
||||||
|
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
|
||||||
|
|
||||||
|
{% block title 'chill_calendar.cancel_calendar_item'|trans %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{{ form_start(form) }}
|
||||||
|
|
||||||
|
{{ form_row(form.cancelReason) }}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li class="cancel">
|
||||||
|
<a
|
||||||
|
class="btn btn-cancel"
|
||||||
|
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'id': accompanyingCourse.id } )}}"
|
||||||
|
>
|
||||||
|
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ form_widget(form.submit, { 'attr' : { 'class': 'btn btn-save' }, 'label': 'Save' } ) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{% extends "@ChillPerson/Person/layout.html.twig" %}
|
||||||
|
|
||||||
|
{% set activeRouteKey = 'chill_calendar_calendar_list' %}
|
||||||
|
|
||||||
|
{% block title 'chill_calendar.cancel_calendar_item'|trans %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{{ form_start(form) }}
|
||||||
|
|
||||||
|
{{ form_row(form.cancelReason) }}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li class="cancel">
|
||||||
|
<a
|
||||||
|
class="btn btn-cancel"
|
||||||
|
href="{{ chill_return_path_or('chill_calendar_calendar_list', { 'id': person.id } )}}"
|
||||||
|
>
|
||||||
|
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ form_widget(form.submit, { 'attr' : { 'class': 'btn btn-save' }, 'label': 'Save' } ) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -34,7 +34,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}
|
{% if calendarItems|length > 0 %}
|
||||||
|
<div class="flex-table list-records context-accompanyingCourse">
|
||||||
|
{% for calendar in calendarItems %}
|
||||||
|
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if calendarItems|length < paginator.getTotalItems %}
|
||||||
|
{{ chill_pagination(paginator) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
|||||||
@@ -33,7 +33,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}
|
{% if calendarItems|length > 0 %}
|
||||||
|
<div class="flex-table list-records context-person">
|
||||||
|
{% for calendar in calendarItems %}
|
||||||
|
{{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if calendarItems|length < paginator.getTotalItems %}
|
||||||
|
{{ chill_pagination(paginator) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% block table_entities_thead_tr %}
|
{% block table_entities_thead_tr %}
|
||||||
<th>{{ 'Id'|trans }}</th>
|
<th>{{ 'Id'|trans }}</th>
|
||||||
<th>{{ 'Name'|trans }}</th>
|
<th>{{ 'Name'|trans }}</th>
|
||||||
<th>{{ 'canceledBy'|trans }}</th>
|
<th>{{ 'Canceled by'|trans }}</th>
|
||||||
<th>{{ 'active'|trans }}</th>
|
<th>{{ 'active'|trans }}</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -40,4 +40,4 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endembed %}
|
{% endembed %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
|
|
||||||
|
{% set activeRouteKey = 'chill_calendar_invitations_list' %}
|
||||||
|
|
||||||
|
{% block title %}{{ 'invite.list.title'|trans }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{{ 'invite.list.title'|trans }}</h1>
|
||||||
|
|
||||||
|
{% if invitations|length == 0 %}
|
||||||
|
<p class="chill-no-data-statement">
|
||||||
|
{{ "invite.list.none"|trans }}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="flex-table list-records">
|
||||||
|
{% for invitation in invitations %}
|
||||||
|
{% set calendar = invitation.getCalendar %}
|
||||||
|
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'user'}) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if invitations|length < paginator.getTotalItems %}
|
||||||
|
{{ chill_pagination(paginator) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_script_tags('mod_answer') }}
|
||||||
|
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('mod_answer') }}
|
||||||
|
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||||
|
{% endblock %}
|
||||||
@@ -19,6 +19,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
|
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
|
||||||
|
|
||||||
use Chill\CalendarBundle\Entity\Calendar;
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
|
use Chill\CalendarBundle\Entity\CancelReason;
|
||||||
use libphonenumber\PhoneNumberFormat;
|
use libphonenumber\PhoneNumberFormat;
|
||||||
use libphonenumber\PhoneNumberUtil;
|
use libphonenumber\PhoneNumberUtil;
|
||||||
use Symfony\Component\Notifier\Message\SmsMessage;
|
use Symfony\Component\Notifier\Message\SmsMessage;
|
||||||
@@ -57,7 +58,7 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu
|
|||||||
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
|
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
|
||||||
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]),
|
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message.txt.twig', ['calendar' => $calendar]),
|
||||||
);
|
);
|
||||||
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) {
|
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus() && (null === $calendar->getCancelReason() || CancelReason::CANCELEDBY_PERSON !== $calendar->getCancelReason()->getCanceledBy())) {
|
||||||
$toUsers[] = new SmsMessage(
|
$toUsers[] = new SmsMessage(
|
||||||
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
|
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
|
||||||
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]),
|
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]),
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
<?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\CalendarBundle\Tests\Controller;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Controller\MyInvitationsController;
|
||||||
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
|
use Chill\CalendarBundle\Entity\Invite;
|
||||||
|
use Chill\CalendarBundle\Repository\InviteRepository;
|
||||||
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class MyInvitationsControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private MyInvitationsController $controller;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
// Create prophecies for dependencies
|
||||||
|
$inviteRepository = $this->prophesize(InviteRepository::class);
|
||||||
|
$paginatorFactory = $this->prophesize(PaginatorFactory::class);
|
||||||
|
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||||
|
|
||||||
|
// Create controller instance
|
||||||
|
$this->controller = new MyInvitationsController(
|
||||||
|
$inviteRepository->reveal(),
|
||||||
|
$paginatorFactory->reveal(),
|
||||||
|
$docGeneratorTemplateRepository->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up necessary services for AbstractController
|
||||||
|
$authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class);
|
||||||
|
$tokenStorage = $this->prophesize(TokenStorageInterface::class);
|
||||||
|
$twig = $this->prophesize(Environment::class);
|
||||||
|
|
||||||
|
// Use reflection to set the container
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
$containerProperty = $reflection->getParentClass()->getProperty('container');
|
||||||
|
$containerProperty->setAccessible(true);
|
||||||
|
|
||||||
|
// Create a mock container
|
||||||
|
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
|
||||||
|
$container->has('security.authorization_checker')->willReturn(true);
|
||||||
|
$container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal());
|
||||||
|
$container->has('security.token_storage')->willReturn(true);
|
||||||
|
$container->get('security.token_storage')->willReturn($tokenStorage->reveal());
|
||||||
|
$container->has('twig')->willReturn(true);
|
||||||
|
$container->get('twig')->willReturn($twig->reveal());
|
||||||
|
|
||||||
|
$containerProperty->setValue($this->controller, $container->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMyInvitationsReturnsCorrectAmountOfInvitations(): void
|
||||||
|
{
|
||||||
|
// Create test user
|
||||||
|
$user = new User();
|
||||||
|
$user->setUsername('testuser');
|
||||||
|
|
||||||
|
// Create test invitations
|
||||||
|
$invite1 = new Invite();
|
||||||
|
$invite1->setUser($user);
|
||||||
|
$invite1->setStatus(Invite::PENDING);
|
||||||
|
|
||||||
|
$invite2 = new Invite();
|
||||||
|
$invite2->setUser($user);
|
||||||
|
$invite2->setStatus(Invite::ACCEPTED);
|
||||||
|
|
||||||
|
$invite3 = new Invite();
|
||||||
|
$invite3->setUser($user);
|
||||||
|
$invite3->setStatus(Invite::DECLINED);
|
||||||
|
|
||||||
|
$allInvitations = [$invite1, $invite2, $invite3];
|
||||||
|
$paginatedInvitations = [$invite1, $invite2]; // First page with 2 items per page
|
||||||
|
|
||||||
|
// Set up repository prophecies
|
||||||
|
$inviteRepository = $this->prophesize(InviteRepository::class);
|
||||||
|
$inviteRepository->findBy(['user' => $user])->willReturn($allInvitations);
|
||||||
|
$inviteRepository->findBy(
|
||||||
|
['user' => $user],
|
||||||
|
['createdAt' => 'DESC'],
|
||||||
|
2, // items per page
|
||||||
|
0 // offset
|
||||||
|
)->willReturn($paginatedInvitations);
|
||||||
|
|
||||||
|
// Set up paginator prophecies
|
||||||
|
$paginator = $this->prophesize(PaginatorInterface::class);
|
||||||
|
$paginator->getItemsPerPage()->willReturn(2);
|
||||||
|
$paginator->getCurrentPageFirstItemNumber()->willReturn(0);
|
||||||
|
|
||||||
|
$paginatorFactory = $this->prophesize(PaginatorFactory::class);
|
||||||
|
$paginatorFactory->create(3)->willReturn($paginator->reveal());
|
||||||
|
|
||||||
|
// Set up doc generator repository
|
||||||
|
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||||
|
$docGeneratorTemplateRepository->findByEntity(Calendar::class)->willReturn([]);
|
||||||
|
|
||||||
|
// Create controller with mocked dependencies
|
||||||
|
$controller = new MyInvitationsController(
|
||||||
|
$inviteRepository->reveal(),
|
||||||
|
$paginatorFactory->reveal(),
|
||||||
|
$docGeneratorTemplateRepository->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up authorization checker to return true for ROLE_USER
|
||||||
|
$authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class);
|
||||||
|
$authorizationChecker->isGranted('ROLE_USER', null)->willReturn(true);
|
||||||
|
|
||||||
|
// Set up token storage to return user
|
||||||
|
$token = $this->prophesize(TokenInterface::class);
|
||||||
|
$token->getUser()->willReturn($user);
|
||||||
|
$tokenStorage = $this->prophesize(TokenStorageInterface::class);
|
||||||
|
$tokenStorage->getToken()->willReturn($token->reveal());
|
||||||
|
|
||||||
|
// Set up twig to return a response
|
||||||
|
$twig = $this->prophesize(Environment::class);
|
||||||
|
$twig->render('@ChillCalendar/Invitations/listByUser.html.twig', [
|
||||||
|
'invitations' => $paginatedInvitations,
|
||||||
|
'paginator' => $paginator->reveal(),
|
||||||
|
'templates' => [],
|
||||||
|
])->willReturn('rendered content');
|
||||||
|
|
||||||
|
// Set up container
|
||||||
|
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
|
||||||
|
$container->has('security.authorization_checker')->willReturn(true);
|
||||||
|
$container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal());
|
||||||
|
$container->has('security.token_storage')->willReturn(true);
|
||||||
|
$container->get('security.token_storage')->willReturn($tokenStorage->reveal());
|
||||||
|
$container->has('twig')->willReturn(true);
|
||||||
|
$container->get('twig')->willReturn($twig->reveal());
|
||||||
|
|
||||||
|
// Use reflection to set the container
|
||||||
|
$reflection = new \ReflectionClass($controller);
|
||||||
|
$containerProperty = $reflection->getParentClass()->getProperty('container');
|
||||||
|
$containerProperty->setAccessible(true);
|
||||||
|
$containerProperty->setValue($controller, $container->reveal());
|
||||||
|
|
||||||
|
// Create request
|
||||||
|
$request = new Request();
|
||||||
|
|
||||||
|
// Execute the action
|
||||||
|
$response = $controller->myInvitations($request);
|
||||||
|
|
||||||
|
// Assert that response is successful
|
||||||
|
self::assertInstanceOf(Response::class, $response);
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
self::assertSame('rendered content', $response->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMyInvitationsPageLoads(): void
|
||||||
|
{
|
||||||
|
// Create test user
|
||||||
|
$user = new User();
|
||||||
|
$user->setUsername('testuser');
|
||||||
|
|
||||||
|
// Set up repository prophecies - no invitations
|
||||||
|
$inviteRepository = $this->prophesize(InviteRepository::class);
|
||||||
|
$inviteRepository->findBy(['user' => $user])->willReturn([]);
|
||||||
|
$inviteRepository->findBy(
|
||||||
|
['user' => $user],
|
||||||
|
['createdAt' => 'DESC'],
|
||||||
|
20, // default items per page
|
||||||
|
0 // offset
|
||||||
|
)->willReturn([]);
|
||||||
|
|
||||||
|
// Set up paginator prophecies
|
||||||
|
$paginator = $this->prophesize(PaginatorInterface::class);
|
||||||
|
$paginator->getItemsPerPage()->willReturn(20);
|
||||||
|
$paginator->getCurrentPageFirstItemNumber()->willReturn(0);
|
||||||
|
|
||||||
|
$paginatorFactory = $this->prophesize(PaginatorFactory::class);
|
||||||
|
$paginatorFactory->create(0)->willReturn($paginator->reveal());
|
||||||
|
|
||||||
|
// Set up doc generator repository
|
||||||
|
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||||
|
$docGeneratorTemplateRepository->findByEntity(Calendar::class)->willReturn([]);
|
||||||
|
|
||||||
|
// Create controller with mocked dependencies
|
||||||
|
$controller = new MyInvitationsController(
|
||||||
|
$inviteRepository->reveal(),
|
||||||
|
$paginatorFactory->reveal(),
|
||||||
|
$docGeneratorTemplateRepository->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up authorization checker to return true for ROLE_USER
|
||||||
|
$authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class);
|
||||||
|
$authorizationChecker->isGranted('ROLE_USER', null)->willReturn(true);
|
||||||
|
|
||||||
|
// Set up token storage to return user
|
||||||
|
$token = $this->prophesize(TokenInterface::class);
|
||||||
|
$token->getUser()->willReturn($user);
|
||||||
|
$tokenStorage = $this->prophesize(TokenStorageInterface::class);
|
||||||
|
$tokenStorage->getToken()->willReturn($token->reveal());
|
||||||
|
|
||||||
|
// Set up twig to return a response
|
||||||
|
$twig = $this->prophesize(Environment::class);
|
||||||
|
$twig->render('@ChillCalendar/Invitations/listByUser.html.twig', [
|
||||||
|
'invitations' => [],
|
||||||
|
'paginator' => $paginator->reveal(),
|
||||||
|
'templates' => [],
|
||||||
|
])->willReturn('empty page content');
|
||||||
|
|
||||||
|
// Set up container
|
||||||
|
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
|
||||||
|
$container->has('security.authorization_checker')->willReturn(true);
|
||||||
|
$container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal());
|
||||||
|
$container->has('security.token_storage')->willReturn(true);
|
||||||
|
$container->get('security.token_storage')->willReturn($tokenStorage->reveal());
|
||||||
|
$container->has('twig')->willReturn(true);
|
||||||
|
$container->get('twig')->willReturn($twig->reveal());
|
||||||
|
|
||||||
|
// Use reflection to set the container
|
||||||
|
$reflection = new \ReflectionClass($controller);
|
||||||
|
$containerProperty = $reflection->getParentClass()->getProperty('container');
|
||||||
|
$containerProperty->setAccessible(true);
|
||||||
|
$containerProperty->setValue($controller, $container->reveal());
|
||||||
|
|
||||||
|
// Create request
|
||||||
|
$request = new Request();
|
||||||
|
|
||||||
|
// Execute the action
|
||||||
|
$response = $controller->myInvitations($request);
|
||||||
|
|
||||||
|
// Assert that page loads successfully
|
||||||
|
self::assertInstanceOf(Response::class, $response);
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
self::assertSame('empty page content', $response->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMyInvitationsRequiresAuthentication(): void
|
||||||
|
{
|
||||||
|
// Create controller with minimal dependencies
|
||||||
|
$inviteRepository = $this->prophesize(InviteRepository::class);
|
||||||
|
$paginatorFactory = $this->prophesize(PaginatorFactory::class);
|
||||||
|
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||||
|
|
||||||
|
$controller = new MyInvitationsController(
|
||||||
|
$inviteRepository->reveal(),
|
||||||
|
$paginatorFactory->reveal(),
|
||||||
|
$docGeneratorTemplateRepository->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up authorization checker to return false for ROLE_USER
|
||||||
|
$authorizationChecker = $this->prophesize(AuthorizationCheckerInterface::class);
|
||||||
|
$authorizationChecker->isGranted('ROLE_USER')->willReturn(false);
|
||||||
|
$authorizationChecker->isGranted('ROLE_USER', null)->willReturn(false);
|
||||||
|
|
||||||
|
// Set up container
|
||||||
|
$container = $this->prophesize(\Psr\Container\ContainerInterface::class);
|
||||||
|
$container->has('security.authorization_checker')->willReturn(true);
|
||||||
|
$container->get('security.authorization_checker')->willReturn($authorizationChecker->reveal());
|
||||||
|
|
||||||
|
// Use reflection to set the container
|
||||||
|
$reflection = new \ReflectionClass($controller);
|
||||||
|
$containerProperty = $reflection->getParentClass()->getProperty('container');
|
||||||
|
$containerProperty->setAccessible(true);
|
||||||
|
$containerProperty->setValue($controller, $container->reveal());
|
||||||
|
|
||||||
|
// Create request
|
||||||
|
$request = new Request();
|
||||||
|
|
||||||
|
// Expect AccessDeniedException
|
||||||
|
$this->expectException(\Symfony\Component\Security\Core\Exception\AccessDeniedException::class);
|
||||||
|
|
||||||
|
// Execute the action
|
||||||
|
$controller->myInvitations($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,8 +31,7 @@ Will send SMS: Un SMS de rappel sera envoyé
|
|||||||
Will not send SMS: Aucun SMS de rappel ne sera envoyé
|
Will not send SMS: Aucun SMS de rappel ne sera envoyé
|
||||||
SMS already sent: Un SMS a été envoyé
|
SMS already sent: Un SMS a été envoyé
|
||||||
|
|
||||||
canceledBy: supprimé par
|
Canceled by: Annulé par
|
||||||
Canceled by: supprimé par
|
|
||||||
Calendar configuration: Gestion des rendez-vous
|
Calendar configuration: Gestion des rendez-vous
|
||||||
|
|
||||||
crud:
|
crud:
|
||||||
@@ -44,6 +43,14 @@ crud:
|
|||||||
title_edit: Modifier le motif d'annulation
|
title_edit: Modifier le motif d'annulation
|
||||||
|
|
||||||
chill_calendar:
|
chill_calendar:
|
||||||
|
canceled: Annulé
|
||||||
|
cancel_reason: Raison d'annulation
|
||||||
|
cancel_calendar_item: Annuler rendez-vous
|
||||||
|
calendar_canceled: Le rendez-vous a été annulé
|
||||||
|
canceled_by:
|
||||||
|
user: Utilisateur
|
||||||
|
person: Usager
|
||||||
|
other: Autre
|
||||||
Document: Document d'un rendez-vous
|
Document: Document d'un rendez-vous
|
||||||
form:
|
form:
|
||||||
The main user is mandatory. He will organize the appointment.: L'utilisateur principal est obligatoire. Il est l'organisateur de l'événement.
|
The main user is mandatory. He will organize the appointment.: L'utilisateur principal est obligatoire. Il est l'organisateur de l'événement.
|
||||||
@@ -86,6 +93,9 @@ invite:
|
|||||||
declined: Refusé
|
declined: Refusé
|
||||||
pending: En attente
|
pending: En attente
|
||||||
tentative: Accepté provisoirement
|
tentative: Accepté provisoirement
|
||||||
|
list:
|
||||||
|
none: Il n'y aucun invitation
|
||||||
|
title: Mes invitations
|
||||||
|
|
||||||
# exports
|
# exports
|
||||||
Exports of calendar: Exports des rendez-vous
|
Exports of calendar: Exports des rendez-vous
|
||||||
|
|||||||
@@ -20,4 +20,9 @@ use Doctrine\Persistence\ObjectRepository;
|
|||||||
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
|
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
|
||||||
{
|
{
|
||||||
public function countByEntity(string $entity): int;
|
public function countByEntity(string $entity): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|DocGeneratorTemplate[]
|
||||||
|
*/
|
||||||
|
public function findByEntity(string $entity, ?int $start = 0, ?int $limit = 50): array;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ use Symfony\Component\Mailer\MailerInterface;
|
|||||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see OnGenerationFailsTest for test suite
|
* @see OnGenerationFailsTest for test suite
|
||||||
*/
|
*/
|
||||||
@@ -40,6 +42,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
private StoredObjectRepositoryInterface $storedObjectRepository,
|
private StoredObjectRepositoryInterface $storedObjectRepository,
|
||||||
private TranslatorInterface $translator,
|
private TranslatorInterface $translator,
|
||||||
private UserRepositoryInterface $userRepository,
|
private UserRepositoryInterface $userRepository,
|
||||||
|
// private LocaleSwitcher $localeSwitcher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static function getSubscribedEvents()
|
public static function getSubscribedEvents()
|
||||||
@@ -118,6 +121,25 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||||
|
/*
|
||||||
|
$this->localeSwitcher->runWithLocale($creator->getLocale(), function () use ($message, $errors, $template, $creator) {
|
||||||
|
$email = (new TemplatedEmail())
|
||||||
|
->to($message->getSendResultToEmail())
|
||||||
|
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||||
|
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
||||||
|
->context([
|
||||||
|
'errors' => $errors,
|
||||||
|
'template' => $template,
|
||||||
|
'creator' => $creator,
|
||||||
|
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Current implementation:
|
||||||
$email = (new TemplatedEmail())
|
$email = (new TemplatedEmail())
|
||||||
->to($message->getSendResultToEmail())
|
->to($message->getSendResultToEmail())
|
||||||
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
|||||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the request of document generation.
|
* Handle the request of document generation.
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +48,7 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
private readonly MailerInterface $mailer,
|
private readonly MailerInterface $mailer,
|
||||||
private readonly TranslatorInterface $translator,
|
private readonly TranslatorInterface $translator,
|
||||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||||
|
// private readonly LocaleSwitcher $localeSwitcher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function __invoke(RequestGenerationMessage $message)
|
public function __invoke(RequestGenerationMessage $message)
|
||||||
@@ -122,6 +125,30 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
|
|
||||||
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
|
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
|
||||||
{
|
{
|
||||||
|
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||||
|
// Note: This method sends emails to admin addresses, not user addresses, so locale switching may not be needed
|
||||||
|
/*
|
||||||
|
$this->localeSwitcher->runWithLocale('fr', function () use ($destinationStoredObject, $message) {
|
||||||
|
// Get the content of the document
|
||||||
|
$content = $this->storedObjectManager->read($destinationStoredObject);
|
||||||
|
$filename = $destinationStoredObject->getFilename();
|
||||||
|
$contentType = $destinationStoredObject->getType();
|
||||||
|
|
||||||
|
// Create the email with the document as an attachment
|
||||||
|
$email = (new TemplatedEmail())
|
||||||
|
->to($message->getSendResultToEmail())
|
||||||
|
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
|
||||||
|
->context([
|
||||||
|
'filename' => $filename,
|
||||||
|
])
|
||||||
|
->subject($this->translator->trans('docgen.data_dump_email.subject'))
|
||||||
|
->attach($content, $filename, $contentType);
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Current implementation:
|
||||||
// Get the content of the document
|
// Get the content of the document
|
||||||
$content = $this->storedObjectManager->read($destinationStoredObject);
|
$content = $this->storedObjectManager->read($destinationStoredObject);
|
||||||
$filename = $destinationStoredObject->getFilename();
|
$filename = $destinationStoredObject->getFilename();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
|
|||||||
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
$this->serializer->serialize(
|
$this->serializer->serialize(
|
||||||
new Collection($items, $paginator),
|
new Collection(array_values($items->toArray()), $paginator),
|
||||||
'json',
|
'json',
|
||||||
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ use Random\RandomException;
|
|||||||
* Store each version of StoredObject's.
|
* Store each version of StoredObject's.
|
||||||
*
|
*
|
||||||
* A version should not be created manually: use the method @see{StoredObject::registerVersion} instead.
|
* A version should not be created manually: use the method @see{StoredObject::registerVersion} instead.
|
||||||
|
*
|
||||||
|
* Each filename must be unique within the same StoredObject. We add a condition on id to apply this condition only for
|
||||||
|
* newly created versions when this new index is applied.
|
||||||
*/
|
*/
|
||||||
#[ORM\Entity]
|
#[ORM\Entity]
|
||||||
#[ORM\Table('chill_doc.stored_object_version')]
|
#[ORM\Table('chill_doc.stored_object_version')]
|
||||||
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])]
|
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])]
|
||||||
|
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_filename', columns: ['filename'], options: ['where' => '(id > 0)'])]
|
||||||
class StoredObjectVersion implements TrackCreationInterface
|
class StoredObjectVersion implements TrackCreationInterface
|
||||||
{
|
{
|
||||||
use TrackCreationTrait;
|
use TrackCreationTrait;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use Chill\DocStoreBundle\Entity\PersonDocument;
|
|||||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
|
||||||
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
|
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
@@ -30,7 +29,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
|
|
||||||
class PersonDocumentType extends AbstractType
|
class PersonDocumentType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag, private readonly CenterResolverDispatcher $centerResolverDispatcher) {}
|
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag) {}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
@@ -57,8 +56,8 @@ class PersonDocumentType extends AbstractType
|
|||||||
|
|
||||||
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
|
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
|
||||||
$builder->add('scope', ScopePickerType::class, [
|
$builder->add('scope', ScopePickerType::class, [
|
||||||
'center' => $this->centerResolverDispatcher->resolveCenter($document),
|
|
||||||
'role' => $options['role'],
|
'role' => $options['role'],
|
||||||
|
'subject' => $document,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ export interface GenericDocForAccompanyingPeriod extends GenericDoc {
|
|||||||
context: "accompanying-period";
|
context: "accompanying-period";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGenericDocForAccompanyingPeriod(
|
||||||
|
doc: GenericDoc,
|
||||||
|
): doc is GenericDocForAccompanyingPeriod {
|
||||||
|
return doc.context === "accompanying-period";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGenericDocWithStoredObject(
|
||||||
|
doc: GenericDoc,
|
||||||
|
): doc is GenericDoc & { storedObject: StoredObject } {
|
||||||
|
return doc.storedObject !== null;
|
||||||
|
}
|
||||||
|
|
||||||
interface BaseMetadataWithHtml extends BaseMetadata {
|
interface BaseMetadataWithHtml extends BaseMetadata {
|
||||||
html: string;
|
html: string;
|
||||||
}
|
}
|
||||||
@@ -44,28 +56,33 @@ export interface GenericDocForAccompanyingCourseDocument
|
|||||||
extends GenericDocForAccompanyingPeriod {
|
extends GenericDocForAccompanyingPeriod {
|
||||||
key: "accompanying_course_document";
|
key: "accompanying_course_document";
|
||||||
metadata: BaseMetadataWithHtml;
|
metadata: BaseMetadataWithHtml;
|
||||||
|
storedObject: StoredObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericDocForAccompanyingCourseActivityDocument
|
export interface GenericDocForAccompanyingCourseActivityDocument
|
||||||
extends GenericDocForAccompanyingPeriod {
|
extends GenericDocForAccompanyingPeriod {
|
||||||
key: "accompanying_course_activity_document";
|
key: "accompanying_course_activity_document";
|
||||||
metadata: BaseMetadataWithHtml;
|
metadata: BaseMetadataWithHtml;
|
||||||
|
storedObject: StoredObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericDocForAccompanyingCourseCalendarDocument
|
export interface GenericDocForAccompanyingCourseCalendarDocument
|
||||||
extends GenericDocForAccompanyingPeriod {
|
extends GenericDocForAccompanyingPeriod {
|
||||||
key: "accompanying_course_calendar_document";
|
key: "accompanying_course_calendar_document";
|
||||||
metadata: BaseMetadataWithHtml;
|
metadata: BaseMetadataWithHtml;
|
||||||
|
storedObject: StoredObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericDocForAccompanyingCoursePersonDocument
|
export interface GenericDocForAccompanyingCoursePersonDocument
|
||||||
extends GenericDocForAccompanyingPeriod {
|
extends GenericDocForAccompanyingPeriod {
|
||||||
key: "person_document";
|
key: "person_document";
|
||||||
metadata: BaseMetadataWithHtml;
|
metadata: BaseMetadataWithHtml;
|
||||||
|
storedObject: StoredObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
|
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
|
||||||
extends GenericDocForAccompanyingPeriod {
|
extends GenericDocForAccompanyingPeriod {
|
||||||
key: "accompanying_period_work_evaluation_document";
|
key: "accompanying_period_work_evaluation_document";
|
||||||
metadata: BaseMetadataWithHtml;
|
metadata: BaseMetadataWithHtml;
|
||||||
|
storedObject: StoredObject;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import {
|
|||||||
StoredObject,
|
StoredObject,
|
||||||
StoredObjectPointInTime,
|
StoredObjectPointInTime,
|
||||||
StoredObjectVersionWithPointInTime,
|
StoredObjectVersionWithPointInTime,
|
||||||
} from "./../../../types";
|
} from "ChillDocStoreAssets/types";
|
||||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||||
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
|
||||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
||||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
|||||||
$storedObject->registerVersion();
|
$storedObject->registerVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove one version in the history
|
||||||
|
$v5 = $storedObject->getVersions()->get(5);
|
||||||
|
$storedObject->removeVersion($v5);
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
$security = $this->prophesize(Security::class);
|
||||||
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
||||||
->willReturn(true)
|
->willReturn(true)
|
||||||
@@ -53,6 +57,7 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
|||||||
self::assertEquals($response->getStatusCode(), 200);
|
self::assertEquals($response->getStatusCode(), 200);
|
||||||
self::assertIsArray($body);
|
self::assertIsArray($body);
|
||||||
self::assertArrayHasKey('results', $body);
|
self::assertArrayHasKey('results', $body);
|
||||||
|
self::assertIsList($body['results']);
|
||||||
self::assertCount(10, $body['results']);
|
self::assertCount(10, $body['results']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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\Migrations\DocStore;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20251013094414 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'DocStore: Enforce filename uniqueness on chill_doc.stored_object_version; clean duplicates and add partial unique index on filename (for new rows only).';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// 1) Clean duplicates: for each (stored_object_id, filename, key, iv), keep only the last inserted row
|
||||||
|
// and delete all others. Use ROW_NUMBER over id DESC to define the last one.
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT id,
|
||||||
|
rank() OVER (
|
||||||
|
PARTITION BY stored_object_id, filename, "key"::jsonb, iv::jsonb
|
||||||
|
ORDER BY id DESC
|
||||||
|
) AS rn
|
||||||
|
FROM chill_doc.stored_object_version
|
||||||
|
)
|
||||||
|
DELETE FROM chill_doc.stored_object_version sov
|
||||||
|
USING ranked r
|
||||||
|
WHERE sov.id = r.id
|
||||||
|
AND r.rn > 1
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
// 2) Create a partial unique index on filename that applies only to subsequently inserted rows.
|
||||||
|
// Per user's instruction, compute the cutoff using the stored_object_id sequence value.
|
||||||
|
$nextVal = (int) $this->connection->fetchOne("SELECT nextval('chill_doc.stored_object_version_id_seq')");
|
||||||
|
|
||||||
|
// Safety: if somehow sequence is not available, fallback to current max id from the table
|
||||||
|
if ($nextVal <= 0) {
|
||||||
|
$nextVal = (int) $this->connection->fetchOne('SELECT COALESCE(MAX(id), 0) FROM chill_doc.stored_object_version');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addSql(sprintf(
|
||||||
|
'CREATE UNIQUE INDEX chill_doc_stored_object_version_unique_by_filename ON chill_doc.stored_object_version (filename) WHERE id > %d',
|
||||||
|
$nextVal
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Drop the partial unique index; data cleanup is irreversible.
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS chill_doc_stored_object_version_unique_by_filename');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -246,7 +246,7 @@ final class EventController extends AbstractController
|
|||||||
'class' => Center::class,
|
'class' => Center::class,
|
||||||
'choices' => $centers,
|
'choices' => $centers,
|
||||||
'placeholder' => $this->translator->trans('Pick a center'),
|
'placeholder' => $this->translator->trans('Pick a center'),
|
||||||
'label' => 'To which centre should the event be associated ?',
|
'label' => 'To which territory should the event be associated ?',
|
||||||
])
|
])
|
||||||
->add('submit', SubmitType::class, [
|
->add('submit', SubmitType::class, [
|
||||||
'label' => 'Next step',
|
'label' => 'Next step',
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ CHILL_EVENT_PARTICIPATION_SEE_DETAILS: Voir le détail d'une participation
|
|||||||
|
|
||||||
# TODO check place to put this
|
# TODO check place to put this
|
||||||
Next step: Étape suivante
|
Next step: Étape suivante
|
||||||
To which centre should the event be associated ?: À quel centre doit être associé l'événement ?
|
To which territory should the event be associated ?: À quel territoire doit être associé l'événement ?
|
||||||
|
|
||||||
# timeline
|
# timeline
|
||||||
past: passé
|
past: passé
|
||||||
@@ -151,7 +151,7 @@ event:
|
|||||||
filter:
|
filter:
|
||||||
event_types: Par types d'événement
|
event_types: Par types d'événement
|
||||||
event_dates: Par date d'événement
|
event_dates: Par date d'événement
|
||||||
center: Par centre
|
center: Par territoire
|
||||||
by_responsable: Par responsable
|
by_responsable: Par responsable
|
||||||
pick_responsable: Filtrer par responsables
|
pick_responsable: Filtrer par responsables
|
||||||
budget:
|
budget:
|
||||||
@@ -188,7 +188,7 @@ event_id: Identifiant
|
|||||||
event_name: Nom
|
event_name: Nom
|
||||||
event_date: Date
|
event_date: Date
|
||||||
event_type: Type d'évenement
|
event_type: Type d'évenement
|
||||||
event_center: Centre
|
event_center: Territoire
|
||||||
event_moderator: Responsable
|
event_moderator: Responsable
|
||||||
event_participants_count: Nombre de participants
|
event_participants_count: Nombre de participants
|
||||||
event_location: Localisation
|
event_location: Localisation
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\User;
|
|||||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
final class UpdateProfileCommand
|
final class UpdateProfileCommand
|
||||||
{
|
{
|
||||||
@@ -23,11 +24,13 @@ final class UpdateProfileCommand
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
#[PhonenumberConstraint]
|
#[PhonenumberConstraint]
|
||||||
public ?PhoneNumber $phonenumber,
|
public ?PhoneNumber $phonenumber,
|
||||||
|
#[Assert\Choice(choices: ['fr', 'nl'], message: 'Locale must be either "fr" or "nl"')]
|
||||||
|
public string $locale = 'fr',
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static function create(User $user, NotificationFlagManager $flagManager): self
|
public static function create(User $user, NotificationFlagManager $flagManager): self
|
||||||
{
|
{
|
||||||
$updateProfileCommand = new self($user->getPhonenumber());
|
$updateProfileCommand = new self($user->getPhonenumber(), $user->getLocale());
|
||||||
|
|
||||||
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
|
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
|
||||||
$updateProfileCommand->setNotificationFlag(
|
$updateProfileCommand->setNotificationFlag(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ final readonly class UpdateProfileCommandHandler
|
|||||||
public function updateProfile(User $user, UpdateProfileCommand $command): void
|
public function updateProfile(User $user, UpdateProfileCommand $command): void
|
||||||
{
|
{
|
||||||
$user->setPhonenumber($command->phonenumber);
|
$user->setPhonenumber($command->phonenumber);
|
||||||
|
$user->setLocale($command->locale);
|
||||||
|
|
||||||
foreach ($command->notificationFlags as $flag => $values) {
|
foreach ($command->notificationFlags as $flag => $values) {
|
||||||
$user->setNotificationImmediately($flag, $values['immediate_email']);
|
$user->setNotificationImmediately($flag, $values['immediate_email']);
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ class CRUDController extends AbstractController
|
|||||||
Resolver::class => Resolver::class,
|
Resolver::class => Resolver::class,
|
||||||
SerializerInterface::class => SerializerInterface::class,
|
SerializerInterface::class => SerializerInterface::class,
|
||||||
FilterOrderHelperFactoryInterface::class => FilterOrderHelperFactoryInterface::class,
|
FilterOrderHelperFactoryInterface::class => FilterOrderHelperFactoryInterface::class,
|
||||||
ManagerRegistry::class => ManagerRegistry::class,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -674,7 +673,7 @@ class CRUDController extends AbstractController
|
|||||||
|
|
||||||
protected function getManagerRegistry(): ManagerRegistry
|
protected function getManagerRegistry(): ManagerRegistry
|
||||||
{
|
{
|
||||||
return $this->container->get(ManagerRegistry::class);
|
return $this->container->get('doctrine');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ class ChillImportUsersCommand extends Command
|
|||||||
|
|
||||||
protected function loadUsers()
|
protected function loadUsers()
|
||||||
{
|
{
|
||||||
$reader = Reader::createFromPath($this->tempInput->getArgument('csvfile'));
|
$reader = Reader::from($this->tempInput->getArgument('csvfile'));
|
||||||
$reader->setHeaderOffset(0);
|
$reader->setHeaderOffset(0);
|
||||||
|
|
||||||
foreach ($reader->getRecords() as $line => $r) {
|
foreach ($reader->getRecords() as $line => $r) {
|
||||||
@@ -362,7 +362,7 @@ class ChillImportUsersCommand extends Command
|
|||||||
|
|
||||||
protected function prepareGroupingCenters()
|
protected function prepareGroupingCenters()
|
||||||
{
|
{
|
||||||
$reader = Reader::createFromPath($this->tempInput->getOption('grouping-centers'));
|
$reader = Reader::from($this->tempInput->getOption('grouping-centers'));
|
||||||
$reader->setHeaderOffset(0);
|
$reader->setHeaderOffset(0);
|
||||||
|
|
||||||
foreach ($reader->getRecords() as $r) {
|
foreach ($reader->getRecords() as $r) {
|
||||||
@@ -378,7 +378,7 @@ class ChillImportUsersCommand extends Command
|
|||||||
|
|
||||||
protected function prepareWriter()
|
protected function prepareWriter()
|
||||||
{
|
{
|
||||||
$this->output = $output = Writer::createFromPath($this->tempInput
|
$this->output = $output = Writer::from($this->tempInput
|
||||||
->getOption('csv-dump'), 'a+');
|
->getOption('csv-dump'), 'a+');
|
||||||
|
|
||||||
$output->insertOne([
|
$output->insertOne([
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class ChillUserSendRenewPasswordCodeCommand extends Command
|
|||||||
protected function getReader()
|
protected function getReader()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$reader = Reader::createFromPath($this->input->getArgument('csvfile'));
|
$reader = Reader::from($this->input->getArgument('csvfile'));
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->error('The csv file could not be read', [
|
$this->logger->error('The csv file could not be read', [
|
||||||
'path' => $this->input->getArgument('csvfile'),
|
'path' => $this->input->getArgument('csvfile'),
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ final readonly class UserExportController
|
|||||||
|
|
||||||
$users = $this->userRepository->findAllAsArray($request->getLocale());
|
$users = $this->userRepository->findAllAsArray($request->getLocale());
|
||||||
|
|
||||||
$csv = Writer::createFromPath('php://temp', 'r+');
|
$csv = Writer::from('php://temp', 'r+');
|
||||||
$csv->insertOne(
|
$csv->insertOne(
|
||||||
array_map(
|
array_map(
|
||||||
fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
|
fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
|
||||||
@@ -104,7 +104,7 @@ final readonly class UserExportController
|
|||||||
|
|
||||||
$userPermissions = $this->userRepository->findAllUserACLAsArray();
|
$userPermissions = $this->userRepository->findAllUserACLAsArray();
|
||||||
|
|
||||||
$csv = Writer::createFromPath('php://temp', 'r+');
|
$csv = Writer::from('php://temp', 'r+');
|
||||||
$csv->insertOne(
|
$csv->insertOne(
|
||||||
array_map(
|
array_map(
|
||||||
fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
|
fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
|
||||||
|
|||||||
@@ -264,11 +264,12 @@ class WorkflowController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||||
|
|
||||||
$total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser());
|
$total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser(), false);
|
||||||
$paginator = $this->paginatorFactory->create($total);
|
$paginator = $this->paginatorFactory->create($total);
|
||||||
|
|
||||||
$workflows = $this->entityWorkflowRepository->findBySubscriber(
|
$workflows = $this->entityWorkflowRepository->findBySubscriber(
|
||||||
$this->security->getUser(),
|
$this->security->getUser(),
|
||||||
|
false,
|
||||||
['createdAt' => 'DESC'],
|
['createdAt' => 'DESC'],
|
||||||
$paginator->getItemsPerPage(),
|
$paginator->getItemsPerPage(),
|
||||||
$paginator->getCurrentPageFirstItemNumber()
|
$paginator->getCurrentPageFirstItemNumber()
|
||||||
|
|||||||
@@ -205,6 +205,11 @@ class ChillMainExtension extends Extension implements
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$container->setParameter(
|
||||||
|
'chill_main.top_banner',
|
||||||
|
$config['top_banner'] ?? []
|
||||||
|
);
|
||||||
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||||
$loader->load('services.yaml');
|
$loader->load('services.yaml');
|
||||||
$loader->load('services/doctrine.yaml');
|
$loader->load('services/doctrine.yaml');
|
||||||
@@ -250,6 +255,7 @@ class ChillMainExtension extends Extension implements
|
|||||||
'name' => $config['installation_name'], ],
|
'name' => $config['installation_name'], ],
|
||||||
'available_languages' => $config['available_languages'],
|
'available_languages' => $config['available_languages'],
|
||||||
'add_address' => $config['add_address'],
|
'add_address' => $config['add_address'],
|
||||||
|
'chill_main_config' => $config,
|
||||||
],
|
],
|
||||||
'form_themes' => ['@ChillMain/Form/fields.html.twig'],
|
'form_themes' => ['@ChillMain/Form/fields.html.twig'],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -168,6 +168,20 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
->arrayNode('top_banner')
|
||||||
|
->canBeUnset()
|
||||||
|
->children()
|
||||||
|
->booleanNode('visible')
|
||||||
|
->defaultFalse()
|
||||||
|
->end()
|
||||||
|
->arrayNode('text')
|
||||||
|
->useAttributeAsKey('lang')
|
||||||
|
->scalarPrototype()->end()
|
||||||
|
->end() // end of text
|
||||||
|
->scalarNode('color')->defaultNull()->end()
|
||||||
|
->scalarNode('background_color')->defaultNull()->end()
|
||||||
|
->end() // end of top_banner children
|
||||||
|
->end() // end of top_banner
|
||||||
->arrayNode('widgets')
|
->arrayNode('widgets')
|
||||||
->canBeEnabled()
|
->canBeEnabled()
|
||||||
->canBeUnset()
|
->canBeUnset()
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||||
private array $notificationFlags = [];
|
private array $notificationFlags = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's preferred locale.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 5, nullable: false, options: ['default' => 'fr'])]
|
||||||
|
private string $locale = 'fr';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User constructor.
|
* User constructor.
|
||||||
*/
|
*/
|
||||||
@@ -716,7 +722,14 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
|
|
||||||
public function getLocale(): string
|
public function getLocale(): string
|
||||||
{
|
{
|
||||||
return 'fr';
|
return $this->locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLocale(string $locale): self
|
||||||
|
{
|
||||||
|
$this->locale = $locale;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Assert\Callback]
|
#[Assert\Callback]
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class EntityWorkflowStep
|
|||||||
/**
|
/**
|
||||||
* @var Collection<int, EntityWorkflowStepHold>
|
* @var Collection<int, EntityWorkflowStepHold>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
|
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class, cascade: ['remove'])]
|
||||||
private Collection $holdsOnStep;
|
private Collection $holdsOnStep;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Scope;
|
|||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
|
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||||
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
@@ -32,65 +33,84 @@ use Symfony\Component\Security\Core\Security;
|
|||||||
* Allow to pick amongst available scope for the current
|
* Allow to pick amongst available scope for the current
|
||||||
* user.
|
* user.
|
||||||
*
|
*
|
||||||
* options :
|
* Options:
|
||||||
*
|
* - `role`: string, the role to check permissions for
|
||||||
* - `center`: the center of the entity
|
* - Either `subject`: object, entity to resolve centers from
|
||||||
* - `role` : the role of the user
|
* - Or `center`: Center|array|null, the center(s) to check
|
||||||
*/
|
*/
|
||||||
class ScopePickerType extends AbstractType
|
class ScopePickerType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
private readonly AuthorizationHelperInterface $authorizationHelper,
|
private readonly AuthorizationHelperInterface $authorizationHelper,
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
private readonly CenterResolverManagerInterface $centerResolverManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
$items = array_values(
|
// Compute centers from subject
|
||||||
|
$centers = $options['center'] ?? null;
|
||||||
|
if (null === $centers && isset($options['subject'])) {
|
||||||
|
$centers = $this->centerResolverManager->resolveCenters($options['subject']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $centers) {
|
||||||
|
throw new \RuntimeException('Either "center" or "subject" must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reachableScopes = array_values(
|
||||||
array_filter(
|
array_filter(
|
||||||
$this->authorizationHelper->getReachableScopes(
|
$this->authorizationHelper->getReachableScopes(
|
||||||
$this->security->getUser(),
|
$this->security->getUser(),
|
||||||
$options['role'],
|
$options['role'],
|
||||||
$options['center']
|
$centers
|
||||||
),
|
),
|
||||||
static fn (Scope $s) => $s->isActive()
|
static fn (Scope $s) => $s->isActive()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (0 === \count($items)) {
|
$builder->setAttribute('reachable_scopes_count', count($reachableScopes));
|
||||||
throw new \RuntimeException('no scopes are reachable. This form should not be shown to user');
|
|
||||||
|
if (0 === count($reachableScopes)) {
|
||||||
|
$builder->setAttribute('has_scopes', false);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (1 !== \count($items)) {
|
$builder->setAttribute('has_scopes', true);
|
||||||
|
|
||||||
|
if (1 !== count($reachableScopes)) {
|
||||||
$builder->add('scope', EntityType::class, [
|
$builder->add('scope', EntityType::class, [
|
||||||
'class' => Scope::class,
|
'class' => Scope::class,
|
||||||
'placeholder' => 'Choose the circle',
|
'placeholder' => 'Choose the circle',
|
||||||
'choice_label' => fn (Scope $c) => $this->translatableStringHelper->localize($c->getName()),
|
'choice_label' => fn (Scope $c) => $this->translatableStringHelper->localize($c->getName()),
|
||||||
'choices' => $items,
|
'choices' => $reachableScopes,
|
||||||
]);
|
]);
|
||||||
$builder->setDataMapper(new ScopePickerDataMapper());
|
$builder->setDataMapper(new ScopePickerDataMapper());
|
||||||
} else {
|
} else {
|
||||||
$builder->add('scope', HiddenType::class, [
|
$builder->add('scope', HiddenType::class, [
|
||||||
'data' => $items[0]->getId(),
|
'data' => $reachableScopes[0]->getId(),
|
||||||
]);
|
]);
|
||||||
$builder->setDataMapper(new ScopePickerDataMapper($items[0]));
|
$builder->setDataMapper(new ScopePickerDataMapper($reachableScopes[0]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||||
{
|
{
|
||||||
$view->vars['fullWidth'] = true;
|
$view->vars['fullWidth'] = true;
|
||||||
|
// display of label is handled by the EntityType
|
||||||
|
$view->vars['label'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
{
|
{
|
||||||
$resolver
|
$resolver
|
||||||
// create `center` option
|
|
||||||
->setRequired('center')
|
|
||||||
->setAllowedTypes('center', [Center::class, 'array', 'null'])
|
|
||||||
// create ``role` option
|
|
||||||
->setRequired('role')
|
->setRequired('role')
|
||||||
->setAllowedTypes('role', ['string']);
|
->setAllowedTypes('role', ['string'])
|
||||||
|
->setDefined('subject')
|
||||||
|
->setAllowedTypes('subject', ['object'])
|
||||||
|
->setDefined('center')
|
||||||
|
->setAllowedTypes('center', [Center::class, 'array', 'null']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/Bundle/ChillMainBundle/Form/Type/UserLocaleType.php
Normal file
43
src/Bundle/ChillMainBundle/Form/Type/UserLocaleType.php
Normal 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\MainBundle\Form\Type;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Intl\Languages;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class UserLocaleType extends AbstractType
|
||||||
|
{
|
||||||
|
public function __construct(private readonly array $availableLanguages) {}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$choices = [];
|
||||||
|
foreach ($this->availableLanguages as $languageCode) {
|
||||||
|
$choices[Languages::getName($languageCode)] = $languageCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'choices' => $choices,
|
||||||
|
'placeholder' => 'user.locale.placeholder',
|
||||||
|
'required' => true,
|
||||||
|
'label' => 'user.locale.label',
|
||||||
|
'help' => 'user.locale.help',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParent(): string
|
||||||
|
{
|
||||||
|
return ChoiceType::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Form;
|
|||||||
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
|
||||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
||||||
|
use Chill\MainBundle\Form\Type\UserLocaleType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -26,6 +27,7 @@ class UpdateProfileType extends AbstractType
|
|||||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
|
->add('locale', UserLocaleType::class)
|
||||||
->add('notificationFlags', NotificationFlagsType::class)
|
->add('notificationFlags', NotificationFlagsType::class)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,16 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
|
|||||||
public function run(array $lastExecutionData): ?array
|
public function run(array $lastExecutionData): ?array
|
||||||
{
|
{
|
||||||
$now = $this->clock->now();
|
$now = $this->clock->now();
|
||||||
|
|
||||||
if (isset($lastExecutionData['last_execution'])) {
|
if (isset($lastExecutionData['last_execution'])) {
|
||||||
$lastExecution = \DateTimeImmutable::createFromFormat(
|
$lastExecution = \DateTimeImmutable::createFromFormat(
|
||||||
\DateTimeImmutable::ATOM,
|
\DateTimeImmutable::ATOM,
|
||||||
$lastExecutionData['last_execution']
|
$lastExecutionData['last_execution']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (false === $lastExecution) {
|
||||||
|
$lastExecution = $now->sub(new \DateInterval('P1D'));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$lastExecution = $now->sub(new \DateInterval('P1D'));
|
$lastExecution = $now->sub(new \DateInterval('P1D'));
|
||||||
}
|
}
|
||||||
@@ -96,7 +101,7 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
|
'last_execution' => $now->format(\DateTimeInterface::ATOM),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ use Symfony\Component\Mime\Email;
|
|||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
// use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
readonly class NotificationMailer
|
readonly class NotificationMailer
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -31,6 +33,7 @@ readonly class NotificationMailer
|
|||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private MessageBusInterface $messageBus,
|
private MessageBusInterface $messageBus,
|
||||||
private TranslatorInterface $translator,
|
private TranslatorInterface $translator,
|
||||||
|
// private LocaleSwitcher $localeSwitcher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
|
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
|
||||||
@@ -56,7 +59,7 @@ readonly class NotificationMailer
|
|||||||
$email
|
$email
|
||||||
->to($dest->getEmail())
|
->to($dest->getEmail())
|
||||||
->subject('Re: '.$comment->getNotification()->getTitle())
|
->subject('Re: '.$comment->getNotification()->getTitle())
|
||||||
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig')
|
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.md.twig')
|
||||||
->context([
|
->context([
|
||||||
'comment' => $comment,
|
'comment' => $comment,
|
||||||
'dest' => $dest,
|
'dest' => $dest,
|
||||||
@@ -137,13 +140,53 @@ readonly class NotificationMailer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||||
|
/*
|
||||||
|
$this->localeSwitcher->runWithLocale($addressee->getLocale(), function () use ($notification, $addressee) {
|
||||||
|
if ($notification->isSystem()) {
|
||||||
|
$email = new Email();
|
||||||
|
$email->text($notification->getMessage());
|
||||||
|
} else {
|
||||||
|
$email = new TemplatedEmail();
|
||||||
|
$email
|
||||||
|
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.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(),
|
||||||
|
'locale' => $addressee->getLocale(),
|
||||||
|
]);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Current implementation:
|
||||||
if ($notification->isSystem()) {
|
if ($notification->isSystem()) {
|
||||||
$email = new Email();
|
$email = new Email();
|
||||||
$email->text($notification->getMessage());
|
$email->text($notification->getMessage());
|
||||||
} else {
|
} else {
|
||||||
$email = new TemplatedEmail();
|
$email = new TemplatedEmail();
|
||||||
$email
|
$email
|
||||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
|
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
|
||||||
->context([
|
->context([
|
||||||
'notification' => $notification,
|
'notification' => $notification,
|
||||||
'dest' => $addressee,
|
'dest' => $addressee,
|
||||||
@@ -182,9 +225,43 @@ readonly class NotificationMailer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
|
||||||
|
/*
|
||||||
|
$this->localeSwitcher->runWithLocale($user->getLocale(), function () use ($user, $notifications) {
|
||||||
|
$email = new TemplatedEmail();
|
||||||
|
$email
|
||||||
|
->htmlTemplate('@ChillMain/Notification/email_daily_digest.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),
|
||||||
|
'locale' => $user->getLocale(),
|
||||||
|
]);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Current implementation:
|
||||||
$email = new TemplatedEmail();
|
$email = new TemplatedEmail();
|
||||||
$email
|
$email
|
||||||
->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
|
->htmlTemplate('@ChillMain/Notification/email_daily_digest.md.twig')
|
||||||
->context([
|
->context([
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'notifications' => $notifications,
|
'notifications' => $notifications,
|
||||||
@@ -222,7 +299,7 @@ readonly class NotificationMailer
|
|||||||
|
|
||||||
$email = new TemplatedEmail();
|
$email = new TemplatedEmail();
|
||||||
$email
|
$email
|
||||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
|
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.md.twig')
|
||||||
->context([
|
->context([
|
||||||
'notification' => $notification,
|
'notification' => $notification,
|
||||||
'dest' => $emailAddress,
|
'dest' => $emailAddress,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use Symfony\Component\Routing\RouterInterface;
|
|||||||
/**
|
/**
|
||||||
* Create paginator instances.
|
* Create paginator instances.
|
||||||
*/
|
*/
|
||||||
final readonly class PaginatorFactory implements PaginatorFactoryInterface
|
class PaginatorFactory implements PaginatorFactoryInterface
|
||||||
{
|
{
|
||||||
final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
|
final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
|
||||||
|
|
||||||
@@ -29,16 +29,16 @@ final readonly class PaginatorFactory implements PaginatorFactoryInterface
|
|||||||
/**
|
/**
|
||||||
* the request stack.
|
* the request stack.
|
||||||
*/
|
*/
|
||||||
private RequestStack $requestStack,
|
private readonly RequestStack $requestStack,
|
||||||
/**
|
/**
|
||||||
* the router and generator for url.
|
* the router and generator for url.
|
||||||
*/
|
*/
|
||||||
private RouterInterface $router,
|
private readonly RouterInterface $router,
|
||||||
/**
|
/**
|
||||||
* the default item per page. This may be overriden by
|
* the default item per page. This may be overriden by
|
||||||
* the request or inside the paginator.
|
* the request or inside the paginator.
|
||||||
*/
|
*/
|
||||||
private int $itemPerPage = 20,
|
private readonly int $itemPerPage = 20,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -57,9 +57,15 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function countBySubscriber(User $user): int
|
/**
|
||||||
|
* @param bool|null $isFinal true to get only the entityWorkflow which is finalized, false to get the workflows that are not finalized, and null to ignore
|
||||||
|
*
|
||||||
|
* @throws \Doctrine\ORM\NoResultException
|
||||||
|
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||||
|
*/
|
||||||
|
public function countBySubscriber(User $user, ?bool $isFinal = null): int
|
||||||
{
|
{
|
||||||
$qb = $this->buildQueryBySubscriber($user)->select('count(ew)');
|
$qb = $this->buildQueryBySubscriber($user, $isFinal)->select('count(ew)');
|
||||||
|
|
||||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
}
|
}
|
||||||
@@ -182,9 +188,14 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findBySubscriber(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
|
/**
|
||||||
|
* @param bool|null $isFinal true to get only the entityWorkflow which is finalized, false to get the workflows that are not finalized, and null to ignore
|
||||||
|
* @param mixed|null $limit
|
||||||
|
* @param mixed|null $offset
|
||||||
|
*/
|
||||||
|
public function findBySubscriber(User $user, ?bool $isFinal = null, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||||
{
|
{
|
||||||
$qb = $this->buildQueryBySubscriber($user)->select('ew');
|
$qb = $this->buildQueryBySubscriber($user, $isFinal)->select('ew');
|
||||||
|
|
||||||
foreach ($orderBy as $key => $sort) {
|
foreach ($orderBy as $key => $sort) {
|
||||||
$qb->addOrderBy('ew.'.$key, $sort);
|
$qb->addOrderBy('ew.'.$key, $sort);
|
||||||
@@ -312,7 +323,7 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildQueryBySubscriber(User $user): QueryBuilder
|
private function buildQueryBySubscriber(User $user, ?bool $isFinal): QueryBuilder
|
||||||
{
|
{
|
||||||
$qb = $this->repository->createQueryBuilder('ew');
|
$qb = $this->repository->createQueryBuilder('ew');
|
||||||
|
|
||||||
@@ -325,6 +336,14 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
|
|
||||||
$qb->setParameter('user', $user);
|
$qb->setParameter('user', $user);
|
||||||
|
|
||||||
|
if (null !== $isFinal) {
|
||||||
|
if ($isFinal) {
|
||||||
|
$qb->andWhere(sprintf('EXISTS (SELECT 1 FROM %s step WHERE step.isFinal = true AND ew = step.entityWorkflow)', EntityWorkflowStep::class));
|
||||||
|
} else {
|
||||||
|
$qb->andWhere(sprintf('NOT EXISTS (SELECT 1 FROM %s step WHERE step.isFinal = true AND ew = step.entityWorkflow)', EntityWorkflowStep::class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
import {
|
||||||
|
GenericDoc,
|
||||||
|
isGenericDocWithStoredObject,
|
||||||
|
} from "ChillDocStoreAssets/types/generic_doc";
|
||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||||
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
|
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
|
||||||
|
|
||||||
@@ -203,6 +206,25 @@ export interface WorkflowAttachment {
|
|||||||
genericDoc: null | GenericDoc;
|
genericDoc: null | GenericDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AttachmentWithDocAndStored = WorkflowAttachment & {
|
||||||
|
genericDoc: GenericDoc & { storedObject: StoredObject };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isAttachmentWithDocAndStored(
|
||||||
|
a: WorkflowAttachment,
|
||||||
|
): a is AttachmentWithDocAndStored {
|
||||||
|
return (
|
||||||
|
isWorkflowAttachmentWithGenericDoc(a) &&
|
||||||
|
isGenericDocWithStoredObject(a.genericDoc)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWorkflowAttachmentWithGenericDoc(
|
||||||
|
attachment: WorkflowAttachment,
|
||||||
|
): attachment is WorkflowAttachment & { genericDoc: GenericDoc } {
|
||||||
|
return attachment.genericDoc !== null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Workflow {
|
export interface Workflow {
|
||||||
name: string;
|
name: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/gener
|
|||||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
|
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
|
||||||
import { GenericDoc } from "ChillDocStoreAssets/types";
|
import { GenericDoc } from "ChillDocStoreAssets/types";
|
||||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
|
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
|
||||||
|
import { trans, WORKFLOW_ATTACHMENTS_ADD_AN_ATTACHMENT } from "translator";
|
||||||
|
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
workflowId: number;
|
workflowId: number;
|
||||||
@@ -83,7 +84,7 @@ const canEditAttachement = computed<boolean>(() => {
|
|||||||
<ul v-if="canEditAttachement" class="record_actions">
|
<ul v-if="canEditAttachement" class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
<button type="button" class="btn btn-create" @click="openModal">
|
<button type="button" class="btn btn-create" @click="openModal">
|
||||||
Ajouter une pièce jointe
|
{{ trans(WORKFLOW_ATTACHMENTS_ADD_AN_ATTACHMENT) }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
|
import {
|
||||||
|
AttachmentWithDocAndStored,
|
||||||
|
EntityWorkflow,
|
||||||
|
isAttachmentWithDocAndStored,
|
||||||
|
WorkflowAttachment,
|
||||||
|
} from "ChillMainAssets/types";
|
||||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
||||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { trans, WORKFLOW_ATTACHMENTS_NO_ATTACHMENT } from "translator";
|
||||||
|
|
||||||
interface AttachmentListProps {
|
interface AttachmentListProps {
|
||||||
attachments: WorkflowAttachment[];
|
attachments: WorkflowAttachment[];
|
||||||
@@ -14,35 +21,43 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<AttachmentListProps>();
|
const props = defineProps<AttachmentListProps>();
|
||||||
|
|
||||||
|
const notNullAttachments = computed<AttachmentWithDocAndStored[]>(() =>
|
||||||
|
props.attachments.filter(
|
||||||
|
(a: WorkflowAttachment): a is AttachmentWithDocAndStored =>
|
||||||
|
isAttachmentWithDocAndStored(a),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const canRemove = computed<boolean>((): boolean => {
|
||||||
|
if (null === props.workflow) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.workflow._permissions.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p
|
<p
|
||||||
v-if="props.attachments.length === 0"
|
v-if="notNullAttachments.length === 0"
|
||||||
class="chill-no-data-statement text-center"
|
class="chill-no-data-statement text-center"
|
||||||
>
|
>
|
||||||
Aucune pièce jointe
|
{{ trans(WORKFLOW_ATTACHMENTS_NO_ATTACHMENT) }}
|
||||||
</p>
|
</p>
|
||||||
<!-- TODO translate -->
|
<div v-else class="flex-table">
|
||||||
<div else class="flex-table">
|
<div v-for="a in notNullAttachments" :key="a.id" class="item-bloc">
|
||||||
<div v-for="a in props.attachments" :key="a.id" class="item-bloc">
|
|
||||||
<generic-doc-item-box
|
<generic-doc-item-box
|
||||||
v-if="a.genericDoc !== null"
|
|
||||||
:generic-doc="a.genericDoc"
|
:generic-doc="a.genericDoc"
|
||||||
></generic-doc-item-box>
|
></generic-doc-item-box>
|
||||||
<div class="item-row separator">
|
<div class="item-row separator">
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li v-if="a.genericDoc?.storedObject !== null">
|
<li>
|
||||||
<document-action-buttons-group
|
<document-action-buttons-group
|
||||||
:stored-object="a.genericDoc.storedObject"
|
:stored-object="a.genericDoc.storedObject"
|
||||||
></document-action-buttons-group>
|
></document-action-buttons-group>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li v-if="canRemove">
|
||||||
v-if="
|
|
||||||
!workflow?._permissions
|
|
||||||
.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-delete"
|
class="btn btn-delete"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
|
|
||||||
interface GenericDocItemBoxProps {
|
interface GenericDocItemBoxProps {
|
||||||
genericDoc: GenericDocForAccompanyingPeriod;
|
genericDoc: GenericDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<GenericDocItemBoxProps>();
|
const props = defineProps<GenericDocItemBoxProps>();
|
||||||
|
|||||||
@@ -1,40 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-grid gap-2 my-3">
|
<div class="d-grid gap-2 my-3">
|
||||||
<button
|
<button
|
||||||
class="btn btn-misc"
|
class="btn btn-outline-primary text-start d-flex align-items-center"
|
||||||
|
:class="{ active: subscriberFinal }"
|
||||||
type="button"
|
type="button"
|
||||||
v-if="!subscriberFinal"
|
@click="
|
||||||
@click="subscribeTo('subscribe', 'final')"
|
subscribeTo(
|
||||||
|
subscriberFinal ? 'unsubscribe' : 'subscribe',
|
||||||
|
'final',
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<i class="fa fa-check fa-fw"></i>
|
<i
|
||||||
{{ trans(WORKFLOW_SUBSCRIBE_FINAL) }}
|
class="fa fa-fw me-2"
|
||||||
|
:class="subscriberFinal ? 'fa-check-square-o' : 'fa-square-o'"
|
||||||
|
></i>
|
||||||
|
<span>{{ trans(WORKFLOW_SUBSCRIBE_FINAL) }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-misc"
|
class="btn btn-outline-primary text-start d-flex align-items-center"
|
||||||
|
:class="{ active: subscriberStep }"
|
||||||
type="button"
|
type="button"
|
||||||
v-if="subscriberFinal"
|
@click="
|
||||||
@click="subscribeTo('unsubscribe', 'final')"
|
subscribeTo(
|
||||||
|
subscriberStep ? 'unsubscribe' : 'subscribe',
|
||||||
|
'step',
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<i class="fa fa-times fa-fw"></i>
|
<i
|
||||||
{{ trans(WORKFLOW_UNSUBSCRIBE_FINAL) }}
|
class="fa fa-fw me-2"
|
||||||
</button>
|
:class="subscriberStep ? 'fa-check-square-o' : 'fa-square-o'"
|
||||||
<button
|
></i>
|
||||||
class="btn btn-misc"
|
<span>{{ trans(WORKFLOW_SUBSCRIBE_ALL_STEPS) }}</span>
|
||||||
type="button"
|
|
||||||
v-if="!subscriberStep"
|
|
||||||
@click="subscribeTo('subscribe', 'step')"
|
|
||||||
>
|
|
||||||
<i class="fa fa-check fa-fw"></i>
|
|
||||||
{{ trans(WORKFLOW_SUBSCRIBE_ALL_STEPS) }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-misc"
|
|
||||||
type="button"
|
|
||||||
v-if="subscriberStep"
|
|
||||||
@click="subscribeTo('unsubscribe', 'step')"
|
|
||||||
>
|
|
||||||
<i class="fa fa-times fa-fw"></i>
|
|
||||||
{{ trans(WORKFLOW_UNSUBSCRIBE_ALL_STEPS) }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -45,9 +43,7 @@ import { defineProps, defineEmits } from "vue";
|
|||||||
import {
|
import {
|
||||||
trans,
|
trans,
|
||||||
WORKFLOW_SUBSCRIBE_FINAL,
|
WORKFLOW_SUBSCRIBE_FINAL,
|
||||||
WORKFLOW_UNSUBSCRIBE_FINAL,
|
|
||||||
WORKFLOW_SUBSCRIBE_ALL_STEPS,
|
WORKFLOW_SUBSCRIBE_ALL_STEPS,
|
||||||
WORKFLOW_UNSUBSCRIBE_ALL_STEPS,
|
|
||||||
} from "translator";
|
} from "translator";
|
||||||
|
|
||||||
// props
|
// props
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{% if chill_main_config.top_banner is defined and chill_main_config.top_banner.text is defined %}
|
||||||
|
{% set banner_text = '' %}
|
||||||
|
{% set current_locale = app.request.locale %}
|
||||||
|
|
||||||
|
{% if chill_main_config.top_banner.text[current_locale] is defined %}
|
||||||
|
{% set banner_text = chill_main_config.top_banner.text[current_locale] %}
|
||||||
|
{% else %}
|
||||||
|
{% set banner_text = chill_main_config.top_banner.text|first %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if banner_text %}
|
||||||
|
<div class="top-banner w-100 text-center py-2"
|
||||||
|
style="{% if chill_main_config.top_banner.color is defined %}color: {{ chill_main_config.top_banner.color }};{% endif %}{% if chill_main_config.top_banner.background_color is defined %}background-color: {{ chill_main_config.top_banner.background_color }};{% endif %}">
|
||||||
|
{{ banner_text }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
@@ -1 +1 @@
|
|||||||
<img class="logo" src="{{ asset('build/images/logo-chill-outil-accompagnement_white.png') }}">
|
<img class="logo" alt="{{ 'login_page.logo_alt'|trans }}" src="{{ asset('build/images/logo-chill-outil-accompagnement_white.png') }}">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#}
|
#}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="{{ app.request.locale }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>
|
<title>
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
|
|
||||||
<form method="POST" action="{{ path('login_check') }}">
|
<form method="POST" action="{{ path('login_check') }}">
|
||||||
<label for="_username">{{ 'Username'|trans }}</label>
|
<label for="_username">{{ 'Username'|trans }}</label>
|
||||||
<input type="text" name="_username" value="{{ last_username }}" />
|
<input type="text" name="_username" value="{{ last_username }}" id="_username" />
|
||||||
<br/>
|
<br/>
|
||||||
<label for="_password">{{ 'Password'|trans }}</label>
|
<label for="_password">{{ 'Password'|trans }}</label>
|
||||||
<input type="password" name="_password" />
|
<input type="password" name="_password" id="_password" />
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" />
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}" />
|
||||||
<br/>
|
<br/>
|
||||||
<button type="submit" name="login">{{ 'Login'|trans }}</button>
|
<button type="submit" name="login">{{ 'Login'|trans }}</button>
|
||||||
|
|||||||
@@ -21,8 +21,6 @@
|
|||||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||||
|
|
||||||
{{ form_row(form.addressesEmails) }}
|
|
||||||
|
|
||||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||||
|
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user