mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-11-10 22:28:23 +00:00
Compare commits
15 Commits
master
...
#361-impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
caaed3e759
|
|||
|
6380fdd9a4
|
|||
|
fcd5080e6f
|
|||
|
086d418aa3
|
|||
|
4e61821e5b
|
|||
|
e3aeab315f
|
|||
|
8ec1063ef8
|
|||
|
aad9c984b1
|
|||
|
34b3e290e1
|
|||
|
0987b575ab
|
|||
|
d960578c5f
|
|||
|
176048bce6
|
|||
|
be210a6dd6
|
|||
|
4323773595
|
|||
|
6d432ca2cb
|
@@ -1,14 +0,0 @@
|
|||||||
## 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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
## v4.6.1 - 2025-10-27
|
|
||||||
### Fixed
|
|
||||||
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
## 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
|
|
||||||
@@ -240,6 +240,9 @@ 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
|
||||||
|
|
||||||
@@ -247,9 +250,6 @@ 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:
|
||||||
|
|||||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -6,47 +6,6 @@ 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.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-symfony-v5@dev",
|
"champs-libres/wopi-bundle": "dev-master@dev",
|
||||||
"champs-libres/wopi-lib": "dev-master@dev",
|
"champs-libres/wopi-lib": "dev-master@dev",
|
||||||
"doctrine/data-fixtures": "^1.8",
|
"doctrine/data-fixtures": "^1.8",
|
||||||
"doctrine/doctrine-bundle": "^2.1",
|
"doctrine/doctrine-bundle": "^2.1",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
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],
|
||||||
@@ -36,5 +37,4 @@ 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,13 +1,6 @@
|
|||||||
chill_main:
|
chill_main:
|
||||||
available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
|
available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
|
||||||
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)%'
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
chill_aside_activity:
|
|
||||||
show_concerned_persons_count: hidden
|
|
||||||
@@ -17,3 +17,9 @@ when@dev:
|
|||||||
defaults:
|
defaults:
|
||||||
template: '@ChillMain/Dev/dev.assets.test2.html.twig'
|
template: '@ChillMain/Dev/dev.assets.test2.html.twig'
|
||||||
|
|
||||||
|
|
||||||
|
sass_address_picker:
|
||||||
|
path: /_dev/address-picker
|
||||||
|
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
|
||||||
|
defaults:
|
||||||
|
template: '@ChillMain/Dev/dev.address-picker.html.twig'
|
||||||
|
|||||||
@@ -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
|
||||||
- Territoire territoire
|
- Center center
|
||||||
- Service service
|
- Cercle cercle
|
||||||
- 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 territoires et les usagers;
|
- la liaison entre les centres 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 territoires (déprécié)
|
2,chill_3party,party_center,Association entre les tiers et les centres (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,"Territoires (territoires, agences, etc.)"
|
56,public,centers,"Centres (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 territoires d'un usagers
|
113,public,chill_person_person_center_history,Historique des centres 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 territoires
|
144,public,regroupment,Regroupement de centres
|
||||||
145,public,regroupment_center,
|
145,public,regroupment_center,
|
||||||
146,public,role_scopes,
|
146,public,role_scopes,
|
||||||
147,public,scopes,Services
|
147,public,scopes,Services
|
||||||
|
|||||||
|
Can't render this file because it has a wrong number of fields in line 28.
|
@@ -45,6 +45,7 @@
|
|||||||
"webpack-cli": "^5.0.1"
|
"webpack-cli": "^5.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fragaria/address-formatter": "^6.6.1",
|
||||||
"@fullcalendar/core": "^6.1.4",
|
"@fullcalendar/core": "^6.1.4",
|
||||||
"@fullcalendar/daygrid": "^6.1.4",
|
"@fullcalendar/daygrid": "^6.1.4",
|
||||||
"@fullcalendar/interaction": "^6.1.4",
|
"@fullcalendar/interaction": "^6.1.4",
|
||||||
@@ -66,10 +67,12 @@
|
|||||||
"mime": "^4.0.0",
|
"mime": "^4.0.0",
|
||||||
"pdfjs-dist": "^4.3.136",
|
"pdfjs-dist": "^4.3.136",
|
||||||
"vis-network": "^9.1.0",
|
"vis-network": "^9.1.0",
|
||||||
"vue": "^3.5.6",
|
"vue": "^3.5.x",
|
||||||
"vue-i18n": "^9.1.6",
|
"vue-i18n": "^9.1.6",
|
||||||
"vue-multiselect": "3.0.0-alpha.2",
|
"vue-multiselect": "3.0.0-alpha.2",
|
||||||
"vue-toast-notification": "^3.1.2",
|
"vue-toast-notification": "^3.1.2",
|
||||||
|
"vue-tsc": "^3.1.0",
|
||||||
|
"vue-use-leaflet": "^0.1.7",
|
||||||
"vuex": "^4.0.0"
|
"vuex": "^4.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
@@ -66,9 +66,6 @@ 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')
|
||||||
@@ -90,8 +87,6 @@ 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';
|
||||||
@@ -181,9 +176,6 @@ class ListActivityHelper
|
|||||||
'usersNames',
|
'usersNames',
|
||||||
'thirdPartiesIds',
|
'thirdPartiesIds',
|
||||||
'thirdPartiesNames',
|
'thirdPartiesNames',
|
||||||
'commentText',
|
|
||||||
'commentDate',
|
|
||||||
'commentUser',
|
|
||||||
'createdBy',
|
'createdBy',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedBy',
|
'updatedBy',
|
||||||
|
|||||||
@@ -90,9 +90,7 @@ 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,8 +42,6 @@ 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')
|
||||||
@@ -61,6 +59,7 @@ 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');
|
||||||
@@ -125,38 +124,12 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
|||||||
|
|
||||||
public function normalizeFormData(array $formData): array
|
public function normalizeFormData(array $formData): array
|
||||||
{
|
{
|
||||||
$normalized = [
|
return ['date_from_rolling' => $formData['date_from_rolling']->normalize(), 'date_to_rolling' => $formData['date_to_rolling']->normalize()];
|
||||||
'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
|
||||||
{
|
{
|
||||||
$denormalized = [
|
return ['date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']), 'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling'])];
|
||||||
'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
|
||||||
@@ -170,12 +143,10 @@ 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 [
|
||||||
[] === $reasons ?
|
[] === $data['reasons'] ?
|
||||||
'export.filter.activity.describe_action_with_no_subject'
|
'export.filter.person_between_dates.describe_action_with_no_subject'
|
||||||
: 'export.filter.activity.describe_action_with_subject',
|
: 'export.filter.person_between_dates.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']),
|
||||||
@@ -183,7 +154,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()).'"',
|
||||||
$reasons
|
$data['reasons']
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -197,7 +168,6 @@ 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')
|
||||||
|
|||||||
@@ -136,14 +136,8 @@ export default {
|
|||||||
issueIsLoading: false,
|
issueIsLoading: false,
|
||||||
actionIsLoading: false,
|
actionIsLoading: false,
|
||||||
actionAreLoaded: false,
|
actionAreLoaded: false,
|
||||||
socialIssuesClassList: {
|
socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`,
|
||||||
"col-form-label": true,
|
socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`,
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
socialActionsClassList: {
|
|
||||||
"col-form-label": true,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -164,21 +158,6 @@ 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,23 +43,11 @@ 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;
|
||||||
text-align: left;
|
max-width: 100%; /* Adjust as needed */
|
||||||
line-height: 1.2em;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
&::before {
|
white-space: nowrap;
|
||||||
position: absolute;
|
|
||||||
left: 11px;
|
|
||||||
top: 0;
|
|
||||||
margin: 0 0.3em 0 -0.75em;
|
|
||||||
}
|
|
||||||
position: relative;
|
|
||||||
padding-left: 1.5em;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,22 +43,7 @@ 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 service
|
circle_name: nom du cercle
|
||||||
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: Service
|
Scope: Cercle
|
||||||
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,15 +398,13 @@ 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: Services du parcours
|
course circles: Cercles 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,7 +25,6 @@ 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');
|
||||||
@@ -39,24 +38,6 @@ 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,12 +141,6 @@ 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,10 +62,6 @@ 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;
|
||||||
@@ -190,16 +186,4 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
<?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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
<?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,7 +21,6 @@ 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;
|
||||||
@@ -30,13 +29,11 @@ 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)
|
||||||
@@ -79,16 +76,6 @@ 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,11 +42,6 @@
|
|||||||
{%- 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,11 +38,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<?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'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<?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,10 +20,6 @@ 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
|
||||||
@@ -74,7 +70,3 @@ 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' }
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<?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,7 +27,6 @@ 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:
|
||||||
@@ -178,7 +177,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: Territoire principal de l'utilisateur
|
main_center: Centre 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
|
||||||
@@ -191,7 +190,6 @@ 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é
|
||||||
@@ -212,8 +210,6 @@ 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
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ 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;
|
||||||
@@ -31,7 +30,6 @@ 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;
|
||||||
@@ -62,7 +60,6 @@ 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,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,55 +111,6 @@ 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.
|
||||||
*/
|
*/
|
||||||
@@ -318,7 +266,7 @@ class CalendarController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->getUser() instanceof User) {
|
if (!$this->getUser() instanceof User) {
|
||||||
throw new UnauthorizedHttpException('you are not a user');
|
throw new UnauthorizedHttpException('you are not an user');
|
||||||
}
|
}
|
||||||
|
|
||||||
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<?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_OTHER],
|
['name' => CancelReason::CANCELEDBY_DONOTCOUNT],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($arr as $a) {
|
foreach ($arr as $a) {
|
||||||
|
|||||||
@@ -269,11 +269,6 @@ 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_OTHER = 'CANCELEDBY_OTHER';
|
final public const CANCELEDBY_DONOTCOUNT = 'CANCELEDBY_DONOTCOUNT';
|
||||||
|
|
||||||
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, options: ['default' => true])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
|
||||||
private bool $active = true;
|
private ?bool $active = null;
|
||||||
|
|
||||||
#[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\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
@@ -28,14 +28,7 @@ class CancelReasonType extends AbstractType
|
|||||||
->add('active', CheckboxType::class, [
|
->add('active', CheckboxType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
->add('canceledBy', ChoiceType::class, [
|
->add('canceledBy', TextType::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)
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
<?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,13 +25,6 @@ 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,7 +21,6 @@ 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;
|
||||||
@@ -32,17 +31,6 @@ 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) {
|
||||||
@@ -50,7 +38,7 @@ class CalendarEntityListener
|
|||||||
new CalendarMessage(
|
new CalendarMessage(
|
||||||
$calendar,
|
$calendar,
|
||||||
CalendarMessage::CALENDAR_PERSIST,
|
CalendarMessage::CALENDAR_PERSIST,
|
||||||
$this->getAuthenticatedUser()
|
$this->security->getUser()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -62,7 +50,7 @@ class CalendarEntityListener
|
|||||||
$this->messageBus->dispatch(
|
$this->messageBus->dispatch(
|
||||||
new CalendarRemovedMessage(
|
new CalendarRemovedMessage(
|
||||||
$calendar,
|
$calendar,
|
||||||
$this->getAuthenticatedUser()
|
$this->security->getUser()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,19 +58,12 @@ class CalendarEntityListener
|
|||||||
|
|
||||||
public function postUpdate(Calendar $calendar, PostUpdateEventArgs $args): void
|
public function postUpdate(Calendar $calendar, PostUpdateEventArgs $args): void
|
||||||
{
|
{
|
||||||
if ($calendar->getStatus() === $calendar::STATUS_CANCELED) {
|
if (!$calendar->preventEnqueueChanges) {
|
||||||
$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->getAuthenticatedUser()
|
$this->security->getUser()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,8 +70,6 @@ class CalendarRemovedMessage
|
|||||||
|
|
||||||
public function getRemoteId(): string
|
public function getRemoteId(): string
|
||||||
{
|
{
|
||||||
dump($this->remoteId);
|
|
||||||
|
|
||||||
return $this->remoteId;
|
return $this->remoteId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ 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([
|
||||||
|
|||||||
@@ -41,7 +41,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): array
|
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
|
||||||
{
|
{
|
||||||
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
|
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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']
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
{# list used in context of person, accompanyingPeriod or user #}
|
{# list used in context of person or accompanyingPeriod #}
|
||||||
|
|
||||||
<div class="item-bloc">
|
{% if calendarItems|length > 0 %}
|
||||||
<div class="item-row main">
|
<div class="flex-table list-records context-accompanyingCourse">
|
||||||
<div class="item-col">
|
|
||||||
<div class="wrap-header">
|
{% for calendar in calendarItems %}
|
||||||
<div class="wl-row">
|
|
||||||
{% if calendar.status == 'canceled' %}
|
<div class="item-bloc">
|
||||||
<div class="badge rounded-pill bg-danger">
|
<div class="item-row main">
|
||||||
<span>{{ 'chill_calendar.canceled'|trans }}: </span>
|
<div class="item-col">
|
||||||
<span>{{ calendar.cancelReason.name|localize_translatable_string }}</span>
|
<div class="wrap-header">
|
||||||
</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">
|
||||||
@@ -25,9 +19,6 @@
|
|||||||
</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') }}
|
||||||
@@ -35,46 +26,44 @@
|
|||||||
{{ 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 %}
|
||||||
{% if calendar.status == 'canceled' %}
|
</p>
|
||||||
</del>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="duration short-message">
|
<div class="duration short-message">
|
||||||
<i class="fa fa-fw fa-hourglass-end"></i>
|
<i class="fa fa-fw fa-hourglass-end"></i>
|
||||||
{{ calendar.duration|date('%H:%I') }}
|
{{ calendar.duration|date('%H:%I') }}
|
||||||
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
|
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
|
||||||
<!-- no sms will be sent -->
|
<!-- no sms will be send -->
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if calendar.smsStatus == 'sms_sent' %}
|
{% if calendar.smsStatus == 'sms_sent' %}
|
||||||
<span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
|
<span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
|
||||||
<i class="fa fa-check "></i>
|
<i class="fa fa-check "></i>
|
||||||
<i class="fa fa-envelope "></i>
|
<i class="fa fa-envelope "></i>
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span title="{{ 'Will send SMS'|trans }}" class="badge bg-info">
|
<span title="{{ 'Will send SMS'|trans }}" class="badge bg-info">
|
||||||
<i class="fa fa-envelope "></i>
|
<i class="fa fa-envelope "></i>
|
||||||
<i class="fa fa-hourglass-end "></i>
|
<i class="fa fa-hourglass-end "></i>
|
||||||
</span>
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 %}
|
{% endif %}
|
||||||
{% endif %}
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item-col">
|
{% if calendar.comment.comment is not empty
|
||||||
<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 %}
|
||||||
@@ -87,133 +76,131 @@
|
|||||||
} %}
|
} %}
|
||||||
</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>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="item-row separator">
|
|
||||||
<ul class="record_actions">
|
|
||||||
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', calendar) and calendar.status is not constant('STATUS_CANCELED', 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 %}
|
{% 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 %}
|
|
||||||
|
|
||||||
{% if calendar.isInvited(app.user) and not calendar.isCanceled %}
|
{% 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>
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="item-row separator">
|
||||||
|
<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') }}"
|
||||||
@@ -226,18 +213,12 @@
|
|||||||
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 } ) }}"
|
||||||
@@ -246,8 +227,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if calendarItems|length < paginator.getTotalItems %}
|
||||||
|
{{ chill_pagination(paginator) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{% 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,18 +34,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if calendarItems|length > 0 %}
|
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}
|
||||||
<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,17 +33,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if calendarItems|length > 0 %}
|
{{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}
|
||||||
<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>{{ 'Canceled by'|trans }}</th>
|
<th>{{ 'canceledBy'|trans }}</th>
|
||||||
<th>{{ 'active'|trans }}</th>
|
<th>{{ 'active'|trans }}</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
{% 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,7 +19,6 @@ 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;
|
||||||
@@ -58,7 +57,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() && (null === $calendar->getCancelReason() || CancelReason::CANCELEDBY_PERSON !== $calendar->getCancelReason()->getCanceledBy())) {
|
} elseif (Calendar::SMS_CANCEL_PENDING === $calendar->getSmsStatus()) {
|
||||||
$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]),
|
||||||
|
|||||||
@@ -1,292 +0,0 @@
|
|||||||
<?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,7 +31,8 @@ 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é
|
||||||
|
|
||||||
Canceled by: Annulé par
|
canceledBy: supprimé par
|
||||||
|
Canceled by: supprimé par
|
||||||
Calendar configuration: Gestion des rendez-vous
|
Calendar configuration: Gestion des rendez-vous
|
||||||
|
|
||||||
crud:
|
crud:
|
||||||
@@ -43,14 +44,6 @@ 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.
|
||||||
@@ -93,9 +86,6 @@ 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,9 +20,4 @@ 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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
|
|||||||
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
$this->serializer->serialize(
|
$this->serializer->serialize(
|
||||||
new Collection(array_values($items->toArray()), $paginator),
|
new Collection($items, $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,14 +23,10 @@ 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;
|
||||||
|
|||||||
@@ -36,18 +36,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -56,33 +44,28 @@ 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 "ChillDocStoreAssets/types";
|
} from "./../../../types";
|
||||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||||
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
|
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/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,10 +40,6 @@ 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)
|
||||||
@@ -57,7 +53,6 @@ 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']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
<?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 territory should the event be associated ?',
|
'label' => 'To which centre 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 territory should the event be associated ?: À quel territoire doit être associé l'événement ?
|
To which centre should the event be associated ?: À quel centre 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 territoire
|
center: Par centre
|
||||||
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: Territoire
|
event_center: Centre
|
||||||
event_moderator: Responsable
|
event_moderator: Responsable
|
||||||
event_participants_count: Nombre de participants
|
event_participants_count: Nombre de participants
|
||||||
event_location: Localisation
|
event_location: Localisation
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ 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,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -673,7 +674,7 @@ class CRUDController extends AbstractController
|
|||||||
|
|
||||||
protected function getManagerRegistry(): ManagerRegistry
|
protected function getManagerRegistry(): ManagerRegistry
|
||||||
{
|
{
|
||||||
return $this->container->get('doctrine');
|
return $this->container->get(ManagerRegistry::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ class ChillImportUsersCommand extends Command
|
|||||||
|
|
||||||
protected function loadUsers()
|
protected function loadUsers()
|
||||||
{
|
{
|
||||||
$reader = Reader::from($this->tempInput->getArgument('csvfile'));
|
$reader = Reader::createFromPath($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::from($this->tempInput->getOption('grouping-centers'));
|
$reader = Reader::createFromPath($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::from($this->tempInput
|
$this->output = $output = Writer::createFromPath($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::from($this->input->getArgument('csvfile'));
|
$reader = Reader::createFromPath($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'),
|
||||||
|
|||||||
@@ -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\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Repository\AddressReferenceRepositoryInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
final readonly class AddressReferenceAggregatedApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Security $security,
|
||||||
|
private AddressReferenceRepositoryInterface $addressReferenceRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route(path: '/api/1.0/main/address-reference/aggregated/search')]
|
||||||
|
public function search(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted('IS_AUTHENTICATED')) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$request->query->has('q')) {
|
||||||
|
throw new BadRequestHttpException('Parameter "q" is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$q = trim($request->query->get('q'));
|
||||||
|
|
||||||
|
if ('' === $q) {
|
||||||
|
throw new BadRequestHttpException('Parameter "q" is required and cannot be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->addressReferenceRepository->findAggregatedBySearchString($q);
|
||||||
|
|
||||||
|
return new JsonResponse(iterator_to_array($result));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Repository\PostalCodeForAddressReferenceRepositoryInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
final readonly class PostalCodeForAddressReferenceApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PostalCodeForAddressReferenceRepositoryInterface $postalCodeForAddressReferenceRepository,
|
||||||
|
private Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/1.0/main/address-reference/postal-code/search')]
|
||||||
|
public function findPostalCodeBySearch(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!$this->security->isGranted('IS_AUTHENTICATED')) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = $request->query->get('q');
|
||||||
|
|
||||||
|
if (null === $search || '' === trim($search)) {
|
||||||
|
throw new BadRequestHttpException('No search query provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
$postalCodes = iterator_to_array($this->postalCodeForAddressReferenceRepository->findPostalCode($search));
|
||||||
|
|
||||||
|
return new JsonResponse($postalCodes, json: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ final readonly class UserExportController
|
|||||||
|
|
||||||
$users = $this->userRepository->findAllAsArray($request->getLocale());
|
$users = $this->userRepository->findAllAsArray($request->getLocale());
|
||||||
|
|
||||||
$csv = Writer::from('php://temp', 'r+');
|
$csv = Writer::createFromPath('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::from('php://temp', 'r+');
|
$csv = Writer::createFromPath('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,12 +264,11 @@ class WorkflowController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||||
|
|
||||||
$total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser(), false);
|
$total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser());
|
||||||
$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,11 +205,6 @@ 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');
|
||||||
@@ -255,7 +250,6 @@ 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,20 +168,6 @@ 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()
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class EntityWorkflowStep
|
|||||||
/**
|
/**
|
||||||
* @var Collection<int, EntityWorkflowStepHold>
|
* @var Collection<int, EntityWorkflowStepHold>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class, cascade: ['remove'])]
|
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
|
||||||
private Collection $holdsOnStep;
|
private Collection $holdsOnStep;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -53,16 +53,11 @@ 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'));
|
||||||
}
|
}
|
||||||
@@ -101,7 +96,7 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'last_execution' => $now->format(\DateTimeInterface::ATOM),
|
'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use Symfony\Component\Routing\RouterInterface;
|
|||||||
/**
|
/**
|
||||||
* Create paginator instances.
|
* Create paginator instances.
|
||||||
*/
|
*/
|
||||||
class PaginatorFactory implements PaginatorFactoryInterface
|
final readonly class PaginatorFactory implements PaginatorFactoryInterface
|
||||||
{
|
{
|
||||||
final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
|
final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
|
||||||
|
|
||||||
@@ -29,16 +29,16 @@ class PaginatorFactory implements PaginatorFactoryInterface
|
|||||||
/**
|
/**
|
||||||
* the request stack.
|
* the request stack.
|
||||||
*/
|
*/
|
||||||
private readonly RequestStack $requestStack,
|
private RequestStack $requestStack,
|
||||||
/**
|
/**
|
||||||
* the router and generator for url.
|
* the router and generator for url.
|
||||||
*/
|
*/
|
||||||
private readonly RouterInterface $router,
|
private 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 readonly int $itemPerPage = 20,
|
private int $itemPerPage = 20,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ namespace Chill\MainBundle\Repository;
|
|||||||
use Chill\MainBundle\Entity\AddressReference;
|
use Chill\MainBundle\Entity\AddressReference;
|
||||||
use Chill\MainBundle\Entity\PostalCode;
|
use Chill\MainBundle\Entity\PostalCode;
|
||||||
use Chill\MainBundle\Search\SearchApiQuery;
|
use Chill\MainBundle\Search\SearchApiQuery;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\NativeQuery;
|
||||||
use Doctrine\ORM\Query\ResultSetMapping;
|
use Doctrine\ORM\Query\ResultSetMapping;
|
||||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
|
||||||
|
|
||||||
final readonly class AddressReferenceRepository implements ObjectRepository
|
final readonly class AddressReferenceRepository implements AddressReferenceRepositoryInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $entityManager;
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
@@ -65,6 +66,121 @@ final readonly class AddressReferenceRepository implements ObjectRepository
|
|||||||
return $this->repository->findAll();
|
return $this->repository->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
|
||||||
|
{
|
||||||
|
$terms = $this->buildTermsFromSearchString($search);
|
||||||
|
if ([] === $terms) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = $this->entityManager->getConnection();
|
||||||
|
$qb = $connection->createQueryBuilder();
|
||||||
|
|
||||||
|
$qb->select('row_number() OVER () AS row_number', 'var.street AS street', 'cmpc.id AS postcode_id', 'cmpc.code AS code', 'cmpc.label AS label', 'jsonb_object_agg(var.address_id, var.streetnumber ORDER BY var.row_number) AS positions')
|
||||||
|
->from('view_chill_main_address_reference', 'var')
|
||||||
|
->innerJoin('var', 'chill_main_postal_code', 'cmpc', 'cmpc.id = var.postcode_id')
|
||||||
|
->groupBy('cmpc.id', 'var.street')
|
||||||
|
->setFirstResult($firstResult)
|
||||||
|
->setMaxResults($maxResults);
|
||||||
|
|
||||||
|
$paramId = 0;
|
||||||
|
|
||||||
|
foreach ($terms as $term) {
|
||||||
|
$qb->andWhere('var.address like UNACCENT(LOWER(?))');
|
||||||
|
$qb->setParameter(++$paramId, "%{$term}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $postalCode) {
|
||||||
|
$qb->andWhere('var.postcode_id = ?');
|
||||||
|
$qb->setParameter(++$paramId, $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $qb->executeQuery();
|
||||||
|
|
||||||
|
foreach ($result->iterateAssociative() as $row) {
|
||||||
|
yield [...$row, 'positions' => json_decode($row['positions'], true, 512, JSON_THROW_ON_ERROR)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<AddressReference>
|
||||||
|
*/
|
||||||
|
public function findBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
|
||||||
|
{
|
||||||
|
$terms = $this->buildTermsFromSearchString($search);
|
||||||
|
if ([] === $terms) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
||||||
|
$rsm->addRootEntityFromClassMetadata(AddressReference::class, 'ar');
|
||||||
|
$baseSql = 'SELECT '.$rsm->generateSelectClause(['ar' => 'ar']).' FROM chill_main_address_reference ar JOIN
|
||||||
|
view_chill_main_address_reference var ON var.address_id = ar.id';
|
||||||
|
$nql = $this->buildQueryBySearchString($rsm, $baseSql, $terms, $postalCode);
|
||||||
|
|
||||||
|
$orderBy = [];
|
||||||
|
$pertinence = [];
|
||||||
|
foreach ($terms as $k => $term) {
|
||||||
|
$pertinence[] =
|
||||||
|
"(EXISTS (SELECT 1 FROM unnest(string_to_array(address, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(:order{$k})))))::int";
|
||||||
|
$pertinence[] = "(address LIKE UNACCENT(LOWER(:order{$k})))::int";
|
||||||
|
$nql->setParameter('order'.$k, $term);
|
||||||
|
}
|
||||||
|
$orderBy[] = implode(' + ', $pertinence).' ASC';
|
||||||
|
$orderBy[] = implode('row_number ASC', $orderBy);
|
||||||
|
|
||||||
|
$nql->setSQL($nql->getSQL().' ORDER BY '.implode(', ', $orderBy));
|
||||||
|
|
||||||
|
return $nql->toIterable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countBySearchString(string $search, PostalCode|int|null $postalCode = null): int
|
||||||
|
{
|
||||||
|
$terms = $this->buildTermsFromSearchString($search);
|
||||||
|
if ([] === $terms) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
||||||
|
$rsm->addScalarResult('c', 'c', Types::INTEGER);
|
||||||
|
$nql = $this->buildQueryBySearchString($rsm, 'SELECT COUNT(var.*) AS c FROM view_chill_main_address_reference var', $terms, $postalCode);
|
||||||
|
|
||||||
|
return $nql->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildTermsFromSearchString(string $search): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
array_map(
|
||||||
|
static fn (string $term) => trim($term),
|
||||||
|
explode(' ', $search)
|
||||||
|
),
|
||||||
|
static fn (string $term) => '' !== $term
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildQueryBySearchString(ResultSetMapping $rsm, string $select, array $terms, PostalCode|int|null $postalCode = null): NativeQuery
|
||||||
|
{
|
||||||
|
$nql = $this->entityManager->createNativeQuery('', $rsm);
|
||||||
|
|
||||||
|
$sql = $select.' WHERE ';
|
||||||
|
|
||||||
|
$wheres = [];
|
||||||
|
foreach ($terms as $k => $term) {
|
||||||
|
$wheres[] = "var.address like :w{$k}";
|
||||||
|
$nql->setParameter("w{$k}", '%'.$term.'%', Types::STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $postalCode) {
|
||||||
|
$wheres[] = 'var.postcode_id = :postalCode';
|
||||||
|
$nql->setParameter('postalCode', $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$nql->setSQL($sql.implode(' AND ', $wheres));
|
||||||
|
|
||||||
|
return $nql;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed|null $limit
|
* @param mixed|null $limit
|
||||||
* @param mixed|null $offset
|
* @param mixed|null $offset
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?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\Repository;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\PostalCode;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
interface AddressReferenceRepositoryInterface extends ObjectRepository
|
||||||
|
{
|
||||||
|
public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?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\Repository;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
|
||||||
|
final readonly class PostalCodeForAddressReferenceRepository implements PostalCodeForAddressReferenceRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(private Connection $connection) {}
|
||||||
|
|
||||||
|
public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable
|
||||||
|
{
|
||||||
|
$terms = $this->buildTermsFromSearchString($search);
|
||||||
|
if ([] === $terms) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->connection->createQueryBuilder();
|
||||||
|
|
||||||
|
$qb->from('chill_main_postal_code', 'cmpc')
|
||||||
|
->join('cmpc', 'view_chill_main_address_reference', 'vcmar', 'vcmar.postcode_id = cmpc.id')
|
||||||
|
->join('vcmar', 'country', 'country', condition: 'cmpc.country_id = country.id')
|
||||||
|
->setFirstResult($firstResult)
|
||||||
|
->setMaxResults($maxResults)
|
||||||
|
;
|
||||||
|
|
||||||
|
$qb->select(
|
||||||
|
'DISTINCT ON (cmpc.code, cmpc.label) cmpc.id AS postcode_id',
|
||||||
|
'cmpc.code AS code',
|
||||||
|
'cmpc.label AS label',
|
||||||
|
'country.id AS country_id',
|
||||||
|
'country.countrycode AS country_code',
|
||||||
|
'country.name AS country_name'
|
||||||
|
);
|
||||||
|
|
||||||
|
$paramId = 0;
|
||||||
|
|
||||||
|
foreach ($terms as $term) {
|
||||||
|
$qb->andWhere('vcmar.address like ?');
|
||||||
|
$qb->setParameter(++$paramId, "%{$term}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $qb->executeQuery();
|
||||||
|
|
||||||
|
foreach ($result->iterateAssociative() as $row) {
|
||||||
|
yield [...$row, 'country_name' => json_decode($row['country_name'], true, 512, JSON_THROW_ON_ERROR)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildTermsFromSearchString(string $search): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
array_map(
|
||||||
|
static fn (string $term) => trim($term),
|
||||||
|
explode(' ', $search)
|
||||||
|
),
|
||||||
|
static fn (string $term) => '' !== $term
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?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\Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for postal code using optimized materialized view.
|
||||||
|
*/
|
||||||
|
interface PostalCodeForAddressReferenceRepositoryInterface
|
||||||
|
{
|
||||||
|
public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable;
|
||||||
|
}
|
||||||
@@ -57,15 +57,9 @@ 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, $isFinal)->select('count(ew)');
|
$qb = $this->buildQueryBySubscriber($user)->select('count(ew)');
|
||||||
|
|
||||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
}
|
}
|
||||||
@@ -188,14 +182,9 @@ 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, $isFinal)->select('ew');
|
$qb = $this->buildQueryBySubscriber($user)->select('ew');
|
||||||
|
|
||||||
foreach ($orderBy as $key => $sort) {
|
foreach ($orderBy as $key => $sort) {
|
||||||
$qb->addOrderBy('ew.'.$key, $sort);
|
$qb->addOrderBy('ew.'.$key, $sort);
|
||||||
@@ -323,7 +312,7 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildQueryBySubscriber(User $user, ?bool $isFinal): QueryBuilder
|
private function buildQueryBySubscriber(User $user): QueryBuilder
|
||||||
{
|
{
|
||||||
$qb = $this->repository->createQueryBuilder('ew');
|
$qb = $this->repository->createQueryBuilder('ew');
|
||||||
|
|
||||||
@@ -336,14 +325,6 @@ 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,7 +1,4 @@
|
|||||||
import {
|
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
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";
|
||||||
|
|
||||||
@@ -78,6 +75,7 @@ export interface Postcode {
|
|||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
center: Point;
|
center: Point;
|
||||||
|
country: Country;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Point {
|
export interface Point {
|
||||||
@@ -93,6 +91,28 @@ export interface Country {
|
|||||||
|
|
||||||
export type AddressRefStatus = "match" | "to_review" | "reviewed";
|
export type AddressRefStatus = "match" | "to_review" | "reviewed";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to create an address
|
||||||
|
*/
|
||||||
|
export interface AddressCreation {
|
||||||
|
confidential: boolean;
|
||||||
|
isNoAddress: boolean;
|
||||||
|
street: string;
|
||||||
|
streetNumber: string;
|
||||||
|
postcode: Postcode;
|
||||||
|
point: Point; // [number, number]; // [longitude, latitude]
|
||||||
|
addressReference: AddressReference;
|
||||||
|
validFrom: DateTime|null;
|
||||||
|
floor: string;
|
||||||
|
corridor: string;
|
||||||
|
steps: string;
|
||||||
|
flat: string;
|
||||||
|
buildingName: string;
|
||||||
|
distribution: string;
|
||||||
|
extra: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface Address {
|
export interface Address {
|
||||||
type: "address";
|
type: "address";
|
||||||
address_id: number;
|
address_id: number;
|
||||||
@@ -111,7 +131,7 @@ export interface Address {
|
|||||||
confidential: boolean;
|
confidential: boolean;
|
||||||
lines: string[];
|
lines: string[];
|
||||||
addressReference: AddressReference | null;
|
addressReference: AddressReference | null;
|
||||||
validFrom: DateTime;
|
validFrom: DateTime | null; // TODO there is no null for validFrom
|
||||||
validTo: DateTime | null;
|
validTo: DateTime | null;
|
||||||
point: Point | null;
|
point: Point | null;
|
||||||
refStatus: AddressRefStatus;
|
refStatus: AddressRefStatus;
|
||||||
@@ -206,25 +226,6 @@ 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;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||||
|
import AddressPicker from "ChillMainAssets/vuejs/AddressPicker/AddressPicker.vue";
|
||||||
|
import {Ref, ref} from "vue";
|
||||||
|
|
||||||
|
const showModal: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
|
const modalDialogClasses = {"modal-dialog": true, "modal-dialog-scrollable": true, "modal-xl": true};
|
||||||
|
|
||||||
|
const clickButton = () => {
|
||||||
|
showModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<modal v-if="showModal" :hide-footer="false" :modal-dialog-class="modalDialogClasses" @close="closeModal">
|
||||||
|
<template v-slot:header>TODO</template>
|
||||||
|
<template v-slot:body>
|
||||||
|
<AddressPicker></AddressPicker>
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
<button class="btn btn-submit" type="button" @click="clickButton">SEARCH ADDRESS</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Address, AddressReference} from "ChillMainAssets/types";
|
||||||
|
import SearchBar from "ChillMainAssets/vuejs/AddressPicker/Component/SearchBar.vue";
|
||||||
|
import {
|
||||||
|
AddressAggregated,
|
||||||
|
AssociatedPostalCode, fetchAddressReference,
|
||||||
|
getAddressesAggregated,
|
||||||
|
getPostalCodes,
|
||||||
|
} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
||||||
|
import {computed, Ref, ref} from "vue";
|
||||||
|
import AddressAggregatedList from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedList.vue";
|
||||||
|
import AddressDetailsForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressDetailsForm.vue";
|
||||||
|
import AddressForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressForm.vue";
|
||||||
|
import {trans, SAVE} from "translator";
|
||||||
|
|
||||||
|
interface AddressPickerProps {
|
||||||
|
suggestions?: Address[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<AddressPickerProps>(), {
|
||||||
|
suggestions: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const addresses: Ref<AddressAggregated[]> = ref([]);
|
||||||
|
const postalCodes: Ref<AssociatedPostalCode[]> = ref([]);
|
||||||
|
const searchTokens: Ref<string[]> = ref([]);
|
||||||
|
const addressReference: Ref<AddressReference|null> = ref(null);
|
||||||
|
|
||||||
|
let abortControllerSearchAddress: null | AbortController = null;
|
||||||
|
let abortControllerSearchPostalCode: null | AbortController = null;
|
||||||
|
|
||||||
|
const searchResultsClasses = computed(() => ({
|
||||||
|
"mid-size": addressReference !== null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const floor = ref<string>("");
|
||||||
|
const corridor = ref<string>("");
|
||||||
|
const steps = ref<string>("");
|
||||||
|
const flat = ref<string>("");
|
||||||
|
const buildingName = ref<string>("");
|
||||||
|
const extra = ref<string>("");
|
||||||
|
const distribution = ref<string>("");
|
||||||
|
|
||||||
|
|
||||||
|
const onSearch = async function (search: string): Promise<void> {
|
||||||
|
performSearchForAddress(search);
|
||||||
|
performSearchForPostalCode(search);
|
||||||
|
searchTokens.value = [search];
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPickPosition = async (id: string) => {
|
||||||
|
console.log('Pick Position', id);
|
||||||
|
addressReference.value = await fetchAddressReference(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const performSearchForAddress = async (search: string): Promise<void> => {
|
||||||
|
if (null !== abortControllerSearchAddress) {
|
||||||
|
abortControllerSearchAddress.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("" === search) {
|
||||||
|
addresses.value = [];
|
||||||
|
abortControllerSearchAddress = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
abortControllerSearchAddress = new AbortController();
|
||||||
|
|
||||||
|
console.log("onSearch", search);
|
||||||
|
try {
|
||||||
|
addresses.value = await getAddressesAggregated(
|
||||||
|
search,
|
||||||
|
abortControllerSearchAddress,
|
||||||
|
);
|
||||||
|
abortControllerSearchAddress = null;
|
||||||
|
|
||||||
|
// check if there is only one result
|
||||||
|
if (addresses.value.length === 1 && Object.keys(addresses.value[0].positions).length === 1) {
|
||||||
|
onPickPosition(Object.keys(addresses.value[0].positions)[0]);
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof DOMException && e.name === "AbortError") {
|
||||||
|
console.log("search aborted for:", search);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const performSearchForPostalCode = async (search: string): Promise<void> => {
|
||||||
|
if (null !== abortControllerSearchPostalCode) {
|
||||||
|
abortControllerSearchPostalCode.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("" === search) {
|
||||||
|
addresses.value = [];
|
||||||
|
abortControllerSearchPostalCode = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
abortControllerSearchPostalCode = new AbortController();
|
||||||
|
|
||||||
|
console.log("onSearch", search);
|
||||||
|
try {
|
||||||
|
postalCodes.value = await getPostalCodes(
|
||||||
|
search,
|
||||||
|
abortControllerSearchPostalCode,
|
||||||
|
);
|
||||||
|
abortControllerSearchPostalCode = null;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof DOMException && e.name === "AbortError") {
|
||||||
|
console.log("search postal code aborted for:", search);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async(): Promise<void> => {
|
||||||
|
console.log("save");
|
||||||
|
console.log("content", floor, corridor, steps, flat, buildingName, extra, distribution);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<search-bar @search="onSearch"></search-bar>
|
||||||
|
<div class="address-pick-content">
|
||||||
|
<div class="search-results" :class="searchResultsClasses">
|
||||||
|
<address-aggregated-list :addresses="addresses" :search-tokens="searchTokens" @pick-position="(id) => onPickPosition(id)"></address-aggregated-list>
|
||||||
|
</div>
|
||||||
|
<div v-if="addressReference !== null" class="address-details-form">
|
||||||
|
<address-details-form :address="addressReference"
|
||||||
|
v-model:floor="floor"
|
||||||
|
v-model:corridor="corridor"
|
||||||
|
v-model:steps="steps"
|
||||||
|
v-model:flat="flat"
|
||||||
|
v-model:building-name="buildingName"
|
||||||
|
v-model:extra="extra"
|
||||||
|
v-model:distribution="distribution"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><button class="btn btn-save">{{ trans(SAVE) }}</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.address-pick-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
&.mid-size {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.address-details-form {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
||||||
|
import AddressAggregatedListItem from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedListItem.vue";
|
||||||
|
|
||||||
|
interface AddressAggregatedListProps {
|
||||||
|
addresses: AddressAggregated[];
|
||||||
|
searchTokens: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<AddressAggregatedListProps>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pickPosition: [id: string]
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-for="a in props.addresses" :key="a.row_number">
|
||||||
|
<address-aggregated-list-item :address="a" :search-tokens="props.searchTokens" @pick-position="(id) => emit('pickPosition', id)"></address-aggregated-list-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
|
||||||
|
interface AddressAggregatedListItemProps {
|
||||||
|
address: AddressAggregated;
|
||||||
|
searchTokens: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<AddressAggregatedListItemProps>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pickPosition: [id: string]
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const showAllPositions = ref<boolean>(false);
|
||||||
|
const positionsToShow = computed((): Record<string, string> => {
|
||||||
|
const obj: Record<string, any> = {};
|
||||||
|
let count = 0;
|
||||||
|
for (const [id, position] of Object.entries(props.address.positions)) {
|
||||||
|
obj[id] = position;
|
||||||
|
count++;
|
||||||
|
if (count >= 10 && !showAllPositions.value) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
})
|
||||||
|
const needToShowMorePosition = computed(() => {
|
||||||
|
return Object.keys(props.address.positions).length > 10;
|
||||||
|
})
|
||||||
|
|
||||||
|
const onClickButton = (id: string) => {
|
||||||
|
console.log('onClickButton', id);
|
||||||
|
emit('pickPosition', id);
|
||||||
|
}
|
||||||
|
const displayAllPositions = () => {
|
||||||
|
showAllPositions.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="street">
|
||||||
|
<span>{{ props.address.street }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="postcode">
|
||||||
|
<span>{{ props.address.code }}</span> <span>{{ address.label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="positions">
|
||||||
|
<ul>
|
||||||
|
<li v-for="(position, id) in positionsToShow" :key="id" >
|
||||||
|
<button type="button" @click="onClickButton(id)" >
|
||||||
|
{{ position }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="needToShowMorePosition">
|
||||||
|
<button @click="displayAllPositions">show all</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.street {
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.postcode {
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
.positions ul {
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {AddressReference} from "ChillMainAssets/types";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {addressReferenceToAddress} from "ChillMainAssets/vuejs/AddressPicker/helper";
|
||||||
|
import AddressDetailsContent from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsContent.vue";
|
||||||
|
import AddressForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressForm.vue";
|
||||||
|
|
||||||
|
export interface AddressDetailsFormProps {
|
||||||
|
address: AddressReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<AddressDetailsFormProps>();
|
||||||
|
|
||||||
|
const floor = ref<string>("");
|
||||||
|
const corridor = ref<string>("");
|
||||||
|
const steps = ref<string>("");
|
||||||
|
const flat = ref<string>("");
|
||||||
|
const buildingName = ref<string>("");
|
||||||
|
const extra = ref<string>("");
|
||||||
|
const distribution = ref<string>("");
|
||||||
|
|
||||||
|
const address = computed(() => addressReferenceToAddress(props.address));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<address-form
|
||||||
|
@update:floor="val => (floor = val)"
|
||||||
|
@update:corridor="val => (corridor = val)"
|
||||||
|
@update:steps="val => (steps = val)"
|
||||||
|
@update:flat="val => (flat = val)"
|
||||||
|
@update:building-name="val => (buildingName = val)"
|
||||||
|
@update:extra="val => (extra = val)"
|
||||||
|
@update:distribution="val => (distribution = val)"
|
||||||
|
></address-form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<address-details-content :address="address"></address-details-content>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ADDRESS_STREET,
|
||||||
|
ADDRESS_STREET_NUMBER,
|
||||||
|
ADDRESS_FLOOR,
|
||||||
|
ADDRESS_CORRIDOR,
|
||||||
|
ADDRESS_STEPS,
|
||||||
|
ADDRESS_FLAT,
|
||||||
|
ADDRESS_BUILDING_NAME,
|
||||||
|
ADDRESS_DISTRIBUTION,
|
||||||
|
ADDRESS_EXTRA,
|
||||||
|
ADDRESS_FILL_AN_ADDRESS,
|
||||||
|
trans,
|
||||||
|
} from "translator";
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
const isNoAddress = ref(false);
|
||||||
|
|
||||||
|
const floor = defineModel("floor");
|
||||||
|
const corridor = defineModel("corridor");
|
||||||
|
const steps = defineModel("steps");
|
||||||
|
const flat = defineModel("flat");
|
||||||
|
const buildingName = defineModel("buildingName");
|
||||||
|
const extra = defineModel("extra");
|
||||||
|
const distribution = defineModel("distribution");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="form-floating my-1">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="floor"
|
||||||
|
:placeholder="trans(ADDRESS_FLOOR)"
|
||||||
|
v-model="floor"
|
||||||
|
/>
|
||||||
|
<label for="floor">{{ trans(ADDRESS_FLOOR) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating my-1">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="corridor"
|
||||||
|
:placeholder="trans(ADDRESS_CORRIDOR)"
|
||||||
|
v-model="corridor"
|
||||||
|
/>
|
||||||
|
<label for="corridor">{{ trans(ADDRESS_CORRIDOR) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating my-1">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="steps"
|
||||||
|
:placeholder="trans(ADDRESS_STEPS)"
|
||||||
|
v-model="steps"
|
||||||
|
/>
|
||||||
|
<label for="steps">{{ trans(ADDRESS_STEPS) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating my-1">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="flat"
|
||||||
|
:placeholder="trans(ADDRESS_FLAT)"
|
||||||
|
v-model="flat"
|
||||||
|
/>
|
||||||
|
<label for="flat">{{ trans(ADDRESS_FLAT) }}</label>
|
||||||
|
</div>
|
||||||
|
<div :class="isNoAddress ? 'col-lg-12' : 'col-lg-6'">
|
||||||
|
<div class="form-floating my-1" v-if="!isNoAddress">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="buildingName"
|
||||||
|
maxlength="255"
|
||||||
|
:placeholder="trans(ADDRESS_BUILDING_NAME)"
|
||||||
|
v-model="buildingName"
|
||||||
|
/>
|
||||||
|
<label for="buildingName">{{
|
||||||
|
trans(ADDRESS_BUILDING_NAME)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating my-1">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="extra"
|
||||||
|
maxlength="255"
|
||||||
|
:placeholder="trans(ADDRESS_EXTRA)"
|
||||||
|
v-model="extra"
|
||||||
|
/>
|
||||||
|
<label for="extra">{{ trans(ADDRESS_EXTRA) }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating my-1" v-if="!isNoAddress">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="distribution"
|
||||||
|
maxlength="255"
|
||||||
|
:placeholder="trans(ADDRESS_DISTRIBUTION)"
|
||||||
|
v-model="distribution"
|
||||||
|
/>
|
||||||
|
<label for="distribution">{{
|
||||||
|
trans(ADDRESS_DISTRIBUTION)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ADDRESS_PICKER_SEARCH_FOR_ADDRESSES, trans } from 'translator';
|
||||||
|
const emits = defineEmits<{
|
||||||
|
search: [search: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let searchTimer = 0;
|
||||||
|
let searchString: string;
|
||||||
|
|
||||||
|
const onInput = function (event: InputEvent) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const value = target.value;
|
||||||
|
searchString = value;
|
||||||
|
|
||||||
|
if (0 === searchTimer) {
|
||||||
|
window.clearTimeout(searchTimer);
|
||||||
|
searchTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTimer = window.setTimeout(() => {
|
||||||
|
if (value === searchString) {
|
||||||
|
emits("search", value);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input type="search" class="form-control" @input="onInput" :placeholder="trans(ADDRESS_PICKER_SEARCH_FOR_ADDRESSES)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import {AddressReference, TranslatableString} from "ChillMainAssets/types";
|
||||||
|
|
||||||
|
export interface AddressAggregated {
|
||||||
|
row_number: number;
|
||||||
|
street: string;
|
||||||
|
postcode_id: number;
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
positions: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssociatedPostalCode {
|
||||||
|
postcode_id: number;
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
country_id: number;
|
||||||
|
country_code: string;
|
||||||
|
country_name: TranslatableString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws {DOMException} when fetch is aborted, the property name is always equals to 'AbortError'
|
||||||
|
*/
|
||||||
|
export const getAddressesAggregated = async (
|
||||||
|
search: string,
|
||||||
|
abortController: AbortController,
|
||||||
|
): Promise<AddressAggregated[]> => {
|
||||||
|
const params = new URLSearchParams({ q: search.trim() });
|
||||||
|
let response = null;
|
||||||
|
|
||||||
|
response = await fetch(
|
||||||
|
`/api/1.0/main/address-reference/aggregated/search?${params}`,
|
||||||
|
{ signal: abortController.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPostalCodes = async (
|
||||||
|
search: string,
|
||||||
|
abortController: AbortController,
|
||||||
|
): Promise<AssociatedPostalCode[]> => {
|
||||||
|
const params = new URLSearchParams({ q: search.trim() });
|
||||||
|
let response = null;
|
||||||
|
|
||||||
|
response = await fetch(
|
||||||
|
`/api/1.0/main/address-reference/postal-code/search?${params}`,
|
||||||
|
{ signal: abortController.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchAddressReference = async (id: string): Promise<AddressReference> => {
|
||||||
|
const response = await fetch(`/api/1.0/main/address-reference/${id}.json`);
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {Address, AddressCreation, AddressReference} from "ChillMainAssets/types";
|
||||||
|
|
||||||
|
export const addressReferenceToAddress = (reference: AddressReference): AddressCreation => {
|
||||||
|
return {
|
||||||
|
street: reference.street,
|
||||||
|
streetNumber: reference.streetNumber,
|
||||||
|
postcode: reference.postcode,
|
||||||
|
floor: "",
|
||||||
|
corridor: "",
|
||||||
|
steps: "",
|
||||||
|
flat: "",
|
||||||
|
buildingName: "",
|
||||||
|
distribution: "",
|
||||||
|
extra: "",
|
||||||
|
confidential: false,
|
||||||
|
addressReference: reference,
|
||||||
|
point: reference.point,
|
||||||
|
isNoAddress: false,
|
||||||
|
validFrom: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import AddressButton from "ChillMainAssets/vuejs/AddressPicker/AddressButton.vue";
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLDivElement>("div[data-address-picker]")
|
||||||
|
.forEach((elem): void => {
|
||||||
|
const app = createApp(AddressButton);
|
||||||
|
|
||||||
|
app.mount(elem);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,6 @@ 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;
|
||||||
@@ -84,7 +83,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">
|
||||||
{{ trans(WORKFLOW_ATTACHMENTS_ADD_AN_ATTACHMENT) }}
|
Ajouter une pièce jointe
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
|
||||||
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[];
|
||||||
@@ -21,43 +14,35 @@ 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="notNullAttachments.length === 0"
|
v-if="props.attachments.length === 0"
|
||||||
class="chill-no-data-statement text-center"
|
class="chill-no-data-statement text-center"
|
||||||
>
|
>
|
||||||
{{ trans(WORKFLOW_ATTACHMENTS_NO_ATTACHMENT) }}
|
Aucune pièce jointe
|
||||||
</p>
|
</p>
|
||||||
<div v-else class="flex-table">
|
<!-- TODO translate -->
|
||||||
<div v-for="a in notNullAttachments" :key="a.id" class="item-bloc">
|
<div else class="flex-table">
|
||||||
|
<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>
|
<li v-if="a.genericDoc?.storedObject !== null">
|
||||||
<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 v-if="canRemove">
|
<li
|
||||||
|
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 { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
|
|
||||||
interface GenericDocItemBoxProps {
|
interface GenericDocItemBoxProps {
|
||||||
genericDoc: GenericDoc;
|
genericDoc: GenericDocForAccompanyingPeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<GenericDocItemBoxProps>();
|
const props = defineProps<GenericDocItemBoxProps>();
|
||||||
|
|||||||
@@ -4,24 +4,27 @@
|
|||||||
:show-button-details="false"
|
:show-button-details="false"
|
||||||
></address-render-box>
|
></address-render-box>
|
||||||
<address-details-ref-matching
|
<address-details-ref-matching
|
||||||
|
v-if="isAddress(props.address)"
|
||||||
:address="props.address"
|
:address="props.address"
|
||||||
@update-address="onUpdateAddress"
|
@update-address="onUpdateAddress"
|
||||||
></address-details-ref-matching>
|
></address-details-ref-matching>
|
||||||
<address-details-map :address="props.address"></address-details-map>
|
<address-details-map :address="props.address"></address-details-map>
|
||||||
<address-details-geographical-layers
|
<address-details-geographical-layers
|
||||||
|
v-if="isAddress(props.address)"
|
||||||
:address="props.address"
|
:address="props.address"
|
||||||
></address-details-geographical-layers>
|
></address-details-geographical-layers>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Address } from "../../../types";
|
import {Address, AddressCreation} from "../../../types";
|
||||||
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
|
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
|
||||||
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
|
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
|
||||||
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
|
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
|
||||||
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
|
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
|
||||||
|
import {isAddress} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
||||||
|
|
||||||
interface AddressModalContentProps {
|
interface AddressModalContentProps {
|
||||||
address: Address;
|
address: Address|AddressCreation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<AddressModalContentProps>();
|
const props = defineProps<AddressModalContentProps>();
|
||||||
|
|||||||
@@ -12,90 +12,91 @@
|
|||||||
Voir sur
|
Voir sur
|
||||||
<a :href="makeUrlGoogleMap(props.address)" target="_blank"
|
<a :href="makeUrlGoogleMap(props.address)" target="_blank"
|
||||||
>Google Maps</a
|
>Google Maps</a
|
||||||
>
|
> <a
|
||||||
<a :href="makeUrlOsm(props.address)" target="_blank">OSM</a>
|
:href="makeUrlOsm(props.address)" target="_blank"
|
||||||
|
>OSM</a>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from "vue";
|
import {computed, onMounted, ref} from "vue";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import markerIconPng from "leaflet/dist/images/marker-icon.png";
|
import markerIconPng from "leaflet/dist/images/marker-icon.png";
|
||||||
import L, { LatLngExpression, LatLngTuple } from "leaflet";
|
import L, { LatLngExpression, LatLngTuple } from "leaflet";
|
||||||
import { Address, Point } from "../../../../types";
|
import {Address, AddressCreation, Point} from "../../../../types";
|
||||||
|
import {buildAddressLines, getAddressPoint} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
||||||
|
import {useLeafletDisplayLayer, useLeafletMap, useLeafletMarker, useLeafletTileLayer} from "vue-use-leaflet";
|
||||||
|
|
||||||
const lonLatForLeaflet = (point: Point): LatLngTuple => {
|
const lonLatForLeaflet = (point: Point): LatLngTuple => {
|
||||||
return [point.coordinates[1], point.coordinates[0]];
|
return [point.coordinates[1], point.coordinates[0]];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MapProps {
|
export interface MapProps {
|
||||||
address: Address;
|
address: Address|AddressCreation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<MapProps>();
|
const props = defineProps<MapProps>();
|
||||||
|
|
||||||
const map_div = ref<HTMLDivElement | null>(null);
|
const markerIcon = L.icon({
|
||||||
let map: L.Map | null = null;
|
iconUrl: markerIconPng,
|
||||||
let marker: L.Marker | null = null;
|
iconAnchor: [12, 41],
|
||||||
|
});
|
||||||
onMounted(() => {
|
const latLngMarker = computed((): LatLngExpression => {
|
||||||
if (map_div.value === null) {
|
if (props.address === null || props.address.point === null) {
|
||||||
// there is no map div when the address does not have any Point
|
return [0, 0, 0];
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.address.point !== null) {
|
return [props.address.point.coordinates[1], props.address.point.coordinates[0], 0]
|
||||||
map = L.map(map_div.value);
|
|
||||||
map.setView(lonLatForLeaflet(props.address.point), 18);
|
|
||||||
|
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
||||||
attribution:
|
|
||||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const markerIcon = L.icon({
|
|
||||||
iconUrl: markerIconPng,
|
|
||||||
iconAnchor: [12, 41],
|
|
||||||
});
|
|
||||||
|
|
||||||
marker = L.marker(lonLatForLeaflet(props.address.point), {
|
|
||||||
icon: markerIcon,
|
|
||||||
});
|
|
||||||
marker.addTo(map);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeUrlGoogleMap = (address: Address): string => {
|
const map_div = ref<HTMLDivElement | null>(null);
|
||||||
|
const map = useLeafletMap(map_div, {zoom: 18, center: latLngMarker});
|
||||||
|
const tileLayer = useLeafletTileLayer(
|
||||||
|
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useLeafletDisplayLayer(map, tileLayer);
|
||||||
|
|
||||||
|
|
||||||
|
const marker = useLeafletMarker(latLngMarker, {icon: markerIcon});
|
||||||
|
useLeafletDisplayLayer(map, marker);
|
||||||
|
|
||||||
|
|
||||||
|
const makeUrlGoogleMap = (address: Address|AddressCreation): string => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("api", "1");
|
params.append("api", "1");
|
||||||
if (address.point !== null && address.addressReference !== null) {
|
const point = getAddressPoint(address);
|
||||||
|
if (point !== null && address.addressReference !== null) {
|
||||||
params.append(
|
params.append(
|
||||||
"query",
|
"query",
|
||||||
`${address.point.coordinates[1]} ${address.point.coordinates[0]}`,
|
`${point.coordinates[1]} ${point.coordinates[0]}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
params.append("query", address.lines.join(", "));
|
params.append("query", buildAddressLines(address).join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
return `https://www.google.com/maps/search/?${params.toString()}`;
|
return `https://www.google.com/maps/search/?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeUrlOsm = (address: Address): string => {
|
const makeUrlOsm = (address: Address|AddressCreation): string => {
|
||||||
if (address.point !== null && address.addressReference !== null) {
|
const point = getAddressPoint(address);
|
||||||
|
if (point !== null && address.addressReference !== null) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("mlat", `${address.point.coordinates[1]}`);
|
params.append("mlat", `${point.coordinates[1]}`);
|
||||||
params.append("mlon", `${address.point.coordinates[0]}`);
|
params.append("mlon", `${point.coordinates[0]}`);
|
||||||
const hashParams = new URLSearchParams();
|
const hashParams = new URLSearchParams();
|
||||||
hashParams.append(
|
hashParams.append(
|
||||||
"map",
|
"map",
|
||||||
`18/${address.point.coordinates[1]}/${address.point.coordinates[0]}`,
|
`18/${point.coordinates[1]}/${point.coordinates[0]}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
|
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("query", address.lines.join(", "));
|
params.append("query", buildAddressLines(address).join(", "));
|
||||||
|
|
||||||
return `https://www.openstreetmap.org/search?${params.toString()}`;
|
return `https://www.openstreetmap.org/search?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user