Compare commits

..

15 Commits

Author SHA1 Message Date
caaed3e759 Add AddressForm component and integrate it into AddressPicker
- Created `AddressForm.vue` for managing address details inputs (e.g., floor, corridor, steps, etc.).
- Replaced placeholder form in `AddressDetailsForm.vue` with the new `AddressForm` component.
- Added bi-directional binding for address fields in `AddressPicker.vue`.
- Updated `package.json` to include `vue-tsc` dependency for improved TypeScript support.
2025-10-06 13:00:43 +02:00
6380fdd9a4 Normalize address search terms by adding UNACCENT and LOWER transformation to AddressReferenceRepository. 2025-10-05 21:04:40 +02:00
fcd5080e6f [wip] Enhance AddressPicker to auto-select position when a single address with one position is found. 2025-10-05 21:04:39 +02:00
086d418aa3 [wip] Refactor AddressDetailsMap to use vue-use-leaflet and enhance AddressPicker layout with dynamic styling adjustments. 2025-10-05 21:04:39 +02:00
4e61821e5b [WIP] refactorization to show details of an address 2025-10-05 21:04:39 +02:00
e3aeab315f [WIP] Create Address button that will open AddressPicker in a modal 2025-10-05 21:04:38 +02:00
8ec1063ef8 [WIP] Add postal code search integration to AddressPicker
Implemented `getPostalCodes` function in `local-search` driver and connected it with `AddressPicker.vue`. Introduced UI changes to display postal codes alongside addresses and ensured search requests are abortable.
2025-10-05 21:04:38 +02:00
aad9c984b1 [WIP] Add postal code search endpoint and controller integration
Introduced a new API endpoint `/api/1.0/main/address-reference/postal-code/search` for searching postal codes matching a query string. Implemented `PostalCodeForAddressReferenceApiController` to handle requests and integrated with `PostalCodeForAddressReferenceRepository`. Enhanced repository to include `country_name` in results by decoding JSON data. Updated API specifications accordingly.
2025-10-05 21:04:38 +02:00
34b3e290e1 [WIP] Add PostalCodeForAddressReferenceRepository and associated tests
Introduced `PostalCodeForAddressReferenceRepository` and its interface to support optimized postal code search using materialized views. Updated `AddressReferenceRepository` to improve query handling. Added test coverage for the new repository functionality.
2025-10-05 21:04:37 +02:00
0987b575ab [WIP] Refactor AddressReferenceRepository to use interface and add tests for AddressReferenceAggregatedApiController 2025-10-05 21:04:37 +02:00
d960578c5f [WIP] Integrate local aggregated address search in AddressPicker
Added a `local-search` driver to support aggregated address fetching. Integrated the `getAddressesAggregated` function with `AddressPicker.vue` for dynamic search suggestions and abortable fetch requests.
2025-10-05 21:04:37 +02:00
176048bce6 [WIP] Add aggregated address search API endpoint
Introduced a new API endpoint `/api/1.0/main/address-reference/aggregated/search` for aggregated address reference search with support for query filtering. Extended repository with `findAggregatedBySearchString` method and updated materialized view `view_chill_main_address_reference`. Added test coverage and API specification details.
2025-10-05 21:04:36 +02:00
be210a6dd6 [WIP] initialize search bar 2025-10-05 21:04:36 +02:00
4323773595 [WIP] initialize an app for address-picker + a demo page 2025-10-05 21:04:32 +02:00
6d432ca2cb Add materialized view and repository methods for address search
Introduced a materialized view `view_chill_main_address_reference` to optimize address search queries and added corresponding repository methods `findBySearchString` and `countBySearchString`. Also included test coverage for the repository to validate the new functionality.
2025-10-05 21:04:15 +02:00
190 changed files with 2048 additions and 3044 deletions

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Insert name of file as the document title when uploading
time: 2025-11-19T13:33:24.778116633+01:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Add missing path paramater 'id' for editing multiple participations
time: 2025-11-19T13:48:02.078949572+01:00
custom:
Issue: ""
SchemaChange: No schema change

View File

@@ -1,6 +0,0 @@
kind: UX
body: Alphabetically order userJobs and mainLocations within user creation form
time: 2025-11-19T15:37:06.393470745+01:00
custom:
Issue: "470"
SchemaChange: No schema change

View File

@@ -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

View File

@@ -1,3 +0,0 @@
## v4.6.1 - 2025-10-27
### Fixed
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php

View File

@@ -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

View File

@@ -1,9 +0,0 @@
## v4.8.0 - 2025-11-17
### Feature
* ([#461](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/461)) Make a calendar item on the 'mes rendez-vous' page clickable. Clicking will navigate to the edit page of the calendar item.
### Fixed
* ([#463](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/463)) Display calendar items for which an invite was accepted on the mes rendez-vous page
* Improve accessibility on login page
### UX
* ([#449](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/449)) Remove the label if there is only one scope and no scope picking field is displayed.

View File

@@ -240,6 +240,9 @@ The tests are run from the project's root (not from the bundle's root).
# Run all tests
vendor/bin/phpunit
# Run tests for a specific bundle
vendor/bin/phpunit --testsuite NameBundle
# Run a specific test file
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
```
When writing tests, only test specific files. Do not run all tests or the full
test suite.
#### Test Structure
Tests are organized by bundle and follow the same structure as the bundle itself:

View File

@@ -6,57 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v4.8.0 - 2025-11-17
### Feature
* ([#461](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/461)) Make a calendar item on the 'mes rendez-vous' page clickable. Clicking will navigate to the edit page of the calendar item.
### Fixed
* ([#463](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/463)) Display calendar items for which an invite was accepted on the mes rendez-vous page
* Improve accessibility on login page
### UX
* ([#449](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/449)) Remove the label if there is only one scope and no scope picking field is displayed.
## v4.7.0 - 2025-11-10
### Feature
* ([#385](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/385)) Create invitation list in user menu
* ([#404](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/404)) Add columns for comments linked to an activity in the activity list export
### Fixed
* ([#451](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/451)) Fix: display also social actions linked to parents of the selected social issue
* ([#453](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/453)) Fix: export actions and their results in csv even when action does not have any goals attached to it.
* Fix the possibility to delete a workflow
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
* ([#457](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/457)) Fix the fusion of thirdparty properties that are located in another schema than public for TO_ONE relations + add extra loop for MANY_TO_MANY relations where thirdparty is the source instead of the target
* ([#428](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/428)) Fix suggestion of referrer when creating notification for accompanyingPeriodWorkDocument
### DX
* Send notifications log to dedicated channel, if it exists
### UX
* ([#425](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/425)) Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
* ([#542](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/542)) Improve the ux for selecting whether user wants to be notified of the final step of a workflow or all steps
* Expand timeSpent choices for evaluation document and translate them to user locale or fallback 'fr'
* ([#455](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/455)) Change the order of display for results and objectives in the social work/action form
* Wrap text when it is too long within badges
## v4.6.1 - 2025-10-27
### Fixed
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php
## v4.6.0 - 2025-10-15
### Feature
* ([#423](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/423)) Create environment banner that can be activated and configured depending on the image deployed
* ([#394](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/394)) Only show active workflow on the page "my tracked workflow"
### Fixed
* Fix loading of classLists in SocialIssuesAcc.vue, ensure elements are present
* Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted
* ([#434](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/434)) Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists
* Fix loading of social issues and social actions within vue component
* ([#446](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/446)) Add unique condition on stored object filename, with cleaning step on existing duplicate filenames
**Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
* [workflow] take permissions into account to delete the workflow attachment
* ([#448](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/448)) Fix the execution of daily cronjob notification, when the previous last execution storage was invalid
## v4.5.1 - 2025-10-03
### Fixed
* Add missing javascript dependency

View File

@@ -14,7 +14,7 @@
"ext-openssl": "*",
"ext-redis": "*",
"ext-zlib": "*",
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
"champs-libres/wopi-bundle": "dev-master@dev",
"champs-libres/wopi-lib": "dev-master@dev",
"doctrine/data-fixtures": "^1.8",
"doctrine/doctrine-bundle": "^2.1",

View File

@@ -2,6 +2,7 @@
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
ChampsLibres\WopiBundle\WopiBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
@@ -36,5 +37,4 @@ return [
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
];

View File

@@ -1,13 +1,6 @@
chill_main:
available_languages: [ '%env(resolve:LOCALE)%', 'en', 'nl' ]
available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
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:
from_email: '%env(resolve:NOTIFICATION_FROM_EMAIL)%'
from_name: '%env(resolve:NOTIFICATION_FROM_NAME)%'

View File

@@ -1,2 +0,0 @@
chill_aside_activity:
show_concerned_persons_count: hidden

View File

@@ -17,3 +17,9 @@ when@dev:
defaults:
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'

View File

@@ -23,8 +23,8 @@ class "Document" {
- text description
- ArrayCollection_DocumentCategory categories
- varchar_150 content #link to openstack
- Territoire territoire
- Service service
- Center center
- Cercle cercle
- User user
- DateTime date # Creation date
}

View File

@@ -38,7 +38,7 @@ Certaines données sont historisées:
- les référents 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.
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.

View File

@@ -1,6 +1,6 @@
order,table_schema,table_name,commentaire
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é)
4,chill_3party,third_party,Tiers
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
54,public,activitytype,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,
58,public,chill_activity_activity_chill_person_socialissue
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
111,public,chill_person_not_duplicate,
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é
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
@@ -142,7 +142,7 @@ order,table_schema,table_name,commentaire
141,public,permission_groups
142,public,permissionsgroup_rolescope
143,public,persons_spoken_languages
144,public,regroupment,Regroupement de territoires
144,public,regroupment,Regroupement de centres
145,public,regroupment_center,
146,public,role_scopes,
147,public,scopes,Services
1 order table_schema table_name commentaire
2 1 chill_3party party_category Catégorie de tiers
3 2 chill_3party party_center Association entre les tiers et les territoires (déprécié) Association entre les tiers et les centres (déprécié)
4 3 chill_3party party_profession Profession du tiers (déprécié)
5 4 chill_3party third_party Tiers
6 5 chill_3party thirdparty_category association tiers - catégories
54 53 public activitytpresence Présence aux échanges
55 54 public activitytype Types d'échanges
56 55 public activitytypecategory Catégories de types d'échanges
57 56 public centers Territoires (territoires, agences, etc.) Centres (territoires, agences, etc.)
58 57 public chill_activity_activity_chill_person_socialaction
59 58 public chill_activity_activity_chill_person_socialissue
60 59 public chill_docgen_template Gabarits de documents
111 110 public chill_person_marital_status Etats civils
112 111 public chill_person_not_duplicate
113 112 public chill_person_person Usagers
114 113 public chill_person_person_center_history Historique des territoires d'un usagers Historique des centres d'un usagers
115 114 public chill_person_persons_to_addresses Déprécié
116 115 public chill_person_phone Numéros d etéléphone supplémentaires d'un usager
117 116 public chill_person_relations Types de relations de filiation
142 141 public permission_groups
143 142 public permissionsgroup_rolescope
144 143 public persons_spoken_languages
145 144 public regroupment Regroupement de territoires Regroupement de centres
146 145 public regroupment_center
147 146 public role_scopes
148 147 public scopes Services

View File

@@ -45,6 +45,7 @@
"webpack-cli": "^5.0.1"
},
"dependencies": {
"@fragaria/address-formatter": "^6.6.1",
"@fullcalendar/core": "^6.1.4",
"@fullcalendar/daygrid": "^6.1.4",
"@fullcalendar/interaction": "^6.1.4",
@@ -66,10 +67,12 @@
"mime": "^4.0.0",
"pdfjs-dist": "^4.3.136",
"vis-network": "^9.1.0",
"vue": "^3.5.6",
"vue": "^3.5.x",
"vue-i18n": "^9.1.6",
"vue-multiselect": "3.0.0-alpha.2",
"vue-toast-notification": "^3.1.2",
"vue-tsc": "^3.1.0",
"vue-use-leaflet": "^0.1.7",
"vuex": "^4.0.0"
},
"browserslist": [

View File

@@ -66,9 +66,6 @@ class ListActivityHelper
->leftJoin('activity.location', 'location')
->addSelect('location.name AS locationName')
->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('activity.createdAt')
->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),
'createdBy', 'updatedBy' => $this->userHelper->getLabel($key, $values, $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) {
if ('_header' === $value) {
return 'Attendee';
@@ -181,9 +176,6 @@ class ListActivityHelper
'usersNames',
'thirdPartiesIds',
'thirdPartiesNames',
'commentText',
'commentDate',
'commentUser',
'createdBy',
'createdAt',
'updatedBy',

View File

@@ -90,9 +90,7 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt
public function getFormDefaultData(): array
{
return [
'reasons' => [],
];
return [];
}
public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array

View File

@@ -42,8 +42,6 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
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
$sqb = $qb->getEntityManager()->createQueryBuilder();
$sqb->select('1')
@@ -61,6 +59,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
if (\in_array('activity', $qb->getAllAliases(), true)) {
$sqb->andWhere('activity_person_having_activity.id = activity.id');
}
if (isset($data['reasons']) && [] !== $data['reasons']) {
// add clause activity reason
$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
{
$normalized = [
'date_from_rolling' => $formData['date_from_rolling']->normalize(),
'date_to_rolling' => $formData['date_to_rolling']->normalize(),
'reasons' => [],
];
if (isset($formData['reasons']) && [] !== $formData['reasons']) {
$normalized['reasons'] = array_map(
fn (ActivityReason $reason) => $reason->getId(),
$formData['reasons']
);
}
return $normalized;
return ['date_from_rolling' => $formData['date_from_rolling']->normalize(), 'date_to_rolling' => $formData['date_to_rolling']->normalize()];
}
public function denormalizeFormData(array $formData, int $fromVersion): array
{
$denormalized = [
'date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']),
'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling']),
'reasons' => [],
];
if (isset($formData['reasons']) && [] !== $formData['reasons']) {
$denormalized['reasons'] = array_map(
fn ($id) => $this->activityReasonRepository->find($id),
$formData['reasons']
);
}
return $denormalized;
return ['date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']), 'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling'])];
}
public function getFormDefaultData(): array
@@ -170,12 +143,10 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
public function describeAction($data, ExportGenerationContext $context): array
{
$reasons = $data['reasons'] ?? [];
return [
[] === $reasons ?
'export.filter.activity.describe_action_with_no_subject'
: 'export.filter.activity.describe_action_with_subject',
[] === $data['reasons'] ?
'export.filter.person_between_dates.describe_action_with_no_subject'
: 'export.filter.person_between_dates.describe_action_with_subject',
[
'date_from' => $this->rollingDateConverter->convert($data['date_from_rolling']),
'date_to' => $this->rollingDateConverter->convert($data['date_to_rolling']),
@@ -183,7 +154,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
', ',
array_map(
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
{
error_log('validateForm called with data: '.json_encode(array_keys($data)));
if ($this->rollingDateConverter->convert($data['date_from_rolling'])
>= $this->rollingDateConverter->convert($data['date_to_rolling'])) {
$context->buildViolation('export.filter.activity.person_between_dates.date mismatch')

View File

@@ -88,8 +88,8 @@ class ActivityType extends AbstractType
if (null !== $options['data']->getPerson()) {
$builder->add('scope', ScopePickerType::class, [
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
'center' => $options['center'],
'role' => ActivityVoter::CREATE === (string) $options['role'] ? ActivityVoter::CREATE_PERSON : (string) $options['role'],
'required' => true,
]);
}

View File

@@ -136,14 +136,8 @@ export default {
issueIsLoading: false,
actionIsLoading: false,
actionAreLoaded: false,
socialIssuesClassList: {
"col-form-label": true,
required: false,
},
socialActionsClassList: {
"col-form-label": true,
required: false,
},
socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`,
socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`,
};
},
computed: {
@@ -164,21 +158,6 @@ export default {
},
},
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 */
this.issueIsLoading = true;
this.actionAreLoaded = false;

View File

@@ -43,23 +43,11 @@ export default {
span.badge {
@include badge_social($social-action-color);
font-size: 95%;
white-space: normal;
word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
margin-right: 1em;
text-align: left;
line-height: 1.2em;
&::before {
position: absolute;
left: 11px;
top: 0;
margin: 0 0.3em 0 -0.75em;
}
position: relative;
padding-left: 1.5em;
max-width: 100%; /* Adjust as needed */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -43,22 +43,7 @@ export default {
span.badge {
@include badge_social($social-issue-color);
font-size: 95%;
white-space: normal;
word-wrap: break-word;
word-break: break-word;
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
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>

View File

@@ -10,7 +10,7 @@ Attendee: Présence de l'usager
attendee: présence de l'usager
list_reasons: liste des sujets
user_username: nom de l'utilisateur
circle_name: nom du service
circle_name: nom du cercle
Remark: Commentaire
No comments: Aucun commentaire
Add a new activity: Ajouter une nouvel échange
@@ -20,7 +20,7 @@ not present: absent
Delete: Supprimer
Update: Mettre à jour
Update activity: Modifier l'échange
Scope: Service
Scope: Cercle
Activity data: Données de l'échange
Activity location: Localisation de l'échange
No reason associated: Aucun sujet
@@ -398,15 +398,13 @@ export:
sent received: Envoyé ou reçu
emergency: Urgence
accompanying course id: Identifiant du parcours
course circles: Services du parcours
course circles: Cercles du parcours
travelTime: Durée de déplacement
durationTime: Durée
id: Identifiant
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
commentText: Commentaire
comment_date: Date de la dernière édition du commentaire
comment_user: Dernière édition par
filter:
activity:

View File

@@ -25,7 +25,6 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
$config = $this->processConfiguration($configuration, $configs);
$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->load('services.yaml');
@@ -39,24 +38,6 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
{
$this->prependRoute($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)

View File

@@ -141,12 +141,6 @@ class Configuration implements ConfigurationInterface
->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();
return $treeBuilder;

View File

@@ -62,10 +62,6 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
#[ORM\ManyToOne(targetEntity: User::class)]
private User $updatedBy;
#[Assert\GreaterThanOrEqual(0)]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true)]
private ?int $concernedPersonsCount = 0;
public function getAgent(): ?User
{
return $this->agent;
@@ -190,16 +186,4 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
public function getConcernedPersonsCount(): ?int
{
return $this->concernedPersonsCount;
}
public function setConcernedPersonsCount(?int $concernedPersonsCount): self
{
$this->concernedPersonsCount = $concernedPersonsCount;
return $this;
}
}

View File

@@ -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';
}
}

View File

@@ -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,
];
}
}

View File

@@ -21,7 +21,6 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
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\FormEvent;
use Symfony\Component\Form\FormEvents;
@@ -30,13 +29,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
final class AsideActivityFormType extends AbstractType
{
private readonly array $timeChoices;
private readonly bool $showConcernedPersonsCount;
public function __construct(
ParameterBagInterface $parameterBag,
) {
$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)
@@ -79,16 +76,6 @@ final class AsideActivityFormType extends AbstractType
->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) {
$builder->get($fieldName)
->addModelTransformer($durationTimeTransformer);

View File

@@ -42,11 +42,6 @@
{%- if entity.location.name is defined -%}
<div><i class="fa fa-fw fa-map-marker"></i>{{ entity.location.name }}</div>
{%- endif -%}
{%- if entity.concernedPersonsCount > 0 -%}
<div><i class="fa fa-fw fa-user"></i>{{ entity.concernedPersonsCount }}</div>
{%- endif -%}
</div>
<div class="item-col" style="justify-content: flex-end;">
<div class="box">

View File

@@ -38,11 +38,6 @@
<dt class="inline">{{ 'Duration'|trans }}</dt>
<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>
{%- if entity.note is empty -%}
<dd>

View File

@@ -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'),
];
}
}

View File

@@ -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'],
];
}
}

View File

@@ -20,10 +20,6 @@ services:
tags:
- { 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
chill.aside_activity.export.date_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
@@ -74,7 +70,3 @@ services:
Chill\AsideActivityBundle\Export\Aggregator\ByLocationAggregator:
tags:
- { 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' }

View File

@@ -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');
}
}

View File

@@ -27,7 +27,6 @@ Emergency: Urgent
by: "Par "
location: Lieu
Asideactivity location: Localisation de l'activité
Concerned persons count: Nombre d'usager concernés
# Crud
crud:
@@ -178,7 +177,7 @@ export:
agent_id: Utilisateur
creator_id: Créateur
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
date: Date
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
Average aside activities duration: Durée moyenne 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 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é
@@ -212,8 +210,6 @@ export:
'Filtered by aside activity location: only %location%': "Filtré par localisation: uniquement %location%"
aggregator:
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
by_user_job:
Aggregate by user job: Grouper les activités annexes par métier des utilisateurs

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\CalendarBundle\Repository\InviteRepository;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Serializer\Model\Collection;
@@ -24,10 +23,7 @@ use Symfony\Component\Routing\Annotation\Route;
class CalendarAPIController extends ApiController
{
public function __construct(
private readonly CalendarRepository $calendarRepository,
private readonly InviteRepository $inviteRepository,
) {}
public function __construct(private readonly CalendarRepository $calendarRepository) {}
#[Route(path: '/api/1.0/calendar/calendar/by-user/{id}.{_format}', name: 'chill_api_single_calendar_list_by-user', requirements: ['_format' => 'json'])]
public function listByUser(User $user, Request $request, string $_format): JsonResponse
@@ -56,37 +52,16 @@ class CalendarAPIController extends ApiController
throw new BadRequestHttpException('dateTo not parsable');
}
// Get calendar items where user is the main user
$ownCalendars = $this->calendarRepository->findByUser(
$total = $this->calendarRepository->countByUser($user, $dateFrom, $dateTo);
$paginator = $this->getPaginatorFactory()->create($total);
$ranges = $this->calendarRepository->findByUser(
$user,
$dateFrom,
$dateTo
$dateTo,
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
// Get calendar items from accepted invites
$acceptedInvites = $this->inviteRepository->findAcceptedInvitesByUserAndDateRange($user, $dateFrom, $dateTo);
$inviteCalendars = array_map(fn ($invite) => $invite->getCalendar(), $acceptedInvites);
// Merge
$allCalendars = array_merge($ownCalendars, $inviteCalendars);
$uniqueCalendars = [];
$seenIds = [];
foreach ($allCalendars as $calendar) {
$id = $calendar->getId();
if (!in_array($id, $seenIds, true)) {
$seenIds[] = $id;
$uniqueCalendars[] = $calendar;
}
}
$total = count($uniqueCalendars);
$paginator = $this->getPaginatorFactory()->create($total);
$offset = $paginator->getCurrentPageFirstItemNumber();
$limit = $paginator->getItemsPerPage();
$ranges = array_slice($uniqueCalendars, $offset, $limit);
$collection = new Collection($ranges, $paginator);
return $this->json($collection, Response::HTTP_OK, [], ['groups' => ['calendar:light']]);

View File

@@ -13,7 +13,6 @@ namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Form\CalendarType;
use Chill\CalendarBundle\Form\CancelType;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface;
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\PersonVoter;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\EntityManagerInterface;
use http\Exception\UnexpectedValueException;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -62,7 +60,6 @@ class CalendarController extends AbstractController
private readonly UserRepositoryInterface $userRepository,
private readonly TranslatorInterface $translator,
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.
*/
@@ -318,7 +266,7 @@ class CalendarController extends AbstractController
}
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 File

@@ -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),
]);
}
}

View File

@@ -35,7 +35,7 @@ class LoadCancelReason extends Fixture implements FixtureGroupInterface
$arr = [
['name' => CancelReason::CANCELEDBY_USER],
['name' => CancelReason::CANCELEDBY_PERSON],
['name' => CancelReason::CANCELEDBY_OTHER],
['name' => CancelReason::CANCELEDBY_DONOTCOUNT],
];
foreach ($arr as $a) {

View File

@@ -269,11 +269,6 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
return $this->cancelReason;
}
public function isCanceled(): bool
{
return null !== $this->cancelReason;
}
public function getCenters(): ?iterable
{
return match ($this->getContext()) {

View File

@@ -18,14 +18,14 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'chill_calendar.cancel_reason')]
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_USER = 'CANCELEDBY_USER';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => true])]
private bool $active = true;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
private ?bool $active = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
private ?string $canceledBy = null;

View File

@@ -15,7 +15,7 @@ use Chill\CalendarBundle\Entity\CancelReason;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -28,14 +28,7 @@ class CancelReasonType extends AbstractType
->add('active', CheckboxType::class, [
'required' => false,
])
->add('canceledBy', ChoiceType::class, [
'choices' => [
'chill_calendar.canceled_by.user' => CancelReason::CANCELEDBY_USER,
'chill_calendar.canceled_by.person' => CancelReason::CANCELEDBY_PERSON,
'chill_calendar.canceled_by.other' => CancelReason::CANCELEDBY_OTHER,
],
'required' => true,
]);
->add('canceledBy', TextType::class);
}
public function configureOptions(OptionsResolver $resolver)

View File

@@ -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,
]);
}
}

View File

@@ -25,13 +25,6 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
if ($this->security->isGranted('ROLE_USER')) {
$menu->addChild('My calendar list', [
'route' => 'chill_calendar_calendar_list_my',
])
->setExtras([
'order' => 8,
'icon' => 'tasks',
]);
$menu->addChild('invite.list.title', [
'route' => 'chill_calendar_invitations_list_my',
])
->setExtras([
'order' => 9,

View File

@@ -21,7 +21,6 @@ namespace Chill\CalendarBundle\Messenger\Doctrine;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostRemoveEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
@@ -32,17 +31,6 @@ class CalendarEntityListener
{
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
{
if (!$calendar->preventEnqueueChanges) {
@@ -50,7 +38,7 @@ class CalendarEntityListener
new CalendarMessage(
$calendar,
CalendarMessage::CALENDAR_PERSIST,
$this->getAuthenticatedUser()
$this->security->getUser()
)
);
}
@@ -62,7 +50,7 @@ class CalendarEntityListener
$this->messageBus->dispatch(
new CalendarRemovedMessage(
$calendar,
$this->getAuthenticatedUser()
$this->security->getUser()
)
);
}
@@ -70,19 +58,12 @@ class CalendarEntityListener
public function postUpdate(Calendar $calendar, PostUpdateEventArgs $args): void
{
if ($calendar->getStatus() === $calendar::STATUS_CANCELED) {
$this->messageBus->dispatch(
new CalendarRemovedMessage(
$calendar,
$this->getAuthenticatedUser()
)
);
} elseif (!$calendar->preventEnqueueChanges) {
if (!$calendar->preventEnqueueChanges) {
$this->messageBus->dispatch(
new CalendarMessage(
$calendar,
CalendarMessage::CALENDAR_UPDATE,
$this->getAuthenticatedUser()
$this->security->getUser()
)
);
}

View File

@@ -70,8 +70,6 @@ class CalendarRemovedMessage
public function getRemoteId(): string
{
dump($this->remoteId);
return $this->remoteId;
}
}

View File

@@ -191,7 +191,6 @@ class CalendarRepository implements ObjectRepository
$qb->expr()->eq('c.mainUser', ':user'),
$qb->expr()->gte('c.startDate', ':startDate'),
$qb->expr()->lte('c.endDate', ':endDate'),
$qb->expr()->isNull('c.cancelReason'),
)
)
->setParameters([

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\Invite;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
@@ -42,7 +41,7 @@ class InviteRepository implements ObjectRepository
/**
* @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);
}
@@ -52,52 +51,6 @@ class InviteRepository implements ObjectRepository
return $this->entityRepository->findOneBy($criteria);
}
/**
* Find accepted invites for a user within a date range.
*
* @return array|Invite[]
*/
public function findAcceptedInvitesByUserAndDateRange(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to): array
{
return $this->buildAcceptedInviteByUserAndDateRangeQuery($user, $from, $to)
->getQuery()
->getResult();
}
/**
* Count accepted invites for a user within a date range.
*/
public function countAcceptedInvitesByUserAndDateRange(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to): int
{
return $this->buildAcceptedInviteByUserAndDateRangeQuery($user, $from, $to)
->select('COUNT(c)')
->getQuery()
->getSingleScalarResult();
}
public function buildAcceptedInviteByUserAndDateRangeQuery(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to)
{
$qb = $this->entityRepository->createQueryBuilder('i');
return $qb
->join('i.calendar', 'c')
->where(
$qb->expr()->andX(
$qb->expr()->eq('i.user', ':user'),
$qb->expr()->eq('i.status', ':status'),
$qb->expr()->gte('c.startDate', ':startDate'),
$qb->expr()->lte('c.endDate', ':endDate'),
$qb->expr()->isNull('c.cancelReason')
)
)
->setParameters([
'user' => $user,
'status' => Invite::ACCEPTED,
'startDate' => $from,
'endDate' => $to,
]);
}
public function getClassName(): string
{
return Invite::class;

View File

@@ -1,6 +1,5 @@
services:
Chill\CalendarBundle\Controller\:
autowire: true
autoconfigure: true
resource: '../../../Controller'
tags: ['controller.service_arguments']

View File

@@ -108,12 +108,9 @@
{{ formatDate(event.endStr, "time") }}:
{{ event.extendedProps.locationName }}</b
>
<a
:href="calendarLink(event.id)"
v-else-if="event.extendedProps.is === 'local'"
>
<b>{{ event.title }}</b>
</a>
<b v-else-if="event.extendedProps.is === 'local'">{{
event.title
}}</b>
<b v-else>no 'is'</b>
<a
v-if="event.extendedProps.is === 'range'"
@@ -489,12 +486,6 @@ function copyWeek() {
});
}
const calendarLink = (calendarId: string) => {
const idStr = calendarId.match(/_(\d+)$/)?.[1];
return `/fr/calendar/calendar/${idStr}/edit`;
};
onMounted(() => {
copyFromWeek.value = dateToISO(getMonday(0));
copyToWeek.value = dateToISO(getMonday(1));

View File

@@ -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">
<div class="item-row main">
<div class="item-col">
<div class="wrap-header">
<div class="wl-row">
{% if calendar.status == 'canceled' %}
<div class="badge rounded-pill bg-danger">
<span>{{ 'chill_calendar.canceled'|trans }}: </span>
<span>{{ calendar.cancelReason.name|localize_translatable_string }}</span>
</div>
{% endif %}
</div>
{% if calendarItems|length > 0 %}
<div class="flex-table list-records context-accompanyingCourse">
{% for calendar in calendarItems %}
<div class="item-bloc">
<div class="item-row main">
<div class="item-col">
<div class="wrap-header">
<div class="wl-row">
<div class="wl-col title">
<p class="date-label">
{% if calendar.status == 'canceled' %}
<del>
{% endif %}
{% 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;">
<span class="badge bg-primary">
@@ -25,9 +19,6 @@
</span>
</a>
{% endif %}
{% if calendar.status == 'canceled' %}
<del>
{% endif %}
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
{{ calendar.startDate|format_datetime('short', 'short') }}
- {{ calendar.endDate|format_datetime('short', 'short') }}
@@ -35,46 +26,44 @@
{{ calendar.startDate|format_datetime('short', 'short') }}
- {{ calendar.endDate|format_datetime('none', 'short') }}
{% endif %}
{% if calendar.status == 'canceled' %}
</del>
{% endif %}
</p>
<div class="duration short-message">
<i class="fa fa-fw fa-hourglass-end"></i>
{{ calendar.duration|date('%H:%I') }}
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
<!-- no sms will be sent -->
{% else %}
{% if calendar.smsStatus == 'sms_sent' %}
<span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
<i class="fa fa-check "></i>
<i class="fa fa-envelope "></i>
</span>
{% else %}
<span title="{{ 'Will send SMS'|trans }}" class="badge bg-info">
<i class="fa fa-envelope "></i>
<i class="fa fa-hourglass-end "></i>
</span>
<div class="duration short-message">
<i class="fa fa-fw fa-hourglass-end"></i>
{{ calendar.duration|date('%H:%I') }}
{% if false == calendar.sendSMS or null == calendar.sendSMS %}
<!-- no sms will be send -->
{% else %}
{% if calendar.smsStatus == 'sms_sent' %}
<span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
<i class="fa fa-check "></i>
<i class="fa fa-envelope "></i>
</span>
{% else %}
<span title="{{ 'Will send SMS'|trans }}" class="badge bg-info">
<i class="fa fa-envelope "></i>
<i class="fa fa-hourglass-end "></i>
</span>
{% endif %}
{% endif %}
</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 %}
</ul>
</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 %}
</ul>
</div>
</div>
</div>
{% if calendar.comment.comment is not empty
{% if calendar.comment.comment is not empty
or calendar.users|length > 0
or calendar.thirdParties|length > 0
or calendar.users|length > 0 %}
@@ -87,133 +76,131 @@
} %}
</div>
</div>
{% 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>
</div>
{% 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>
</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 %}
{% 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) %}
<li>
<div invite-answer data-status="{{ invite.status|e('html_attr') }}"
@@ -226,18 +213,12 @@
class="btn btn-show "></a>
</li>
{% endif %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) and calendar.status is not constant('STATUS_CANCELED', calendar) %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { 'id': calendar.id }) }}"
class="btn btn-update "></a>
</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 %}
{% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_delete', { 'id': calendar.id } ) }}"
@@ -246,8 +227,14 @@
{% endif %}
</ul>
</div>
</div>
{% endfor %}
{% if calendarItems|length < paginator.getTotalItems %}
{{ chill_pagination(paginator) }}
{% endif %}
</div>
</div>
{% endif %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -34,18 +34,7 @@
{% endif %}
</p>
{% else %}
{% if calendarItems|length > 0 %}
<div class="flex-table list-records context-accompanyingCourse">
{% for calendar in calendarItems %}
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}
{% endfor %}
</div>
{% if calendarItems|length < paginator.getTotalItems %}
{{ chill_pagination(paginator) }}
{% endif %}
{% endif %}
{{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}
{% endif %}
<ul class="record_actions sticky-form-buttons">

View File

@@ -33,17 +33,7 @@
{% endif %}
</p>
{% else %}
{% if calendarItems|length > 0 %}
<div class="flex-table list-records context-person">
{% for calendar in calendarItems %}
{{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}
{% endfor %}
</div>
{% if calendarItems|length < paginator.getTotalItems %}
{{ chill_pagination(paginator) }}
{% endif %}
{% endif %}
{{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}
{% endif %}
<ul class="record_actions sticky-form-buttons">

View File

@@ -5,7 +5,7 @@
{% block table_entities_thead_tr %}
<th>{{ 'Id'|trans }}</th>
<th>{{ 'Name'|trans }}</th>
<th>{{ 'Canceled by'|trans }}</th>
<th>{{ 'canceledBy'|trans }}</th>
<th>{{ 'active'|trans }}</th>
<th>&nbsp;</th>
{% endblock %}
@@ -40,4 +40,4 @@
</li>
{% endblock %}
{% endembed %}
{% endblock %}
{% endblock %}

View File

@@ -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 %}

View File

@@ -19,7 +19,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Service\ShortMessageNotification;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CancelReason;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\Notifier\Message\SmsMessage;
@@ -58,7 +57,7 @@ class DefaultShortMessageForCalendarBuilder implements ShortMessageForCalendarBu
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
$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(
$this->phoneUtil->format($person->getMobilenumber(), PhoneNumberFormat::E164),
$this->engine->render('@ChillCalendar/CalendarShortMessage/short_message_canceled.txt.twig', ['calendar' => $calendar]),

View File

@@ -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);
}
}

View File

@@ -31,7 +31,8 @@ Will send SMS: Un SMS de rappel sera envoyé
Will not send SMS: Aucun SMS de rappel ne sera 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
crud:
@@ -43,14 +44,6 @@ crud:
title_edit: Modifier le motif d'annulation
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
form:
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é
pending: En attente
tentative: Accepté provisoirement
list:
none: Il n'y aucun invitation
title: Mes invitations
# exports
Exports of calendar: Exports des rendez-vous

View File

@@ -20,9 +20,4 @@ use Doctrine\Persistence\ObjectRepository;
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
{
public function countByEntity(string $entity): int;
/**
* @return array|DocGeneratorTemplate[]
*/
public function findByEntity(string $entity, ?int $start = 0, ?int $limit = 50): array;
}

View File

@@ -25,8 +25,6 @@ use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Contracts\Translation\TranslatorInterface;
// use Symfony\Component\Translation\LocaleSwitcher;
/**
* @see OnGenerationFailsTest for test suite
*/
@@ -42,7 +40,6 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
private StoredObjectRepositoryInterface $storedObjectRepository,
private TranslatorInterface $translator,
private UserRepositoryInterface $userRepository,
// private LocaleSwitcher $localeSwitcher,
) {}
public static function getSubscribedEvents()
@@ -121,25 +118,6 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
return;
}
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
/*
$this->localeSwitcher->runWithLocale($creator->getLocale(), function () use ($message, $errors, $template, $creator) {
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
->context([
'errors' => $errors,
'template' => $template,
'creator' => $creator,
'stored_object_id' => $message->getDestinationStoredObjectId(),
]);
$this->mailer->send($email);
});
*/
// Current implementation:
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))

View File

@@ -27,8 +27,6 @@ use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
// use Symfony\Component\Translation\LocaleSwitcher;
/**
* Handle the request of document generation.
*/
@@ -48,7 +46,6 @@ class RequestGenerationHandler implements MessageHandlerInterface
private readonly MailerInterface $mailer,
private readonly TranslatorInterface $translator,
private readonly StoredObjectManagerInterface $storedObjectManager,
// private readonly LocaleSwitcher $localeSwitcher,
) {}
public function __invoke(RequestGenerationMessage $message)
@@ -125,30 +122,6 @@ class RequestGenerationHandler implements MessageHandlerInterface
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
{
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
// Note: This method sends emails to admin addresses, not user addresses, so locale switching may not be needed
/*
$this->localeSwitcher->runWithLocale('fr', function () use ($destinationStoredObject, $message) {
// Get the content of the document
$content = $this->storedObjectManager->read($destinationStoredObject);
$filename = $destinationStoredObject->getFilename();
$contentType = $destinationStoredObject->getType();
// Create the email with the document as an attachment
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
->context([
'filename' => $filename,
])
->subject($this->translator->trans('docgen.data_dump_email.subject'))
->attach($content, $filename, $contentType);
$this->mailer->send($email);
});
*/
// Current implementation:
// Get the content of the document
$content = $this->storedObjectManager->read($destinationStoredObject);
$filename = $destinationStoredObject->getFilename();

View File

@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
return new JsonResponse(
$this->serializer->serialize(
new Collection(array_values($items->toArray()), $paginator),
new Collection($items, $paginator),
'json',
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
),

View File

@@ -23,14 +23,10 @@ use Random\RandomException;
* Store each version of StoredObject's.
*
* 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\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_filename', columns: ['filename'], options: ['where' => '(id > 0)'])]
class StoredObjectVersion implements TrackCreationInterface
{
use TrackCreationTrait;

View File

@@ -17,6 +17,7 @@ use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\EntityRepository;
@@ -29,7 +30,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class PersonDocumentType extends AbstractType
{
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag) {}
public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly ScopeResolverDispatcher $scopeResolverDispatcher, private readonly ParameterBagInterface $parameterBag, private readonly CenterResolverDispatcher $centerResolverDispatcher) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@@ -56,8 +57,8 @@ class PersonDocumentType extends AbstractType
if ($isScopeConcerned && $this->parameterBag->get('chill_main')['acl']['form_show_scopes']) {
$builder->add('scope', ScopePickerType::class, [
'center' => $this->centerResolverDispatcher->resolveCenter($document),
'role' => $options['role'],
'subject' => $document,
]);
}
}

View File

@@ -36,18 +36,6 @@ export interface GenericDocForAccompanyingPeriod extends GenericDoc {
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 {
html: string;
}
@@ -56,33 +44,28 @@ export interface GenericDocForAccompanyingCourseDocument
extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}
export interface GenericDocForAccompanyingCourseActivityDocument
extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_activity_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}
export interface GenericDocForAccompanyingCourseCalendarDocument
extends GenericDocForAccompanyingPeriod {
key: "accompanying_course_calendar_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}
export interface GenericDocForAccompanyingCoursePersonDocument
extends GenericDocForAccompanyingPeriod {
key: "person_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
extends GenericDocForAccompanyingPeriod {
key: "accompanying_period_work_evaluation_document";
metadata: BaseMetadataWithHtml;
storedObject: StoredObject;
}

View File

@@ -3,9 +3,9 @@ import {
StoredObject,
StoredObjectPointInTime,
StoredObjectVersionWithPointInTime,
} from "ChillDocStoreAssets/types";
} from "./../../../types";
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 RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";

View File

@@ -40,10 +40,6 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
$storedObject->registerVersion();
}
// remove one version in the history
$v5 = $storedObject->getVersions()->get(5);
$storedObject->removeVersion($v5);
$security = $this->prophesize(Security::class);
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
->willReturn(true)
@@ -57,7 +53,6 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
self::assertEquals($response->getStatusCode(), 200);
self::assertIsArray($body);
self::assertArrayHasKey('results', $body);
self::assertIsList($body['results']);
self::assertCount(10, $body['results']);
}

View File

@@ -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');
}
}

View File

@@ -246,7 +246,7 @@ final class EventController extends AbstractController
'class' => Center::class,
'choices' => $centers,
'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, [
'label' => 'Next step',

View File

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

View File

@@ -64,7 +64,7 @@ CHILL_EVENT_PARTICIPATION_SEE_DETAILS: Voir le détail d'une participation
# TODO check place to put this
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
past: passé
@@ -151,7 +151,7 @@ event:
filter:
event_types: Par types d'événement
event_dates: Par date d'événement
center: Par territoire
center: Par centre
by_responsable: Par responsable
pick_responsable: Filtrer par responsables
budget:
@@ -188,7 +188,7 @@ event_id: Identifiant
event_name: Nom
event_date: Date
event_type: Type d'évenement
event_center: Territoire
event_center: Centre
event_moderator: Responsable
event_participants_count: Nombre de participants
event_location: Localisation

View File

@@ -15,7 +15,6 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\NotificationFlagManager;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use libphonenumber\PhoneNumber;
use Symfony\Component\Validator\Constraints as Assert;
final class UpdateProfileCommand
{
@@ -24,13 +23,11 @@ final class UpdateProfileCommand
public function __construct(
#[PhonenumberConstraint]
public ?PhoneNumber $phonenumber,
#[Assert\Choice(choices: ['fr', 'nl'], message: 'Locale must be either "fr" or "nl"')]
public string $locale = 'fr',
) {}
public static function create(User $user, NotificationFlagManager $flagManager): self
{
$updateProfileCommand = new self($user->getPhonenumber(), $user->getLocale());
$updateProfileCommand = new self($user->getPhonenumber());
foreach ($flagManager->getAllNotificationFlagProviders() as $provider) {
$updateProfileCommand->setNotificationFlag(

View File

@@ -18,7 +18,6 @@ final readonly class UpdateProfileCommandHandler
public function updateProfile(User $user, UpdateProfileCommand $command): void
{
$user->setPhonenumber($command->phonenumber);
$user->setLocale($command->locale);
foreach ($command->notificationFlags as $flag => $values) {
$user->setNotificationImmediately($flag, $values['immediate_email']);

View File

@@ -102,6 +102,7 @@ class CRUDController extends AbstractController
Resolver::class => Resolver::class,
SerializerInterface::class => SerializerInterface::class,
FilterOrderHelperFactoryInterface::class => FilterOrderHelperFactoryInterface::class,
ManagerRegistry::class => ManagerRegistry::class,
]
);
}
@@ -673,7 +674,7 @@ class CRUDController extends AbstractController
protected function getManagerRegistry(): ManagerRegistry
{
return $this->container->get('doctrine');
return $this->container->get(ManagerRegistry::class);
}
/**

View File

@@ -334,7 +334,7 @@ class ChillImportUsersCommand extends Command
protected function loadUsers()
{
$reader = Reader::from($this->tempInput->getArgument('csvfile'));
$reader = Reader::createFromPath($this->tempInput->getArgument('csvfile'));
$reader->setHeaderOffset(0);
foreach ($reader->getRecords() as $line => $r) {
@@ -362,7 +362,7 @@ class ChillImportUsersCommand extends Command
protected function prepareGroupingCenters()
{
$reader = Reader::from($this->tempInput->getOption('grouping-centers'));
$reader = Reader::createFromPath($this->tempInput->getOption('grouping-centers'));
$reader->setHeaderOffset(0);
foreach ($reader->getRecords() as $r) {
@@ -378,7 +378,7 @@ class ChillImportUsersCommand extends Command
protected function prepareWriter()
{
$this->output = $output = Writer::from($this->tempInput
$this->output = $output = Writer::createFromPath($this->tempInput
->getOption('csv-dump'), 'a+');
$output->insertOne([

View File

@@ -119,7 +119,7 @@ class ChillUserSendRenewPasswordCodeCommand extends Command
protected function getReader()
{
try {
$reader = Reader::from($this->input->getArgument('csvfile'));
$reader = Reader::createFromPath($this->input->getArgument('csvfile'));
} catch (\Exception $e) {
$this->logger->error('The csv file could not be read', [
'path' => $this->input->getArgument('csvfile'),

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -43,7 +43,7 @@ final readonly class UserExportController
$users = $this->userRepository->findAllAsArray($request->getLocale());
$csv = Writer::from('php://temp', 'r+');
$csv = Writer::createFromPath('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
@@ -104,7 +104,7 @@ final readonly class UserExportController
$userPermissions = $this->userRepository->findAllUserACLAsArray();
$csv = Writer::from('php://temp', 'r+');
$csv = Writer::createFromPath('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.'.$e),

View File

@@ -264,12 +264,11 @@ class WorkflowController extends AbstractController
{
$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);
$workflows = $this->entityWorkflowRepository->findBySubscriber(
$this->security->getUser(),
false,
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()

View File

@@ -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->load('services.yaml');
$loader->load('services/doctrine.yaml');
@@ -255,7 +250,6 @@ class ChillMainExtension extends Extension implements
'name' => $config['installation_name'], ],
'available_languages' => $config['available_languages'],
'add_address' => $config['add_address'],
'chill_main_config' => $config,
],
'form_themes' => ['@ChillMain/Form/fields.html.twig'],
];

View File

@@ -168,20 +168,6 @@ class Configuration implements ConfigurationInterface
->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')
->canBeEnabled()
->canBeUnset()

View File

@@ -128,12 +128,6 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
private array $notificationFlags = [];
/**
* User's preferred locale.
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 5, nullable: false, options: ['default' => 'fr'])]
private string $locale = 'fr';
/**
* User constructor.
*/
@@ -722,14 +716,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
public function getLocale(): string
{
return $this->locale;
}
public function setLocale(string $locale): self
{
$this->locale = $locale;
return $this;
return 'fr';
}
#[Assert\Callback]

View File

@@ -123,7 +123,7 @@ class EntityWorkflowStep
/**
* @var Collection<int, EntityWorkflowStepHold>
*/
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class, cascade: ['remove'])]
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
private Collection $holdsOnStep;
/**

View File

@@ -16,7 +16,6 @@ use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
@@ -33,84 +32,65 @@ use Symfony\Component\Security\Core\Security;
* Allow to pick amongst available scope for the current
* user.
*
* Options:
* - `role`: string, the role to check permissions for
* - Either `subject`: object, entity to resolve centers from
* - Or `center`: Center|array|null, the center(s) to check
* options :
*
* - `center`: the center of the entity
* - `role` : the role of the user
*/
class ScopePickerType extends AbstractType
{
public function __construct(
private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly AuthorizationHelperInterface $authorizationHelper,
private readonly Security $security,
private readonly CenterResolverManagerInterface $centerResolverManager,
private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Compute centers from subject
$centers = $options['center'] ?? null;
if (null === $centers && isset($options['subject'])) {
$centers = $this->centerResolverManager->resolveCenters($options['subject']);
}
if (null === $centers) {
throw new \RuntimeException('Either "center" or "subject" must be provided');
}
$reachableScopes = array_values(
$items = array_values(
array_filter(
$this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
$options['role'],
$centers
$options['center']
),
static fn (Scope $s) => $s->isActive()
)
);
$builder->setAttribute('reachable_scopes_count', count($reachableScopes));
if (0 === count($reachableScopes)) {
$builder->setAttribute('has_scopes', false);
return;
if (0 === \count($items)) {
throw new \RuntimeException('no scopes are reachable. This form should not be shown to user');
}
$builder->setAttribute('has_scopes', true);
if (1 !== count($reachableScopes)) {
if (1 !== \count($items)) {
$builder->add('scope', EntityType::class, [
'class' => Scope::class,
'placeholder' => 'Choose the circle',
'choice_label' => fn (Scope $c) => $this->translatableStringHelper->localize($c->getName()),
'choices' => $reachableScopes,
'choices' => $items,
]);
$builder->setDataMapper(new ScopePickerDataMapper());
} else {
$builder->add('scope', HiddenType::class, [
'data' => $reachableScopes[0]->getId(),
'data' => $items[0]->getId(),
]);
$builder->setDataMapper(new ScopePickerDataMapper($reachableScopes[0]));
$builder->setDataMapper(new ScopePickerDataMapper($items[0]));
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['fullWidth'] = true;
// display of label is handled by the EntityType
$view->vars['label'] = false;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
// create `center` option
->setRequired('center')
->setAllowedTypes('center', [Center::class, 'array', 'null'])
// create ``role` option
->setRequired('role')
->setAllowedTypes('role', ['string'])
->setDefined('subject')
->setAllowedTypes('subject', ['object'])
->setDefined('center')
->setAllowedTypes('center', [Center::class, 'array', 'null']);
->setAllowedTypes('role', ['string']);
}
}

View File

@@ -1,43 +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\MainBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Intl\Languages;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserLocaleType extends AbstractType
{
public function __construct(private readonly array $availableLanguages) {}
public function configureOptions(OptionsResolver $resolver): void
{
$choices = [];
foreach ($this->availableLanguages as $languageCode) {
$choices[Languages::getName($languageCode)] = $languageCode;
}
$resolver->setDefaults([
'choices' => $choices,
'placeholder' => 'user.locale.placeholder',
'required' => true,
'label' => 'user.locale.label',
'help' => 'user.locale.help',
]);
}
public function getParent(): string
{
return ChoiceType::class;
}
}

View File

@@ -14,7 +14,6 @@ namespace Chill\MainBundle\Form;
use Chill\MainBundle\Action\User\UpdateProfile\UpdateProfileCommand;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\NotificationFlagsType;
use Chill\MainBundle\Form\Type\UserLocaleType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -27,7 +26,6 @@ class UpdateProfileType extends AbstractType
->add('phonenumber', ChillPhoneNumberType::class, [
'required' => false,
])
->add('locale', UserLocaleType::class)
->add('notificationFlags', NotificationFlagsType::class)
;
}

View File

@@ -18,7 +18,6 @@ use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\PickCivilityType;
use Chill\MainBundle\Repository\UserJobRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
@@ -37,7 +36,7 @@ use Symfony\Component\Validator\Constraints\Regex;
class UserType extends AbstractType
{
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, protected ParameterBagInterface $parameterBag, private readonly UserJobRepository $userJobRepository) {}
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, protected ParameterBagInterface $parameterBag) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@@ -81,7 +80,12 @@ class UserType extends AbstractType
'placeholder' => 'choose a job',
'class' => UserJob::class,
'choice_label' => fn (UserJob $c) => $this->translatableStringHelper->localize($c->getLabel()),
'choices' => $this->loadAndSortUserJobs(),
'query_builder' => static function (EntityRepository $er) {
$qb = $er->createQueryBuilder('uj');
$qb->where('uj.active = TRUE');
return $qb;
},
])
->add('mainLocation', EntityType::class, [
'label' => 'Main location',
@@ -92,7 +96,6 @@ class UserType extends AbstractType
'query_builder' => static function (EntityRepository $er) {
$qb = $er->createQueryBuilder('l');
$qb->orderBy('l.locationType');
$qb->orderBy('l.name', 'ASC');
$qb->where('l.availableForUsers = TRUE');
return $qb;
@@ -152,20 +155,6 @@ class UserType extends AbstractType
}
}
private function loadAndSortUserJobs(): array
{
$items = $this->userJobRepository->findBy(['active' => true]);
usort(
$items,
fn ($a, $b) => mb_strtolower((string) $this->translatableStringHelper->localize($a->getLabel()))
<=>
mb_strtolower((string) $this->translatableStringHelper->localize($b->getLabel()))
);
return $items;
}
/**
* @param OptionsResolverInterface $resolver
*/

View File

@@ -53,16 +53,11 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
public function run(array $lastExecutionData): ?array
{
$now = $this->clock->now();
if (isset($lastExecutionData['last_execution'])) {
$lastExecution = \DateTimeImmutable::createFromFormat(
\DateTimeImmutable::ATOM,
$lastExecutionData['last_execution']
);
if (false === $lastExecution) {
$lastExecution = $now->sub(new \DateInterval('P1D'));
}
} else {
$lastExecution = $now->sub(new \DateInterval('P1D'));
}
@@ -101,7 +96,7 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
]);
return [
'last_execution' => $now->format(\DateTimeInterface::ATOM),
'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
];
}
}

View File

@@ -24,8 +24,6 @@ use Symfony\Component\Mime\Email;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
// use Symfony\Component\Translation\LocaleSwitcher;
readonly class NotificationMailer
{
public function __construct(
@@ -33,7 +31,6 @@ readonly class NotificationMailer
private LoggerInterface $logger,
private MessageBusInterface $messageBus,
private TranslatorInterface $translator,
// private LocaleSwitcher $localeSwitcher,
) {}
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
@@ -59,7 +56,7 @@ readonly class NotificationMailer
$email
->to($dest->getEmail())
->subject('Re: '.$comment->getNotification()->getTitle())
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.md.twig')
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig')
->context([
'comment' => $comment,
'dest' => $dest,
@@ -140,53 +137,13 @@ readonly class NotificationMailer
return;
}
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
/*
$this->localeSwitcher->runWithLocale($addressee->getLocale(), function () use ($notification, $addressee) {
if ($notification->isSystem()) {
$email = new Email();
$email->text($notification->getMessage());
} else {
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
->context([
'notification' => $notification,
'dest' => $addressee,
]);
}
$email
->subject($notification->getTitle())
->to($addressee->getEmail());
try {
$this->mailer->send($email);
$this->logger->info('[NotificationMailer] Email sent successfully', [
'notification_id' => $notification->getId(),
'addressee_email' => $addressee->getEmail(),
'locale' => $addressee->getLocale(),
]);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] Could not send an email notification', [
'to' => $addressee->getEmail(),
'notification_id' => $notification->getId(),
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
]);
throw $e;
}
});
*/
// Current implementation:
if ($notification->isSystem()) {
$email = new Email();
$email->text($notification->getMessage());
} else {
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig')
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
->context([
'notification' => $notification,
'dest' => $addressee,
@@ -225,43 +182,9 @@ readonly class NotificationMailer
return;
}
// Implementation with LocaleSwitcher (commented out - to be activated after migration to sf7.2):
/*
$this->localeSwitcher->runWithLocale($user->getLocale(), function () use ($user, $notifications) {
$email = new TemplatedEmail();
$email
->htmlTemplate('@ChillMain/Notification/email_daily_digest.md.twig')
->context([
'user' => $user,
'notifications' => $notifications,
'notification_count' => count($notifications),
])
->subject($this->translator->trans('notification.Daily Notification Digest'))
->to($user->getEmail());
try {
$this->mailer->send($email);
$this->logger->info('[NotificationMailer] Daily digest email sent successfully', [
'user_email' => $user->getEmail(),
'notification_count' => count($notifications),
'locale' => $user->getLocale(),
]);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] Could not send daily digest email', [
'to' => $user->getEmail(),
'notification_count' => count($notifications),
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
]);
throw $e;
}
});
*/
// Current implementation:
$email = new TemplatedEmail();
$email
->htmlTemplate('@ChillMain/Notification/email_daily_digest.md.twig')
->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
->context([
'user' => $user,
'notifications' => $notifications,
@@ -299,7 +222,7 @@ readonly class NotificationMailer
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.md.twig')
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
->context([
'notification' => $notification,
'dest' => $emailAddress,

View File

@@ -17,7 +17,7 @@ use Symfony\Component\Routing\RouterInterface;
/**
* Create paginator instances.
*/
class PaginatorFactory implements PaginatorFactoryInterface
final readonly class PaginatorFactory implements PaginatorFactoryInterface
{
final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
@@ -29,16 +29,16 @@ class PaginatorFactory implements PaginatorFactoryInterface
/**
* the request stack.
*/
private readonly RequestStack $requestStack,
private RequestStack $requestStack,
/**
* the router and generator for url.
*/
private readonly RouterInterface $router,
private RouterInterface $router,
/**
* the default item per page. This may be overriden by
* the request or inside the paginator.
*/
private readonly int $itemPerPage = 20,
private int $itemPerPage = 20,
) {}
/**

View File

@@ -14,13 +14,14 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Search\SearchApiQuery;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;
final readonly class AddressReferenceRepository implements ObjectRepository
final readonly class AddressReferenceRepository implements AddressReferenceRepositoryInterface
{
private EntityManagerInterface $entityManager;
@@ -65,6 +66,121 @@ final readonly class AddressReferenceRepository implements ObjectRepository
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 $offset

View File

@@ -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;
}

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