mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-03 18:58:24 +00:00 
			
		
		
		
	Compare commits
	
		
			35 Commits
		
	
	
		
			385-invita
			...
			280-add-mi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 980036fd3d | |||
| a70f8a0579 | |||
| bf38ec22c9 | |||
| 9c2abb2dfa | |||
| 94744b9542 | |||
| f42bb498e4 | |||
| 01889ac671 | |||
| 62e5842311 | |||
| 
						
						
							
						
						8ad6f397a8
	
				 | 
					
					
						|||
| d713704633 | |||
| b1fa9242a0 | |||
| 6ac554f93a | |||
| 372d8e5825 | |||
| 10f05e5559 | |||
| ddb2a65419 | |||
| 8d40a8089f | |||
| e1bf4a24d2 | |||
| 208a378185 | |||
| 9089c8959b | |||
| 
						
						
							
						
						1b9b581c31
	
				 | 
					
					
						|||
| aa1abe4c88 | |||
| d82c9cc9a7 | |||
| a7e3b1c5d2 | |||
| 84cf11933d | |||
| bc2fbee5c6 | |||
| ebd10ca522 | |||
| 
						
						
							
						
						d3a31be412
	
				 | 
					
					
						|||
| 
						
						
							
						
						d159a82f88
	
				 | 
					
					
						|||
| 
						
						
							
						
						c2d9c73fd4
	
				 | 
					
					
						|||
| 0d6d15fcf7 | |||
| 
						
						
							
						
						f9ad96c78b
	
				 | 
					
					
						|||
| 
						
						
							
						
						fcc9529a20
	
				 | 
					
					
						|||
| 
						
						
							
						
						955cb817c4
	
				 | 
					
					
						|||
| 823f9546b9 | |||
| be39fa16e7 | 
							
								
								
									
										7
									
								
								.changes/unreleased/DX-20251027-150053.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.changes/unreleased/DX-20251027-150053.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					kind: DX
 | 
				
			||||||
 | 
					body: |
 | 
				
			||||||
 | 
					    Send notifications log to dedicated channel, if it exists
 | 
				
			||||||
 | 
					time: 2025-10-27T15:00:53.309372316+01:00
 | 
				
			||||||
 | 
					custom:
 | 
				
			||||||
 | 
					    Issue: ""
 | 
				
			||||||
 | 
					    SchemaChange: No schema change
 | 
				
			||||||
							
								
								
									
										6
									
								
								.changes/unreleased/DX-20251030-123732.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/DX-20251030-123732.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					kind: DX
 | 
				
			||||||
 | 
					body: Add missing fixtures for proper loading of AccompanyingPeriods
 | 
				
			||||||
 | 
					time: 2025-10-30T12:37:32.824593456+01:00
 | 
				
			||||||
 | 
					custom:
 | 
				
			||||||
 | 
					    Issue: "280"
 | 
				
			||||||
 | 
					    SchemaChange: No schema change
 | 
				
			||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
kind: Feature
 | 
					 | 
				
			||||||
body: Create invitation list in user menu
 | 
					 | 
				
			||||||
time: 2025-08-08T12:08:02.446361367+02:00
 | 
					 | 
				
			||||||
custom:
 | 
					 | 
				
			||||||
    Issue: "385"
 | 
					 | 
				
			||||||
    SchemaChange: No schema change
 | 
					 | 
				
			||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
kind: Fixed
 | 
					 | 
				
			||||||
body: Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
 | 
					 | 
				
			||||||
time: 2025-09-18T11:40:44.858533536+02:00
 | 
					 | 
				
			||||||
custom:
 | 
					 | 
				
			||||||
    Issue: "426"
 | 
					 | 
				
			||||||
    SchemaChange: No schema change
 | 
					 | 
				
			||||||
							
								
								
									
										13
									
								
								.changes/v4.5.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.changes/v4.5.0.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					## v4.5.0 - 2025-10-03
 | 
				
			||||||
 | 
					### Feature
 | 
				
			||||||
 | 
					* Only allow delete of attachment on workflows that are not final   
 | 
				
			||||||
 | 
					* Move up signature buttons on index workflow page for easier access   
 | 
				
			||||||
 | 
					* Filter out document from attachment list if it is the same as the workflow document   
 | 
				
			||||||
 | 
					* Block edition on attached document on workflow, if the workflow is finalized or sent external   
 | 
				
			||||||
 | 
					* Convert workflow's attached document to pdf while sending them external   
 | 
				
			||||||
 | 
					* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition   
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance   
 | 
				
			||||||
 | 
					* Fix permissions on storedObject which are subject by a workflow   
 | 
				
			||||||
 | 
					### DX
 | 
				
			||||||
 | 
					* Introduce a WaitingScreen component to display a waiting screen   
 | 
				
			||||||
							
								
								
									
										4
									
								
								.changes/v4.5.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.changes/v4.5.1.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					## v4.5.1 - 2025-10-03
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					* Add missing javascript dependency   
 | 
				
			||||||
 | 
					* Add exception handling for conversion of attachment on sending external, when documens are already in pdf   
 | 
				
			||||||
							
								
								
									
										14
									
								
								.changes/v4.6.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.changes/v4.6.0.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					## v4.6.0 - 2025-10-15
 | 
				
			||||||
 | 
					### Feature
 | 
				
			||||||
 | 
					* ([#423](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/423)) Create environment banner that can be activated and configured depending on the image deployed   
 | 
				
			||||||
 | 
					* ([#394](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/394)) Only show active workflow on the page "my tracked workflow"   
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					* Fix loading of classLists in SocialIssuesAcc.vue, ensure elements are present   
 | 
				
			||||||
 | 
					* Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted   
 | 
				
			||||||
 | 
					* ([#434](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/434)) Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists   
 | 
				
			||||||
 | 
					* Fix loading of social issues and social actions within vue component   
 | 
				
			||||||
 | 
					* ([#446](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/446)) Add unique condition on stored object filename, with cleaning step on existing duplicate filenames   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  **Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
 | 
				
			||||||
 | 
					* [workflow] take permissions into account to delete the workflow attachment   
 | 
				
			||||||
 | 
					* ([#448](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/448)) Fix the execution of daily cronjob notification, when the previous last execution storage was invalid   
 | 
				
			||||||
							
								
								
									
										3
									
								
								.changes/v4.6.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v4.6.1.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					## v4.6.1 - 2025-10-27
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php   
 | 
				
			||||||
@@ -240,9 +240,6 @@ The tests are run from the project's root (not from the bundle's root).
 | 
				
			|||||||
# Run all tests
 | 
					# Run all tests
 | 
				
			||||||
vendor/bin/phpunit
 | 
					vendor/bin/phpunit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Run tests for a specific bundle
 | 
					 | 
				
			||||||
vendor/bin/phpunit --testsuite NameBundle
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Run a specific test file
 | 
					# Run a specific test file
 | 
				
			||||||
vendor/bin/phpunit path/to/TestFile.php
 | 
					vendor/bin/phpunit path/to/TestFile.php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -250,6 +247,9 @@ vendor/bin/phpunit path/to/TestFile.php
 | 
				
			|||||||
vendor/bin/phpunit --filter methodName path/to/TestFile.php
 | 
					vendor/bin/phpunit --filter methodName path/to/TestFile.php
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When writing tests, only test specific files. Do not run all tests or the full
 | 
				
			||||||
 | 
					test suite.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Test Structure
 | 
					#### Test Structure
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Tests are organized by bundle and follow the same structure as the bundle itself:
 | 
					Tests are organized by bundle and follow the same structure as the bundle itself:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -6,6 +6,44 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
 | 
				
			|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
 | 
					and is generated by [Changie](https://github.com/miniscruff/changie).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## v4.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   
 | 
				
			||||||
 | 
					* Add exception handling for conversion of attachment on sending external, when documens are already in pdf   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## v4.5.0 - 2025-10-03
 | 
				
			||||||
 | 
					### Feature
 | 
				
			||||||
 | 
					* Only allow delete of attachment on workflows that are not final   
 | 
				
			||||||
 | 
					* Move up signature buttons on index workflow page for easier access   
 | 
				
			||||||
 | 
					* Filter out document from attachment list if it is the same as the workflow document   
 | 
				
			||||||
 | 
					* Block edition on attached document on workflow, if the workflow is finalized or sent external   
 | 
				
			||||||
 | 
					* Convert workflow's attached document to pdf while sending them external   
 | 
				
			||||||
 | 
					* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition   
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance   
 | 
				
			||||||
 | 
					* Fix permissions on storedObject which are subject by a workflow   
 | 
				
			||||||
 | 
					### DX
 | 
				
			||||||
 | 
					* Introduce a WaitingScreen component to display a waiting screen   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## v4.4.2 - 2025-09-12
 | 
					## v4.4.2 - 2025-09-12
 | 
				
			||||||
### Fixed
 | 
					### Fixed
 | 
				
			||||||
* Fix document generation and workflow generation do not work on accompanying period work documents   
 | 
					* Fix document generation and workflow generation do not work on accompanying period work documents   
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,13 @@
 | 
				
			|||||||
chill_main:
 | 
					chill_main:
 | 
				
			||||||
    available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
 | 
					    available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
 | 
				
			||||||
    available_countries: ['BE', 'FR']
 | 
					    available_countries: ['BE', 'FR']
 | 
				
			||||||
 | 
					    top_banner:
 | 
				
			||||||
 | 
					        visible: false
 | 
				
			||||||
 | 
					        text:
 | 
				
			||||||
 | 
					            fr: 'Vous travaillez actuellement avec la version de PRÉ-PRODUCTION.'
 | 
				
			||||||
 | 
					            nl: 'Je werkt momenteel in de PRE-PRODUCTIE versie'
 | 
				
			||||||
 | 
					        color: '#353535'
 | 
				
			||||||
 | 
					        background_color: '#d8bb48'
 | 
				
			||||||
    notifications:
 | 
					    notifications:
 | 
				
			||||||
        from_email: '%env(resolve:NOTIFICATION_FROM_EMAIL)%'
 | 
					        from_email: '%env(resolve:NOTIFICATION_FROM_EMAIL)%'
 | 
				
			||||||
        from_name: '%env(resolve:NOTIFICATION_FROM_NAME)%'
 | 
					        from_name: '%env(resolve:NOTIFICATION_FROM_NAME)%'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,6 +55,7 @@
 | 
				
			|||||||
    "@tsconfig/node20": "^20.1.4",
 | 
					    "@tsconfig/node20": "^20.1.4",
 | 
				
			||||||
    "@types/dompurify": "^3.0.5",
 | 
					    "@types/dompurify": "^3.0.5",
 | 
				
			||||||
    "@types/leaflet": "^1.9.3",
 | 
					    "@types/leaflet": "^1.9.3",
 | 
				
			||||||
 | 
					    "@vueuse/core": "^13.9.0",
 | 
				
			||||||
    "bootstrap-icons": "^1.11.3",
 | 
					    "bootstrap-icons": "^1.11.3",
 | 
				
			||||||
    "dropzone": "^5.7.6",
 | 
					    "dropzone": "^5.7.6",
 | 
				
			||||||
    "es6-promise": "^4.2.8",
 | 
					    "es6-promise": "^4.2.8",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -90,7 +90,9 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function getFormDefaultData(): array
 | 
					    public function getFormDefaultData(): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return [];
 | 
					        return [
 | 
				
			||||||
 | 
					            'reasons' => [],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array
 | 
					    public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,6 +42,8 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
 | 
					    public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        error_log('alterQuery called with data: '.json_encode(array_keys($data)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // create a subquery for activity
 | 
					        // create a subquery for activity
 | 
				
			||||||
        $sqb = $qb->getEntityManager()->createQueryBuilder();
 | 
					        $sqb = $qb->getEntityManager()->createQueryBuilder();
 | 
				
			||||||
        $sqb->select('1')
 | 
					        $sqb->select('1')
 | 
				
			||||||
@@ -59,7 +61,6 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
				
			|||||||
        if (\in_array('activity', $qb->getAllAliases(), true)) {
 | 
					        if (\in_array('activity', $qb->getAllAliases(), true)) {
 | 
				
			||||||
            $sqb->andWhere('activity_person_having_activity.id = activity.id');
 | 
					            $sqb->andWhere('activity_person_having_activity.id = activity.id');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (isset($data['reasons']) && [] !== $data['reasons']) {
 | 
					        if (isset($data['reasons']) && [] !== $data['reasons']) {
 | 
				
			||||||
            // add clause activity reason
 | 
					            // add clause activity reason
 | 
				
			||||||
            $sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');
 | 
					            $sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');
 | 
				
			||||||
@@ -124,12 +125,38 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function normalizeFormData(array $formData): array
 | 
					    public function normalizeFormData(array $formData): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return ['date_from_rolling' => $formData['date_from_rolling']->normalize(), 'date_to_rolling' => $formData['date_to_rolling']->normalize()];
 | 
					        $normalized = [
 | 
				
			||||||
 | 
					            'date_from_rolling' => $formData['date_from_rolling']->normalize(),
 | 
				
			||||||
 | 
					            'date_to_rolling' => $formData['date_to_rolling']->normalize(),
 | 
				
			||||||
 | 
					            'reasons' => [],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isset($formData['reasons']) && [] !== $formData['reasons']) {
 | 
				
			||||||
 | 
					            $normalized['reasons'] = array_map(
 | 
				
			||||||
 | 
					                fn (ActivityReason $reason) => $reason->getId(),
 | 
				
			||||||
 | 
					                $formData['reasons']
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $normalized;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function denormalizeFormData(array $formData, int $fromVersion): array
 | 
					    public function denormalizeFormData(array $formData, int $fromVersion): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return ['date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']), 'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling'])];
 | 
					        $denormalized = [
 | 
				
			||||||
 | 
					            'date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']),
 | 
				
			||||||
 | 
					            'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling']),
 | 
				
			||||||
 | 
					            'reasons' => [],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isset($formData['reasons']) && [] !== $formData['reasons']) {
 | 
				
			||||||
 | 
					            $denormalized['reasons'] = array_map(
 | 
				
			||||||
 | 
					                fn ($id) => $this->activityReasonRepository->find($id),
 | 
				
			||||||
 | 
					                $formData['reasons']
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $denormalized;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getFormDefaultData(): array
 | 
					    public function getFormDefaultData(): array
 | 
				
			||||||
@@ -143,10 +170,12 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function describeAction($data, ExportGenerationContext $context): array
 | 
					    public function describeAction($data, ExportGenerationContext $context): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        $reasons = $data['reasons'] ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            [] === $data['reasons'] ?
 | 
					            [] === $reasons ?
 | 
				
			||||||
                'export.filter.person_between_dates.describe_action_with_no_subject'
 | 
					                'export.filter.activity.describe_action_with_no_subject'
 | 
				
			||||||
                : 'export.filter.person_between_dates.describe_action_with_subject',
 | 
					                : 'export.filter.activity.describe_action_with_subject',
 | 
				
			||||||
            [
 | 
					            [
 | 
				
			||||||
                'date_from' => $this->rollingDateConverter->convert($data['date_from_rolling']),
 | 
					                'date_from' => $this->rollingDateConverter->convert($data['date_from_rolling']),
 | 
				
			||||||
                'date_to' => $this->rollingDateConverter->convert($data['date_to_rolling']),
 | 
					                'date_to' => $this->rollingDateConverter->convert($data['date_to_rolling']),
 | 
				
			||||||
@@ -154,7 +183,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
				
			|||||||
                    ', ',
 | 
					                    ', ',
 | 
				
			||||||
                    array_map(
 | 
					                    array_map(
 | 
				
			||||||
                        fn (ActivityReason $r): string => '"'.$this->translatableStringHelper->localize($r->getName()).'"',
 | 
					                        fn (ActivityReason $r): string => '"'.$this->translatableStringHelper->localize($r->getName()).'"',
 | 
				
			||||||
                        $data['reasons']
 | 
					                        $reasons
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
@@ -168,6 +197,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function validateForm($data, ExecutionContextInterface $context): void
 | 
					    public function validateForm($data, ExecutionContextInterface $context): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        error_log('validateForm called with data: '.json_encode(array_keys($data)));
 | 
				
			||||||
        if ($this->rollingDateConverter->convert($data['date_from_rolling'])
 | 
					        if ($this->rollingDateConverter->convert($data['date_from_rolling'])
 | 
				
			||||||
            >= $this->rollingDateConverter->convert($data['date_to_rolling'])) {
 | 
					            >= $this->rollingDateConverter->convert($data['date_to_rolling'])) {
 | 
				
			||||||
            $context->buildViolation('export.filter.activity.person_between_dates.date mismatch')
 | 
					            $context->buildViolation('export.filter.activity.person_between_dates.date mismatch')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -136,8 +136,14 @@ export default {
 | 
				
			|||||||
            issueIsLoading: false,
 | 
					            issueIsLoading: false,
 | 
				
			||||||
            actionIsLoading: false,
 | 
					            actionIsLoading: false,
 | 
				
			||||||
            actionAreLoaded: false,
 | 
					            actionAreLoaded: false,
 | 
				
			||||||
            socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`,
 | 
					            socialIssuesClassList: {
 | 
				
			||||||
            socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`,
 | 
					                "col-form-label": true,
 | 
				
			||||||
 | 
					                required: false,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            socialActionsClassList: {
 | 
				
			||||||
 | 
					                "col-form-label": true,
 | 
				
			||||||
 | 
					                required: false,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
@@ -158,6 +164,21 @@ export default {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    mounted() {
 | 
					    mounted() {
 | 
				
			||||||
 | 
					        /* Load classNames after element is present */
 | 
				
			||||||
 | 
					        const socialActionsEl = document.querySelector(
 | 
				
			||||||
 | 
					            "input#chill_activitybundle_activity_socialActions",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        if (socialActionsEl && socialActionsEl.hasAttribute("required")) {
 | 
				
			||||||
 | 
					            this.socialActionsClassList.required = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const socialIssuesEl = document.querySelector(
 | 
				
			||||||
 | 
					            "input#chill_activitybundle_activity_socialIssues",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        if (socialIssuesEl && socialIssuesEl.hasAttribute("required")) {
 | 
				
			||||||
 | 
					            this.socialIssuesClassList.required = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /* Load other issues in multiselect */
 | 
					        /* Load other issues in multiselect */
 | 
				
			||||||
        this.issueIsLoading = true;
 | 
					        this.issueIsLoading = true;
 | 
				
			||||||
        this.actionAreLoaded = false;
 | 
					        this.actionAreLoaded = false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -266,7 +266,7 @@ class CalendarController extends AbstractController
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!$this->getUser() instanceof User) {
 | 
					        if (!$this->getUser() instanceof User) {
 | 
				
			||||||
            throw new UnauthorizedHttpException('you are not a user');
 | 
					            throw new UnauthorizedHttpException('you are not an user');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $view = '@ChillCalendar/Calendar/listByUser.html.twig';
 | 
					        $view = '@ChillCalendar/Calendar/listByUser.html.twig';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,58 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare(strict_types=1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 * Chill is a software for social workers
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * For the full copyright and license information, please view
 | 
					 | 
				
			||||||
 * the LICENSE file that was distributed with this source code.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Chill\CalendarBundle\Controller;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use Chill\CalendarBundle\Entity\Calendar;
 | 
					 | 
				
			||||||
use Chill\CalendarBundle\Repository\InviteRepository;
 | 
					 | 
				
			||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
 | 
					 | 
				
			||||||
use Chill\MainBundle\Entity\User;
 | 
					 | 
				
			||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
					 | 
				
			||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
					 | 
				
			||||||
use Symfony\Component\HttpFoundation\Request;
 | 
					 | 
				
			||||||
use Symfony\Component\HttpFoundation\Response;
 | 
					 | 
				
			||||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
 | 
					 | 
				
			||||||
use Symfony\Component\Routing\Annotation\Route;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class MyInvitationsController extends AbstractController
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public function __construct(private readonly InviteRepository $inviteRepository, private readonly PaginatorFactory $paginator, private readonly DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository) {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[Route(path: '/{_locale}/calendar/invitations/my', name: 'chill_calendar_invitations_list_my')]
 | 
					 | 
				
			||||||
    public function myInvitations(Request $request): Response
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $this->denyAccessUnlessGranted('ROLE_USER');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $user = $this->getUser();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!$user instanceof User) {
 | 
					 | 
				
			||||||
            throw new UnauthorizedHttpException('you are not a user');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $total = count($this->inviteRepository->findBy(['user' => $user]));
 | 
					 | 
				
			||||||
        $paginator = $this->paginator->create($total);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $invitations = $this->inviteRepository->findBy(
 | 
					 | 
				
			||||||
            ['user' => $user],
 | 
					 | 
				
			||||||
            ['createdAt' => 'DESC'],
 | 
					 | 
				
			||||||
            $paginator->getItemsPerPage(),
 | 
					 | 
				
			||||||
            $paginator->getCurrentPageFirstItemNumber()
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $view = '@ChillCalendar/Invitations/listByUser.html.twig';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $this->render($view, [
 | 
					 | 
				
			||||||
            'invitations' => $invitations,
 | 
					 | 
				
			||||||
            'paginator' => $paginator,
 | 
					 | 
				
			||||||
            'templates' => $this->docGeneratorTemplateRepository->findByEntity(Calendar::class),
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -30,13 +30,6 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
 | 
				
			|||||||
                    'order' => 9,
 | 
					                    'order' => 9,
 | 
				
			||||||
                    'icon' => 'tasks',
 | 
					                    'icon' => 'tasks',
 | 
				
			||||||
                ]);
 | 
					                ]);
 | 
				
			||||||
            $menu->addChild('My invitations list', [
 | 
					 | 
				
			||||||
                'route' => 'chill_calendar_invitations_list_my',
 | 
					 | 
				
			||||||
            ])
 | 
					 | 
				
			||||||
                ->setExtras([
 | 
					 | 
				
			||||||
                    'order' => 9,
 | 
					 | 
				
			||||||
                    'icon' => 'tasks',
 | 
					 | 
				
			||||||
                ]);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,7 @@ class InviteRepository implements ObjectRepository
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @return array|Invite[]
 | 
					     * @return array|Invite[]
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
 | 
					    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
 | 
					        return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,229 +1,240 @@
 | 
				
			|||||||
{# list used in context of person, accompanyingPeriod or user #}
 | 
					{# list used in context of person or accompanyingPeriod #}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="item-bloc">
 | 
					{% if calendarItems|length > 0 %}
 | 
				
			||||||
    <div class="item-row main">
 | 
					    <div class="flex-table list-records context-accompanyingCourse">
 | 
				
			||||||
        <div class="item-col">
 | 
					 | 
				
			||||||
            <div class="wrap-header">
 | 
					 | 
				
			||||||
                <div class="wl-row">
 | 
					 | 
				
			||||||
                    <div class="wl-col title">
 | 
					 | 
				
			||||||
                        <p class="date-label">
 | 
					 | 
				
			||||||
                            {% 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">
 | 
					 | 
				
			||||||
                                            <i class="fa fa-random"></i> {{ calendar.accompanyingPeriod.id }}
 | 
					 | 
				
			||||||
                                    </span>
 | 
					 | 
				
			||||||
                                </a>
 | 
					 | 
				
			||||||
                            {% endif %}
 | 
					 | 
				
			||||||
                            {% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
 | 
					 | 
				
			||||||
                                    {{ calendar.startDate|format_datetime('short', 'short') }}
 | 
					 | 
				
			||||||
                                    - {{ calendar.endDate|format_datetime('short', 'short') }}
 | 
					 | 
				
			||||||
                            {% else %}
 | 
					 | 
				
			||||||
                                {{ calendar.startDate|format_datetime('short', 'short') }}
 | 
					 | 
				
			||||||
                                    - {{ calendar.endDate|format_datetime('none', 'short') }}
 | 
					 | 
				
			||||||
                            {% endif %}
 | 
					 | 
				
			||||||
                        </p>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        <div class="duration short-message">
 | 
					        {% for calendar in calendarItems %}
 | 
				
			||||||
                            <i class="fa fa-fw fa-hourglass-end"></i>
 | 
					
 | 
				
			||||||
                            {{ calendar.duration|date('%H:%I') }}
 | 
					            <div class="item-bloc">
 | 
				
			||||||
                            {% if false == calendar.sendSMS or null == calendar.sendSMS %}
 | 
					                <div class="item-row main">
 | 
				
			||||||
                                <!-- no sms will be send -->
 | 
					                    <div class="item-col">
 | 
				
			||||||
                            {% else %}
 | 
					                        <div class="wrap-header">
 | 
				
			||||||
                                {% if calendar.smsStatus == 'sms_sent' %}
 | 
					                            <div class="wl-row">
 | 
				
			||||||
                                    <span title="{{ 'SMS already sent'|trans }}" class="badge bg-info">
 | 
					                                <div class="wl-col title">
 | 
				
			||||||
                                        <i class="fa fa-check "></i>
 | 
					                                    <p class="date-label">
 | 
				
			||||||
                                        <i class="fa fa-envelope "></i>
 | 
					                                        {% if context == 'person' and calendar.context == 'accompanying_period' %}
 | 
				
			||||||
                                    </span>
 | 
					                                            <a href="{{ chill_path_add_return_path('chill_person_accompanying_course_index', {'accompanying_period_id': calendar.accompanyingPeriod.id}) }}" style="text-decoration: none;">
 | 
				
			||||||
                                {% else %}
 | 
					                                                <span class="badge bg-primary">
 | 
				
			||||||
                                    <span title="{{ 'Will send SMS'|trans }}" class="badge bg-info">
 | 
					                                                        <i class="fa fa-random"></i> {{ calendar.accompanyingPeriod.id }}
 | 
				
			||||||
                                        <i class="fa fa-envelope "></i>
 | 
					                                                </span>
 | 
				
			||||||
                                        <i class="fa fa-hourglass-end "></i>
 | 
					                                            </a>
 | 
				
			||||||
                                    </span>
 | 
					                                        {% endif %}
 | 
				
			||||||
                                {% endif %}
 | 
					                                        {% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
 | 
				
			||||||
                            {% endif %}
 | 
					                                                {{ calendar.startDate|format_datetime('short', 'short') }}
 | 
				
			||||||
 | 
					                                                - {{ calendar.endDate|format_datetime('short', 'short') }}
 | 
				
			||||||
 | 
					                                        {% else %}
 | 
				
			||||||
 | 
					                                            {{ calendar.startDate|format_datetime('short', 'short') }}
 | 
				
			||||||
 | 
					                                                - {{ calendar.endDate|format_datetime('none', 'short') }}
 | 
				
			||||||
 | 
					                                        {% 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 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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    </div>
 | 
					                        <div class="item-col">
 | 
				
			||||||
                </div>
 | 
					                            <ul class="list-content">
 | 
				
			||||||
            </div>
 | 
					                                {% if calendar.mainUser is not empty %}
 | 
				
			||||||
 | 
					                                    <span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
 | 
				
			||||||
            <div class="item-col">
 | 
					 | 
				
			||||||
                <ul class="list-content">
 | 
					 | 
				
			||||||
                    {% if calendar.mainUser is not empty %}
 | 
					 | 
				
			||||||
                        <span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
 | 
					 | 
				
			||||||
                    {% endif %}
 | 
					 | 
				
			||||||
                </ul>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {% if calendar.comment.comment is not empty
 | 
					 | 
				
			||||||
        or calendar.users|length > 0
 | 
					 | 
				
			||||||
        or calendar.thirdParties|length > 0
 | 
					 | 
				
			||||||
        or calendar.users|length > 0 %}
 | 
					 | 
				
			||||||
        <div class="item-row details separator">
 | 
					 | 
				
			||||||
            <div class="item-col">
 | 
					 | 
				
			||||||
                {% include '@ChillActivity/Activity/concernedGroups.html.twig' with {
 | 
					 | 
				
			||||||
                    'context': calendar.context == 'person' ?  'calendar_person' : 'calendar_accompanyingCourse',
 | 
					 | 
				
			||||||
                    'render': 'wrap-list',
 | 
					 | 
				
			||||||
                    'entity': calendar
 | 
					 | 
				
			||||||
                } %}
 | 
					 | 
				
			||||||
            </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 %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <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 %}
 | 
					                                {% endif %}
 | 
				
			||||||
                            </ul>
 | 
					                            </ul>
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    {% endif %}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="item-row separator">
 | 
					                {% if calendar.comment.comment is not empty
 | 
				
			||||||
        <ul class="record_actions">
 | 
					                    or calendar.users|length > 0
 | 
				
			||||||
                {% if is_granted('CHILL_CALENDAR_DOC_EDIT', calendar) %}
 | 
					                    or calendar.thirdParties|length > 0
 | 
				
			||||||
                    {% if templates|length == 0 %}
 | 
					                    or calendar.users|length > 0 %}
 | 
				
			||||||
                        <li>
 | 
					                    <div class="item-row details separator">
 | 
				
			||||||
                            <a class="btn btn-create"
 | 
					                        <div class="item-col">
 | 
				
			||||||
                               href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
 | 
					                            {% include '@ChillActivity/Activity/concernedGroups.html.twig' with {
 | 
				
			||||||
                                {{ 'chill_calendar.Add a document'|trans }}
 | 
					                                'context': calendar.context == 'person' ?  'calendar_person' : 'calendar_accompanyingCourse',
 | 
				
			||||||
                            </a>
 | 
					                                'render': 'wrap-list',
 | 
				
			||||||
                        </li>
 | 
					                                'entity': calendar
 | 
				
			||||||
                    {% else %}
 | 
					                            } %}
 | 
				
			||||||
                        <li>
 | 
					                        </div>
 | 
				
			||||||
                            <div class="dropdown">
 | 
					
 | 
				
			||||||
                                <button class="btn btn-create dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
					                    </div>
 | 
				
			||||||
                                    {{ 'chill_calendar.Add a document'|trans }}
 | 
					 | 
				
			||||||
                                </button>
 | 
					 | 
				
			||||||
                                <ul class="dropdown-menu">
 | 
					 | 
				
			||||||
                                    <li>
 | 
					 | 
				
			||||||
                                        <a class="dropdown-item"
 | 
					 | 
				
			||||||
                                           href="{{ chill_path_add_return_path('chill_calendar_calendardoc_new', {'id': calendar.id }) }}">
 | 
					 | 
				
			||||||
                                            {{ 'chill_calendar.Upload a document'|trans }}
 | 
					 | 
				
			||||||
                                        </a>
 | 
					 | 
				
			||||||
                                    </li>
 | 
					 | 
				
			||||||
                                    {% for template in templates %}
 | 
					 | 
				
			||||||
                                    <li>
 | 
					 | 
				
			||||||
                                        <a class="dropdown-item"
 | 
					 | 
				
			||||||
                                            href="{{ chill_path_add_return_path('chill_docgenerator_generate_from_template', {'template': template.id, 'entityClassName': 'Chill\\CalendarBundle\\Entity\\Calendar', 'entityId': calendar.id}) }}"
 | 
					 | 
				
			||||||
                                        >
 | 
					 | 
				
			||||||
                                           {{ template.name|localize_translatable_string }}
 | 
					 | 
				
			||||||
                                        </a>
 | 
					 | 
				
			||||||
                                    </li>
 | 
					 | 
				
			||||||
                                    {% endfor %}
 | 
					 | 
				
			||||||
                                </ul>
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                        </li>
 | 
					 | 
				
			||||||
                    {% endif %}
 | 
					 | 
				
			||||||
                {% endif %}
 | 
					                {% endif %}
 | 
				
			||||||
            {% if calendar.activity is null and (
 | 
					 | 
				
			||||||
                (calendar.context == 'accompanying_period' and is_granted('CHILL_ACTIVITY_CREATE', calendar.accompanyingPeriod))
 | 
					 | 
				
			||||||
                or
 | 
					 | 
				
			||||||
                (calendar.context == 'person' and is_granted('CHILL_ACTIVITY_CREATE', calendar.person))
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            %}
 | 
					 | 
				
			||||||
                <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)) %}
 | 
					                {% if calendar.comment.comment is not empty %}
 | 
				
			||||||
                {% set invite = calendar.inviteForUser(app.user) %}
 | 
					                    <div class="item-row details separator">
 | 
				
			||||||
                <li>
 | 
					                        <div class="item-col comment">
 | 
				
			||||||
                    <div invite-answer data-status="{{ invite.status|e('html_attr') }}"
 | 
					                            {{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
 | 
				
			||||||
                         data-calendar-id="{{ calendar.id|e('html_attr') }}"></div>
 | 
					                        </div>
 | 
				
			||||||
                </li>
 | 
					                    </div>
 | 
				
			||||||
            {% endif %}
 | 
					                {% endif %}
 | 
				
			||||||
            {% if false %}
 | 
					
 | 
				
			||||||
                <li>
 | 
					                {% if calendar.location is not empty %}
 | 
				
			||||||
                    <a href="{{ chill_path_add_return_path('chill_calendar_calendar_show', { 'id': calendar.id}) }}"
 | 
					                    <div class="item-row separator">
 | 
				
			||||||
                       class="btn btn-show "></a>
 | 
					                        <div>
 | 
				
			||||||
                </li>
 | 
					                            {% if calendar.location.address is not same as(null) and calendar.location.name is not empty %}
 | 
				
			||||||
            {% endif %}
 | 
					                                <i class="fa fa-map-marker"></i>{% endif %}
 | 
				
			||||||
            {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', calendar) %}
 | 
					                            {% if calendar.location.name is not empty %}{{ calendar.location.name }}{% endif %}
 | 
				
			||||||
            <li>
 | 
					                            {% 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 %}
 | 
				
			||||||
                <a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { 'id': calendar.id }) }}"
 | 
					                                <i class="fa fa-map-marker"></i>{% endif %}
 | 
				
			||||||
                   class="btn btn-update "></a>
 | 
					                            {% if calendar.location.phonenumber1 is not empty %}<i
 | 
				
			||||||
            </li>
 | 
					                                class="fa fa-phone"></i> {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %}
 | 
				
			||||||
            {% endif %}
 | 
					                            {% if calendar.location.phonenumber2 is not empty %}<i
 | 
				
			||||||
            {% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
 | 
					                                class="fa fa-phone"></i> {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %}
 | 
				
			||||||
            <li>
 | 
					                        </div>
 | 
				
			||||||
                <a href="{{ chill_path_add_return_path('chill_calendar_calendar_delete', { 'id': calendar.id } ) }}"
 | 
					                    </div>
 | 
				
			||||||
                   class="btn btn-delete "></a>
 | 
					                {% endif %}
 | 
				
			||||||
            </li>
 | 
					
 | 
				
			||||||
            {% endif %}
 | 
					                <div class="item-row separator column">
 | 
				
			||||||
        </ul>
 | 
					                    <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') }}"
 | 
				
			||||||
 | 
					                                     data-calendar-id="{{ calendar.id|e('html_attr') }}"></div>
 | 
				
			||||||
 | 
					                            </li>
 | 
				
			||||||
 | 
					                        {% endif %}
 | 
				
			||||||
 | 
					                        {% if false %}
 | 
				
			||||||
 | 
					                            <li>
 | 
				
			||||||
 | 
					                                <a href="{{ chill_path_add_return_path('chill_calendar_calendar_show', { 'id': calendar.id}) }}"
 | 
				
			||||||
 | 
					                                   class="btn btn-show "></a>
 | 
				
			||||||
 | 
					                            </li>
 | 
				
			||||||
 | 
					                        {% endif %}
 | 
				
			||||||
 | 
					                        {% 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>
 | 
				
			||||||
 | 
					                        {% endif %}
 | 
				
			||||||
 | 
					                        {% if is_granted('CHILL_CALENDAR_CALENDAR_DELETE', calendar) %}
 | 
				
			||||||
 | 
					                        <li>
 | 
				
			||||||
 | 
					                            <a href="{{ chill_path_add_return_path('chill_calendar_calendar_delete', { 'id': calendar.id } ) }}"
 | 
				
			||||||
 | 
					                               class="btn btn-delete "></a>
 | 
				
			||||||
 | 
					                        </li>
 | 
				
			||||||
 | 
					                        {% endif %}
 | 
				
			||||||
 | 
					                    </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        {% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {% if calendarItems|length < paginator.getTotalItems %}
 | 
				
			||||||
 | 
					            {{ chill_pagination(paginator) }}
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,18 +34,7 @@
 | 
				
			|||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
    {% else %}
 | 
					    {% else %}
 | 
				
			||||||
        {% if calendarItems|length > 0 %}
 | 
					        {{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}
 | 
				
			||||||
            <div class="flex-table list-records context-accompanyingCourse">
 | 
					 | 
				
			||||||
                {% for calendar in calendarItems %}
 | 
					 | 
				
			||||||
                    {{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }}
 | 
					 | 
				
			||||||
                {% endfor %}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {% if calendarItems|length < paginator.getTotalItems %}
 | 
					 | 
				
			||||||
                {{ chill_pagination(paginator) }}
 | 
					 | 
				
			||||||
            {% endif %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {% endif %}
 | 
					 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ul class="record_actions sticky-form-buttons">
 | 
					    <ul class="record_actions sticky-form-buttons">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,17 +33,7 @@
 | 
				
			|||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
    {% else %}
 | 
					    {% else %}
 | 
				
			||||||
        {% if calendarItems|length > 0 %}
 | 
					        {{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}
 | 
				
			||||||
            <div class="flex-table list-records context-person">
 | 
					 | 
				
			||||||
                {% for calendar in calendarItems %}
 | 
					 | 
				
			||||||
                    {{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }}
 | 
					 | 
				
			||||||
                {% endfor %}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {% if calendarItems|length < paginator.getTotalItems %}
 | 
					 | 
				
			||||||
                {{ chill_pagination(paginator) }}
 | 
					 | 
				
			||||||
            {% endif %}
 | 
					 | 
				
			||||||
        {% endif %}
 | 
					 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ul class="record_actions sticky-form-buttons">
 | 
					    <ul class="record_actions sticky-form-buttons">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,40 +0,0 @@
 | 
				
			|||||||
{% extends "@ChillMain/layout.html.twig" %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% set activeRouteKey = 'chill_calendar_invitations_list' %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{% block title %}{{ 'My invitations list' |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 %}
 | 
					 | 
				
			||||||
@@ -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);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -86,9 +86,6 @@ invite:
 | 
				
			|||||||
    declined: Refusé
 | 
					    declined: Refusé
 | 
				
			||||||
    pending: En attente
 | 
					    pending: En attente
 | 
				
			||||||
    tentative: Accepté provisoirement
 | 
					    tentative: Accepté provisoirement
 | 
				
			||||||
    list:
 | 
					 | 
				
			||||||
        none: Il n'y aucun invitation
 | 
					 | 
				
			||||||
        title: Mes invitations
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# exports
 | 
					# exports
 | 
				
			||||||
Exports of calendar: Exports des rendez-vous
 | 
					Exports of calendar: Exports des rendez-vous
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,9 +20,4 @@ use Doctrine\Persistence\ObjectRepository;
 | 
				
			|||||||
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
 | 
					interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public function countByEntity(string $entity): int;
 | 
					    public function countByEntity(string $entity): int;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @return array|DocGeneratorTemplate[]
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function findByEntity(string $entity, ?int $start = 0, ?int $limit = 50): array;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return new JsonResponse(
 | 
					        return new JsonResponse(
 | 
				
			||||||
            $this->serializer->serialize(
 | 
					            $this->serializer->serialize(
 | 
				
			||||||
                new Collection($items, $paginator),
 | 
					                new Collection(array_values($items->toArray()), $paginator),
 | 
				
			||||||
                'json',
 | 
					                'json',
 | 
				
			||||||
                [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
 | 
					                [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,10 +23,14 @@ use Random\RandomException;
 | 
				
			|||||||
 * Store each version of StoredObject's.
 | 
					 * Store each version of StoredObject's.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * A version should not be created manually: use the method @see{StoredObject::registerVersion} instead.
 | 
					 * A version should not be created manually: use the method @see{StoredObject::registerVersion} instead.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Each filename must be unique within the same StoredObject. We add a condition on id to apply this condition only for
 | 
				
			||||||
 | 
					 * newly created versions when this new index is applied.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
#[ORM\Entity]
 | 
					#[ORM\Entity]
 | 
				
			||||||
#[ORM\Table('chill_doc.stored_object_version')]
 | 
					#[ORM\Table('chill_doc.stored_object_version')]
 | 
				
			||||||
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])]
 | 
					#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])]
 | 
				
			||||||
 | 
					#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_filename', columns: ['filename'], options: ['where' => '(id > 0)'])]
 | 
				
			||||||
class StoredObjectVersion implements TrackCreationInterface
 | 
					class StoredObjectVersion implements TrackCreationInterface
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    use TrackCreationTrait;
 | 
					    use TrackCreationTrait;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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\DocStoreBundle\Exception;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ConversionWithSameMimeTypeException extends \RuntimeException
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function __construct(string $mimeType, ?\Throwable $previous = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        parent::__construct("Conversion to same MIME type '{$mimeType}' is not allowed: already at the same MIME type", 0, $previous);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -25,7 +25,7 @@ export interface GenericDoc {
 | 
				
			|||||||
    type: "doc_store_generic_doc";
 | 
					    type: "doc_store_generic_doc";
 | 
				
			||||||
    uniqueKey: string;
 | 
					    uniqueKey: string;
 | 
				
			||||||
    key: string;
 | 
					    key: string;
 | 
				
			||||||
    identifiers: object;
 | 
					    identifiers: { id: number };
 | 
				
			||||||
    context: "person" | "accompanying-period";
 | 
					    context: "person" | "accompanying-period";
 | 
				
			||||||
    doc_date: DateTime;
 | 
					    doc_date: DateTime;
 | 
				
			||||||
    metadata: GenericDocMetadata;
 | 
					    metadata: GenericDocMetadata;
 | 
				
			||||||
@@ -36,6 +36,18 @@ export interface GenericDocForAccompanyingPeriod extends GenericDoc {
 | 
				
			|||||||
    context: "accompanying-period";
 | 
					    context: "accompanying-period";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isGenericDocForAccompanyingPeriod(
 | 
				
			||||||
 | 
					    doc: GenericDoc,
 | 
				
			||||||
 | 
					): doc is GenericDocForAccompanyingPeriod {
 | 
				
			||||||
 | 
					    return doc.context === "accompanying-period";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isGenericDocWithStoredObject(
 | 
				
			||||||
 | 
					    doc: GenericDoc,
 | 
				
			||||||
 | 
					): doc is GenericDoc & { storedObject: StoredObject } {
 | 
				
			||||||
 | 
					    return doc.storedObject !== null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface BaseMetadataWithHtml extends BaseMetadata {
 | 
					interface BaseMetadataWithHtml extends BaseMetadata {
 | 
				
			||||||
    html: string;
 | 
					    html: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -44,28 +56,33 @@ export interface GenericDocForAccompanyingCourseDocument
 | 
				
			|||||||
    extends GenericDocForAccompanyingPeriod {
 | 
					    extends GenericDocForAccompanyingPeriod {
 | 
				
			||||||
    key: "accompanying_course_document";
 | 
					    key: "accompanying_course_document";
 | 
				
			||||||
    metadata: BaseMetadataWithHtml;
 | 
					    metadata: BaseMetadataWithHtml;
 | 
				
			||||||
 | 
					    storedObject: StoredObject;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GenericDocForAccompanyingCourseActivityDocument
 | 
					export interface GenericDocForAccompanyingCourseActivityDocument
 | 
				
			||||||
    extends GenericDocForAccompanyingPeriod {
 | 
					    extends GenericDocForAccompanyingPeriod {
 | 
				
			||||||
    key: "accompanying_course_activity_document";
 | 
					    key: "accompanying_course_activity_document";
 | 
				
			||||||
    metadata: BaseMetadataWithHtml;
 | 
					    metadata: BaseMetadataWithHtml;
 | 
				
			||||||
 | 
					    storedObject: StoredObject;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GenericDocForAccompanyingCourseCalendarDocument
 | 
					export interface GenericDocForAccompanyingCourseCalendarDocument
 | 
				
			||||||
    extends GenericDocForAccompanyingPeriod {
 | 
					    extends GenericDocForAccompanyingPeriod {
 | 
				
			||||||
    key: "accompanying_course_calendar_document";
 | 
					    key: "accompanying_course_calendar_document";
 | 
				
			||||||
    metadata: BaseMetadataWithHtml;
 | 
					    metadata: BaseMetadataWithHtml;
 | 
				
			||||||
 | 
					    storedObject: StoredObject;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GenericDocForAccompanyingCoursePersonDocument
 | 
					export interface GenericDocForAccompanyingCoursePersonDocument
 | 
				
			||||||
    extends GenericDocForAccompanyingPeriod {
 | 
					    extends GenericDocForAccompanyingPeriod {
 | 
				
			||||||
    key: "person_document";
 | 
					    key: "person_document";
 | 
				
			||||||
    metadata: BaseMetadataWithHtml;
 | 
					    metadata: BaseMetadataWithHtml;
 | 
				
			||||||
 | 
					    storedObject: StoredObject;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
 | 
					export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
 | 
				
			||||||
    extends GenericDocForAccompanyingPeriod {
 | 
					    extends GenericDocForAccompanyingPeriod {
 | 
				
			||||||
    key: "accompanying_period_work_evaluation_document";
 | 
					    key: "accompanying_period_work_evaluation_document";
 | 
				
			||||||
    metadata: BaseMetadataWithHtml;
 | 
					    metadata: BaseMetadataWithHtml;
 | 
				
			||||||
 | 
					    storedObject: StoredObject;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,9 +3,9 @@ import {
 | 
				
			|||||||
    StoredObject,
 | 
					    StoredObject,
 | 
				
			||||||
    StoredObjectPointInTime,
 | 
					    StoredObjectPointInTime,
 | 
				
			||||||
    StoredObjectVersionWithPointInTime,
 | 
					    StoredObjectVersionWithPointInTime,
 | 
				
			||||||
} from "./../../../types";
 | 
					} from "ChillDocStoreAssets/types";
 | 
				
			||||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
 | 
					import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
 | 
				
			||||||
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
 | 
					import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
 | 
				
			||||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
 | 
					import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
 | 
				
			||||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
 | 
					import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
 | 
				
			||||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
 | 
					import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,6 +46,16 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
 | 
					    public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        // we first try to get the permission from the workflow, as attachement (this is the less intensive query)
 | 
				
			||||||
 | 
					        $workflowPermissionAsAttachment = match ($attribute) {
 | 
				
			||||||
 | 
					            StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject),
 | 
				
			||||||
 | 
					            StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Retrieve the related entity
 | 
					        // Retrieve the related entity
 | 
				
			||||||
        $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
 | 
					        $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,7 +76,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
 | 
				
			|||||||
        return match ($workflowPermission) {
 | 
					        return match ($workflowPermission) {
 | 
				
			||||||
            WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
 | 
					            WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
 | 
				
			||||||
            WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
 | 
					            WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
 | 
				
			||||||
            WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
 | 
					            WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,12 @@ namespace Chill\DocStoreBundle\Security\Authorization;
 | 
				
			|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
					use Chill\DocStoreBundle\Entity\StoredObject;
 | 
				
			||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
					use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Interface for voting on stored object permissions.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Each time a stored object is attached to a document, the voter is responsible for determining
 | 
				
			||||||
 | 
					 * whether the user has the necessary permissions to access or modify the stored object.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
interface StoredObjectVoterInterface
 | 
					interface StoredObjectVoterInterface
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
 | 
					    public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
 | 
				
			|||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
 | 
					use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
 | 
				
			||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
 | 
					use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
 | 
				
			||||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
 | 
					use Chill\DocStoreBundle\Entity\StoredObjectVersion;
 | 
				
			||||||
 | 
					use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException;
 | 
				
			||||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
 | 
					use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
 | 
				
			||||||
use Chill\WopiBundle\Service\WopiConverter;
 | 
					use Chill\WopiBundle\Service\WopiConverter;
 | 
				
			||||||
use Symfony\Component\Mime\MimeTypesInterface;
 | 
					use Symfony\Component\Mime\MimeTypesInterface;
 | 
				
			||||||
@@ -41,9 +42,10 @@ class StoredObjectToPdfConverter
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true
 | 
					     * @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @throws \UnexpectedValueException    if the preferred mime type for the conversion is not found
 | 
					     * @throws \UnexpectedValueException           if the preferred mime type for the conversion is not found
 | 
				
			||||||
     * @throws \RuntimeException            if the conversion or storage of the new version fails
 | 
					     * @throws \RuntimeException                   if the conversion or storage of the new version fails
 | 
				
			||||||
     * @throws StoredObjectManagerException
 | 
					     * @throws StoredObjectManagerException
 | 
				
			||||||
 | 
					     * @throws ConversionWithSameMimeTypeException if the document has already the same mime type79*
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
 | 
					    public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -56,7 +58,7 @@ class StoredObjectToPdfConverter
 | 
				
			|||||||
        $currentVersion = $storedObject->getCurrentVersion();
 | 
					        $currentVersion = $storedObject->getCurrentVersion();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($currentVersion->getType() === $newMimeType) {
 | 
					        if ($currentVersion->getType() === $newMimeType) {
 | 
				
			||||||
            throw new \UnexpectedValueException('Already at the same mime type');
 | 
					            throw new ConversionWithSameMimeTypeException($newMimeType);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $content = $this->storedObjectManager->read($currentVersion);
 | 
					        $content = $this->storedObjectManager->read($currentVersion);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,6 +40,10 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
 | 
				
			|||||||
            $storedObject->registerVersion();
 | 
					            $storedObject->registerVersion();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // remove one version in the history
 | 
				
			||||||
 | 
					        $v5 = $storedObject->getVersions()->get(5);
 | 
				
			||||||
 | 
					        $storedObject->removeVersion($v5);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $security = $this->prophesize(Security::class);
 | 
					        $security = $this->prophesize(Security::class);
 | 
				
			||||||
        $security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
 | 
					        $security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
 | 
				
			||||||
            ->willReturn(true)
 | 
					            ->willReturn(true)
 | 
				
			||||||
@@ -53,6 +57,7 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
 | 
				
			|||||||
        self::assertEquals($response->getStatusCode(), 200);
 | 
					        self::assertEquals($response->getStatusCode(), 200);
 | 
				
			||||||
        self::assertIsArray($body);
 | 
					        self::assertIsArray($body);
 | 
				
			||||||
        self::assertArrayHasKey('results', $body);
 | 
					        self::assertArrayHasKey('results', $body);
 | 
				
			||||||
 | 
					        self::assertIsList($body['results']);
 | 
				
			||||||
        self::assertCount(10, $body['results']);
 | 
					        self::assertCount(10, $body['results']);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -86,9 +86,165 @@ class AbstractStoredObjectVoterTest extends TestCase
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @dataProvider dataProviderVoteOnAttribute
 | 
					     * @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function testVoteOnAttribute(
 | 
					    public function testVoteOnAttributeWithStoredObjectPermission(
 | 
				
			||||||
 | 
					        StoredObjectRoleEnum $attribute,
 | 
				
			||||||
 | 
					        bool $expected,
 | 
				
			||||||
 | 
					        bool $isGrantedRegularPermission,
 | 
				
			||||||
 | 
					        string $isGrantedWorkflowPermission,
 | 
				
			||||||
 | 
					        string $isGrantedStoredObjectAttachment,
 | 
				
			||||||
 | 
					    ): void {
 | 
				
			||||||
 | 
					        $storedObject = new StoredObject();
 | 
				
			||||||
 | 
					        $repository = new DummyRepository($related = new \stdClass());
 | 
				
			||||||
 | 
					        $token = new UsernamePasswordToken(new User(), 'dummy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $security = $this->prophesize(Security::class);
 | 
				
			||||||
 | 
					        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $security = $this->prophesize(Security::class);
 | 
				
			||||||
 | 
					        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (StoredObjectRoleEnum::SEE === $attribute) {
 | 
				
			||||||
 | 
					            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
 | 
				
			||||||
 | 
					                ->shouldBeCalled()
 | 
				
			||||||
 | 
					                ->willReturn($isGrantedStoredObjectAttachment);
 | 
				
			||||||
 | 
					            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
 | 
				
			||||||
 | 
					                ->willReturn($isGrantedWorkflowPermission);
 | 
				
			||||||
 | 
					        } elseif (StoredObjectRoleEnum::EDIT === $attribute) {
 | 
				
			||||||
 | 
					            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
 | 
				
			||||||
 | 
					                ->shouldBeCalled()
 | 
				
			||||||
 | 
					                ->willReturn($isGrantedStoredObjectAttachment);
 | 
				
			||||||
 | 
					            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
 | 
				
			||||||
 | 
					                ->willReturn($isGrantedWorkflowPermission);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            throw new \LogicException('Invalid attribute for StoredObjectVoter');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter {
 | 
				
			||||||
 | 
					            public function __construct(private $repository, $helper, $security)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                parent::__construct($security, $helper);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            protected function getRepository(): AssociatedEntityToStoredObjectInterface
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return $this->repository;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            protected function getClass(): string
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return \stdClass::class;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            protected function attributeToRole(StoredObjectRoleEnum $attribute): string
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return 'SOME_ROLE';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            protected function canBeAssociatedWithWorkflow(): bool
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $actual = $storedObjectVoter->voteOnAttribute($attribute, $storedObject, $token);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::assertEquals($expected, $actual);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
 | 
				
			||||||
 | 
					            yield 'Not related to any workflow nor attachment ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            yield 'Force grant inverse the regular permission (so) ('.$action.')' => [
 | 
				
			||||||
 | 
					                $attribute,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
				
			||||||
 | 
					                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function testVoteOnAttributeWithoutStoredObjectPermission(
 | 
				
			||||||
        StoredObjectRoleEnum $attribute,
 | 
					        StoredObjectRoleEnum $attribute,
 | 
				
			||||||
        bool $expected,
 | 
					        bool $expected,
 | 
				
			||||||
        bool $canBeAssociatedWithWorkflow,
 | 
					        bool $canBeAssociatedWithWorkflow,
 | 
				
			||||||
@@ -105,6 +261,10 @@ class AbstractStoredObjectVoterTest extends TestCase
 | 
				
			|||||||
        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
 | 
					        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
 | 
					        $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
 | 
				
			||||||
 | 
					        $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (null !== $isGrantedWorkflowPermissionRead) {
 | 
					        if (null !== $isGrantedWorkflowPermissionRead) {
 | 
				
			||||||
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
 | 
					            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
 | 
				
			||||||
                ->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
 | 
					                ->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
 | 
				
			||||||
@@ -123,7 +283,7 @@ class AbstractStoredObjectVoterTest extends TestCase
 | 
				
			|||||||
        self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
 | 
					        self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static function dataProviderVoteOnAttribute(): iterable
 | 
					    public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        // not associated on a workflow
 | 
					        // not associated on a workflow
 | 
				
			||||||
        yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
 | 
					        yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Chill is a software for social workers
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * For the full copyright and license information, please view
 | 
				
			||||||
 | 
					 * the LICENSE file that was distributed with this source code.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Chill\Migrations\DocStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Doctrine\DBAL\Schema\Schema;
 | 
				
			||||||
 | 
					use Doctrine\Migrations\AbstractMigration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class Version20251013094414 extends AbstractMigration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function getDescription(): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return 'DocStore: Enforce filename uniqueness on chill_doc.stored_object_version; clean duplicates and add partial unique index on filename (for new rows only).';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function up(Schema $schema): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // 1) Clean duplicates: for each (stored_object_id, filename, key, iv), keep only the last inserted row
 | 
				
			||||||
 | 
					        //    and delete all others. Use ROW_NUMBER over id DESC to define the last one.
 | 
				
			||||||
 | 
					        $this->addSql(<<<'SQL'
 | 
				
			||||||
 | 
					            WITH ranked AS (
 | 
				
			||||||
 | 
					                SELECT id,
 | 
				
			||||||
 | 
					                       rank() OVER (
 | 
				
			||||||
 | 
					                           PARTITION BY stored_object_id, filename, "key"::jsonb, iv::jsonb
 | 
				
			||||||
 | 
					                           ORDER BY id DESC
 | 
				
			||||||
 | 
					                       ) AS rn
 | 
				
			||||||
 | 
					                FROM chill_doc.stored_object_version
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            DELETE FROM chill_doc.stored_object_version sov
 | 
				
			||||||
 | 
					            USING ranked r
 | 
				
			||||||
 | 
					            WHERE sov.id = r.id
 | 
				
			||||||
 | 
					              AND r.rn > 1
 | 
				
			||||||
 | 
					        SQL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 2) Create a partial unique index on filename that applies only to subsequently inserted rows.
 | 
				
			||||||
 | 
					        //    Per user's instruction, compute the cutoff using the stored_object_id sequence value.
 | 
				
			||||||
 | 
					        $nextVal = (int) $this->connection->fetchOne("SELECT nextval('chill_doc.stored_object_version_id_seq')");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Safety: if somehow sequence is not available, fallback to current max id from the table
 | 
				
			||||||
 | 
					        if ($nextVal <= 0) {
 | 
				
			||||||
 | 
					            $nextVal = (int) $this->connection->fetchOne('SELECT COALESCE(MAX(id), 0) FROM chill_doc.stored_object_version');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->addSql(sprintf(
 | 
				
			||||||
 | 
					            'CREATE UNIQUE INDEX chill_doc_stored_object_version_unique_by_filename ON chill_doc.stored_object_version (filename) WHERE id > %d',
 | 
				
			||||||
 | 
					            $nextVal
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function down(Schema $schema): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Drop the partial unique index; data cleanup is irreversible.
 | 
				
			||||||
 | 
					        $this->addSql('DROP INDEX IF EXISTS chill_doc_stored_object_version_unique_by_filename');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -334,7 +334,7 @@ class ChillImportUsersCommand extends Command
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    protected function loadUsers()
 | 
					    protected function loadUsers()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $reader = Reader::createFromPath($this->tempInput->getArgument('csvfile'));
 | 
					        $reader = Reader::from($this->tempInput->getArgument('csvfile'));
 | 
				
			||||||
        $reader->setHeaderOffset(0);
 | 
					        $reader->setHeaderOffset(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach ($reader->getRecords() as $line => $r) {
 | 
					        foreach ($reader->getRecords() as $line => $r) {
 | 
				
			||||||
@@ -362,7 +362,7 @@ class ChillImportUsersCommand extends Command
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    protected function prepareGroupingCenters()
 | 
					    protected function prepareGroupingCenters()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $reader = Reader::createFromPath($this->tempInput->getOption('grouping-centers'));
 | 
					        $reader = Reader::from($this->tempInput->getOption('grouping-centers'));
 | 
				
			||||||
        $reader->setHeaderOffset(0);
 | 
					        $reader->setHeaderOffset(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach ($reader->getRecords() as $r) {
 | 
					        foreach ($reader->getRecords() as $r) {
 | 
				
			||||||
@@ -378,7 +378,7 @@ class ChillImportUsersCommand extends Command
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    protected function prepareWriter()
 | 
					    protected function prepareWriter()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->output = $output = Writer::createFromPath($this->tempInput
 | 
					        $this->output = $output = Writer::from($this->tempInput
 | 
				
			||||||
            ->getOption('csv-dump'), 'a+');
 | 
					            ->getOption('csv-dump'), 'a+');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $output->insertOne([
 | 
					        $output->insertOne([
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -119,7 +119,7 @@ class ChillUserSendRenewPasswordCodeCommand extends Command
 | 
				
			|||||||
    protected function getReader()
 | 
					    protected function getReader()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $reader = Reader::createFromPath($this->input->getArgument('csvfile'));
 | 
					            $reader = Reader::from($this->input->getArgument('csvfile'));
 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
            $this->logger->error('The csv file could not be read', [
 | 
					            $this->logger->error('The csv file could not be read', [
 | 
				
			||||||
                'path' => $this->input->getArgument('csvfile'),
 | 
					                'path' => $this->input->getArgument('csvfile'),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,7 @@ final readonly class UserExportController
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $users = $this->userRepository->findAllAsArray($request->getLocale());
 | 
					        $users = $this->userRepository->findAllAsArray($request->getLocale());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Writer::createFromPath('php://temp', 'r+');
 | 
					        $csv = Writer::from('php://temp', 'r+');
 | 
				
			||||||
        $csv->insertOne(
 | 
					        $csv->insertOne(
 | 
				
			||||||
            array_map(
 | 
					            array_map(
 | 
				
			||||||
                fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
 | 
					                fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
 | 
				
			||||||
@@ -104,7 +104,7 @@ final readonly class UserExportController
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $userPermissions = $this->userRepository->findAllUserACLAsArray();
 | 
					        $userPermissions = $this->userRepository->findAllUserACLAsArray();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Writer::createFromPath('php://temp', 'r+');
 | 
					        $csv = Writer::from('php://temp', 'r+');
 | 
				
			||||||
        $csv->insertOne(
 | 
					        $csv->insertOne(
 | 
				
			||||||
            array_map(
 | 
					            array_map(
 | 
				
			||||||
                fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
 | 
					                fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Chill\MainBundle\Controller;
 | 
					namespace Chill\MainBundle\Controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\MainBundle\CRUD\Controller\ApiController;
 | 
				
			||||||
use Chill\MainBundle\Entity\User;
 | 
					use Chill\MainBundle\Entity\User;
 | 
				
			||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
					use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
				
			||||||
@@ -27,7 +28,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 | 
				
			|||||||
use Symfony\Component\Security\Core\Security;
 | 
					use Symfony\Component\Security\Core\Security;
 | 
				
			||||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
					use Symfony\Component\Serializer\SerializerInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WorkflowApiController
 | 
					class WorkflowApiController extends ApiController
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {}
 | 
					    public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -264,11 +264,12 @@ class WorkflowController extends AbstractController
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
 | 
					        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser());
 | 
					        $total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser(), false);
 | 
				
			||||||
        $paginator = $this->paginatorFactory->create($total);
 | 
					        $paginator = $this->paginatorFactory->create($total);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $workflows = $this->entityWorkflowRepository->findBySubscriber(
 | 
					        $workflows = $this->entityWorkflowRepository->findBySubscriber(
 | 
				
			||||||
            $this->security->getUser(),
 | 
					            $this->security->getUser(),
 | 
				
			||||||
 | 
					            false,
 | 
				
			||||||
            ['createdAt' => 'DESC'],
 | 
					            ['createdAt' => 'DESC'],
 | 
				
			||||||
            $paginator->getItemsPerPage(),
 | 
					            $paginator->getItemsPerPage(),
 | 
				
			||||||
            $paginator->getCurrentPageFirstItemNumber()
 | 
					            $paginator->getCurrentPageFirstItemNumber()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,7 +44,7 @@ final readonly class WorkflowSignatureStateChangeController
 | 
				
			|||||||
            $signature,
 | 
					            $signature,
 | 
				
			||||||
            $request,
 | 
					            $request,
 | 
				
			||||||
            EntityWorkflowStepSignatureVoter::CANCEL,
 | 
					            EntityWorkflowStepSignatureVoter::CANCEL,
 | 
				
			||||||
            function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
 | 
					            fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsCanceled($signature),
 | 
				
			||||||
            '@ChillMain/WorkflowSignature/cancel.html.twig',
 | 
					            '@ChillMain/WorkflowSignature/cancel.html.twig',
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -56,11 +56,18 @@ final readonly class WorkflowSignatureStateChangeController
 | 
				
			|||||||
            $signature,
 | 
					            $signature,
 | 
				
			||||||
            $request,
 | 
					            $request,
 | 
				
			||||||
            EntityWorkflowStepSignatureVoter::REJECT,
 | 
					            EntityWorkflowStepSignatureVoter::REJECT,
 | 
				
			||||||
            function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
 | 
					            fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsRejected($signature),
 | 
				
			||||||
            '@ChillMain/WorkflowSignature/reject.html.twig',
 | 
					            '@ChillMain/WorkflowSignature/reject.html.twig',
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param callable(EntityWorkflowStepSignature): string $markSignature
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @throws \Twig\Error\LoaderError
 | 
				
			||||||
 | 
					     * @throws \Twig\Error\RuntimeError
 | 
				
			||||||
 | 
					     * @throws \Twig\Error\SyntaxError
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    private function markSignatureAction(
 | 
					    private function markSignatureAction(
 | 
				
			||||||
        EntityWorkflowStepSignature $signature,
 | 
					        EntityWorkflowStepSignature $signature,
 | 
				
			||||||
        Request $request,
 | 
					        Request $request,
 | 
				
			||||||
@@ -79,12 +86,13 @@ final readonly class WorkflowSignatureStateChangeController
 | 
				
			|||||||
        $form->handleRequest($request);
 | 
					        $form->handleRequest($request);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($form->isSubmitted() && $form->isValid()) {
 | 
					        if ($form->isSubmitted() && $form->isValid()) {
 | 
				
			||||||
            $this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) {
 | 
					            $expectedStep = $this->entityManager->wrapInTransaction(fn () => $markSignature($signature));
 | 
				
			||||||
                $markSignature($signature);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return new RedirectResponse(
 | 
					            return new RedirectResponse(
 | 
				
			||||||
                $this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
 | 
					                $this->chillUrlGenerator->forwardReturnPath(
 | 
				
			||||||
 | 
					                    'chill_main_workflow_wait',
 | 
				
			||||||
 | 
					                    ['id' => $signature->getStep()->getEntityWorkflow()->getId(), 'expectedStep' => $expectedStep]
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Chill is a software for social workers
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * For the full copyright and license information, please view
 | 
				
			||||||
 | 
					 * the LICENSE file that was distributed with this source code.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Chill\MainBundle\Controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
 | 
				
			||||||
 | 
					use Symfony\Component\HttpFoundation\RedirectResponse;
 | 
				
			||||||
 | 
					use Symfony\Component\HttpFoundation\Response;
 | 
				
			||||||
 | 
					use Symfony\Component\Routing\Annotation\Route;
 | 
				
			||||||
 | 
					use Twig\Environment;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final readonly class WorkflowWaitStepChangeController
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        private ChillUrlGeneratorInterface $chillUrlGenerator,
 | 
				
			||||||
 | 
					        private Environment $twig,
 | 
				
			||||||
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[Route('/{_locale}/main/workflow/{id}/wait/{expectedStep}', name: 'chill_main_workflow_wait', methods: ['GET'])]
 | 
				
			||||||
 | 
					    public function waitForSignatureChange(EntityWorkflow $entityWorkflow, string $expectedStep): Response
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if ($entityWorkflow->getStep() === $expectedStep) {
 | 
				
			||||||
 | 
					            return new RedirectResponse(
 | 
				
			||||||
 | 
					                $this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return new Response(
 | 
				
			||||||
 | 
					            $this->twig->render('@ChillMain/Workflow/waiting.html.twig', ['workflow' => $entityWorkflow, 'expectedStep' => $expectedStep])
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Chill is a software for social workers
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * For the full copyright and license information, please view
 | 
				
			||||||
 | 
					 * the LICENSE file that was distributed with this source code.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Chill\MainBundle\DataFixtures\ORM;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Location;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\LocationType;
 | 
				
			||||||
 | 
					use Doctrine\Common\DataFixtures\AbstractFixture;
 | 
				
			||||||
 | 
					use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
 | 
				
			||||||
 | 
					use Doctrine\Persistence\ObjectManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LoadAdministrativeLocation extends AbstractFixture implements OrderedFixtureInterface
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    final public const ADMINISTRATIVE_LOCATION = 'administrative_location';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getOrder(): int
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return 9000;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function load(ObjectManager $manager): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $o = new Location();
 | 
				
			||||||
 | 
					        /** @var LocationType $locationType */
 | 
				
			||||||
 | 
					        $locationType = $this->getReference(LoadLocationType::LOCATION_TYPE.'_0');
 | 
				
			||||||
 | 
					        $o->setLocationType($locationType);
 | 
				
			||||||
 | 
					        $o->setName('Commune de Bruxelles');
 | 
				
			||||||
 | 
					        $o->setAvailableForUsers(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $manager->persist($o);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->addReference(self::ADMINISTRATIVE_LOCATION, $o);
 | 
				
			||||||
 | 
					        echo "Adding one Administrative Location\n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $manager->flush();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -25,6 +25,8 @@ class LoadLocationType extends AbstractFixture implements ContainerAwareInterfac
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    private ?ContainerInterface $container = null;
 | 
					    private ?ContainerInterface $container = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final public const LOCATION_TYPE = 'location_type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getOrder(): int
 | 
					    public function getOrder(): int
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return 52;
 | 
					        return 52;
 | 
				
			||||||
@@ -53,13 +55,15 @@ class LoadLocationType extends AbstractFixture implements ContainerAwareInterfac
 | 
				
			|||||||
            ],
 | 
					            ],
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach ($arr as $a) {
 | 
					        foreach ($arr as $index => $a) {
 | 
				
			||||||
            $locationType = (new LocationType())
 | 
					            $locationType = (new LocationType())
 | 
				
			||||||
                ->setTitle($a['name'])
 | 
					                ->setTitle($a['name'])
 | 
				
			||||||
                ->setAvailableForUsers(true)
 | 
					                ->setAvailableForUsers(true)
 | 
				
			||||||
                ->setActive(true)
 | 
					                ->setActive(true)
 | 
				
			||||||
                ->setAddressRequired($a['address_required']);
 | 
					                ->setAddressRequired($a['address_required']);
 | 
				
			||||||
            $manager->persist($locationType);
 | 
					            $manager->persist($locationType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $this->addReference(self::LOCATION_TYPE.'_'.$index, $locationType);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $manager->flush();
 | 
					        $manager->flush();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										41
									
								
								src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUserJob.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUserJob.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Chill is a software for social workers
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * For the full copyright and license information, please view
 | 
				
			||||||
 | 
					 * the LICENSE file that was distributed with this source code.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Chill\MainBundle\DataFixtures\ORM;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\UserJob;
 | 
				
			||||||
 | 
					use Doctrine\Common\DataFixtures\AbstractFixture;
 | 
				
			||||||
 | 
					use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
 | 
				
			||||||
 | 
					use Doctrine\Persistence\ObjectManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LoadUserJob extends AbstractFixture implements OrderedFixtureInterface
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    final public const USER_JOB = 'user_job';
 | 
				
			||||||
 | 
					    private array $socialWorker = ['en' => 'social worker', 'fr' => 'travailleur social'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getOrder(): int
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return 9000;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function load(ObjectManager $manager): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $o = new UserJob();
 | 
				
			||||||
 | 
					        $o->setLabel($this->socialWorker);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $manager->persist($o);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->addReference(self::USER_JOB, $o);
 | 
				
			||||||
 | 
					        echo "Adding one AccompanyingPeriod User Job\n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $manager->flush();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -30,6 +30,7 @@ use Chill\MainBundle\Controller\UserGroupAdminController;
 | 
				
			|||||||
use Chill\MainBundle\Controller\UserGroupApiController;
 | 
					use Chill\MainBundle\Controller\UserGroupApiController;
 | 
				
			||||||
use Chill\MainBundle\Controller\UserJobApiController;
 | 
					use Chill\MainBundle\Controller\UserJobApiController;
 | 
				
			||||||
use Chill\MainBundle\Controller\UserJobController;
 | 
					use Chill\MainBundle\Controller\UserJobController;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Controller\WorkflowApiController;
 | 
				
			||||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
 | 
					use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
 | 
				
			||||||
use Chill\MainBundle\Doctrine\DQL\Age;
 | 
					use Chill\MainBundle\Doctrine\DQL\Age;
 | 
				
			||||||
use Chill\MainBundle\Doctrine\DQL\Extract;
 | 
					use Chill\MainBundle\Doctrine\DQL\Extract;
 | 
				
			||||||
@@ -66,6 +67,7 @@ use Chill\MainBundle\Entity\Regroupment;
 | 
				
			|||||||
use Chill\MainBundle\Entity\User;
 | 
					use Chill\MainBundle\Entity\User;
 | 
				
			||||||
use Chill\MainBundle\Entity\UserGroup;
 | 
					use Chill\MainBundle\Entity\UserGroup;
 | 
				
			||||||
use Chill\MainBundle\Entity\UserJob;
 | 
					use Chill\MainBundle\Entity\UserJob;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
use Chill\MainBundle\Form\CenterType;
 | 
					use Chill\MainBundle\Form\CenterType;
 | 
				
			||||||
use Chill\MainBundle\Form\CivilityType;
 | 
					use Chill\MainBundle\Form\CivilityType;
 | 
				
			||||||
use Chill\MainBundle\Form\CountryType;
 | 
					use Chill\MainBundle\Form\CountryType;
 | 
				
			||||||
@@ -79,6 +81,7 @@ use Chill\MainBundle\Form\UserGroupType;
 | 
				
			|||||||
use Chill\MainBundle\Form\UserJobType;
 | 
					use Chill\MainBundle\Form\UserJobType;
 | 
				
			||||||
use Chill\MainBundle\Form\UserType;
 | 
					use Chill\MainBundle\Form\UserType;
 | 
				
			||||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
 | 
					use Chill\MainBundle\Security\Authorization\ChillExportVoter;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
 | 
				
			||||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
 | 
					use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
 | 
				
			||||||
use Ramsey\Uuid\Doctrine\UuidType;
 | 
					use Ramsey\Uuid\Doctrine\UuidType;
 | 
				
			||||||
use Symfony\Component\Config\FileLocator;
 | 
					use Symfony\Component\Config\FileLocator;
 | 
				
			||||||
@@ -202,6 +205,11 @@ class ChillMainExtension extends Extension implements
 | 
				
			|||||||
                []
 | 
					                []
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $container->setParameter(
 | 
				
			||||||
 | 
					            'chill_main.top_banner',
 | 
				
			||||||
 | 
					            $config['top_banner'] ?? []
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
 | 
					        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
 | 
				
			||||||
        $loader->load('services.yaml');
 | 
					        $loader->load('services.yaml');
 | 
				
			||||||
        $loader->load('services/doctrine.yaml');
 | 
					        $loader->load('services/doctrine.yaml');
 | 
				
			||||||
@@ -247,6 +255,7 @@ class ChillMainExtension extends Extension implements
 | 
				
			|||||||
                    'name' => $config['installation_name'], ],
 | 
					                    'name' => $config['installation_name'], ],
 | 
				
			||||||
                'available_languages' => $config['available_languages'],
 | 
					                'available_languages' => $config['available_languages'],
 | 
				
			||||||
                'add_address' => $config['add_address'],
 | 
					                'add_address' => $config['add_address'],
 | 
				
			||||||
 | 
					                'chill_main_config' => $config,
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            'form_themes' => ['@ChillMain/Form/fields.html.twig'],
 | 
					            'form_themes' => ['@ChillMain/Form/fields.html.twig'],
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
@@ -940,6 +949,21 @@ class ChillMainExtension extends Extension implements
 | 
				
			|||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
 | 
					                [
 | 
				
			||||||
 | 
					                    'class' => EntityWorkflow::class,
 | 
				
			||||||
 | 
					                    'name' => 'workflow',
 | 
				
			||||||
 | 
					                    'base_path' => '/api/1.0/main/workflow',
 | 
				
			||||||
 | 
					                    'base_role' => EntityWorkflowVoter::SEE,
 | 
				
			||||||
 | 
					                    'controller' => WorkflowApiController::class,
 | 
				
			||||||
 | 
					                    'actions' => [
 | 
				
			||||||
 | 
					                        '_entity' => [
 | 
				
			||||||
 | 
					                            'methods' => [
 | 
				
			||||||
 | 
					                                Request::METHOD_GET => true,
 | 
				
			||||||
 | 
					                                Request::METHOD_HEAD => true,
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -168,6 +168,20 @@ class Configuration implements ConfigurationInterface
 | 
				
			|||||||
            ->end()
 | 
					            ->end()
 | 
				
			||||||
            ->end()
 | 
					            ->end()
 | 
				
			||||||
            ->end()
 | 
					            ->end()
 | 
				
			||||||
 | 
					            ->arrayNode('top_banner')
 | 
				
			||||||
 | 
					            ->canBeUnset()
 | 
				
			||||||
 | 
					            ->children()
 | 
				
			||||||
 | 
					            ->booleanNode('visible')
 | 
				
			||||||
 | 
					            ->defaultFalse()
 | 
				
			||||||
 | 
					            ->end()
 | 
				
			||||||
 | 
					            ->arrayNode('text')
 | 
				
			||||||
 | 
					            ->useAttributeAsKey('lang')
 | 
				
			||||||
 | 
					            ->scalarPrototype()->end()
 | 
				
			||||||
 | 
					            ->end() // end of text
 | 
				
			||||||
 | 
					            ->scalarNode('color')->defaultNull()->end()
 | 
				
			||||||
 | 
					            ->scalarNode('background_color')->defaultNull()->end()
 | 
				
			||||||
 | 
					            ->end() // end of top_banner children
 | 
				
			||||||
 | 
					            ->end() // end of top_banner
 | 
				
			||||||
            ->arrayNode('widgets')
 | 
					            ->arrayNode('widgets')
 | 
				
			||||||
            ->canBeEnabled()
 | 
					            ->canBeEnabled()
 | 
				
			||||||
            ->canBeUnset()
 | 
					            ->canBeUnset()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,11 +53,16 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
 | 
				
			|||||||
    public function run(array $lastExecutionData): ?array
 | 
					    public function run(array $lastExecutionData): ?array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $now = $this->clock->now();
 | 
					        $now = $this->clock->now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (isset($lastExecutionData['last_execution'])) {
 | 
					        if (isset($lastExecutionData['last_execution'])) {
 | 
				
			||||||
            $lastExecution = \DateTimeImmutable::createFromFormat(
 | 
					            $lastExecution = \DateTimeImmutable::createFromFormat(
 | 
				
			||||||
                \DateTimeImmutable::ATOM,
 | 
					                \DateTimeImmutable::ATOM,
 | 
				
			||||||
                $lastExecutionData['last_execution']
 | 
					                $lastExecutionData['last_execution']
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (false === $lastExecution) {
 | 
				
			||||||
 | 
					                $lastExecution = $now->sub(new \DateInterval('P1D'));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            $lastExecution = $now->sub(new \DateInterval('P1D'));
 | 
					            $lastExecution = $now->sub(new \DateInterval('P1D'));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -96,7 +101,7 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
 | 
				
			|||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
 | 
					            'last_execution' => $now->format(\DateTimeInterface::ATOM),
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ use Symfony\Component\Routing\RouterInterface;
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Create paginator instances.
 | 
					 * Create paginator instances.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class PaginatorFactory implements PaginatorFactoryInterface
 | 
					final readonly class PaginatorFactory implements PaginatorFactoryInterface
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
 | 
					    final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,16 +29,16 @@ class PaginatorFactory implements PaginatorFactoryInterface
 | 
				
			|||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * the request stack.
 | 
					         * the request stack.
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        private readonly RequestStack $requestStack,
 | 
					        private RequestStack $requestStack,
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * the router and generator for url.
 | 
					         * the router and generator for url.
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        private readonly RouterInterface $router,
 | 
					        private RouterInterface $router,
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * the default item per page. This may be overriden by
 | 
					         * the default item per page. This may be overriden by
 | 
				
			||||||
         * the request or inside the paginator.
 | 
					         * the request or inside the paginator.
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        private readonly int $itemPerPage = 20,
 | 
					        private int $itemPerPage = 20,
 | 
				
			||||||
    ) {}
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,9 +57,15 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
				
			|||||||
        return (int) $qb->getQuery()->getSingleScalarResult();
 | 
					        return (int) $qb->getQuery()->getSingleScalarResult();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function countBySubscriber(User $user): int
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param bool|null $isFinal true to get only the entityWorkflow which is finalized, false to get the workflows that are not finalized, and null to ignore
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @throws \Doctrine\ORM\NoResultException
 | 
				
			||||||
 | 
					     * @throws \Doctrine\ORM\NonUniqueResultException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function countBySubscriber(User $user, ?bool $isFinal = null): int
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $qb = $this->buildQueryBySubscriber($user)->select('count(ew)');
 | 
					        $qb = $this->buildQueryBySubscriber($user, $isFinal)->select('count(ew)');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (int) $qb->getQuery()->getSingleScalarResult();
 | 
					        return (int) $qb->getQuery()->getSingleScalarResult();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -182,9 +188,14 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
				
			|||||||
        return $qb->getQuery()->getResult();
 | 
					        return $qb->getQuery()->getResult();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function findBySubscriber(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param bool|null  $isFinal true to get only the entityWorkflow which is finalized, false to get the workflows that are not finalized, and null to ignore
 | 
				
			||||||
 | 
					     * @param mixed|null $limit
 | 
				
			||||||
 | 
					     * @param mixed|null $offset
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function findBySubscriber(User $user, ?bool $isFinal = null, ?array $orderBy = null, $limit = null, $offset = null): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $qb = $this->buildQueryBySubscriber($user)->select('ew');
 | 
					        $qb = $this->buildQueryBySubscriber($user, $isFinal)->select('ew');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach ($orderBy as $key => $sort) {
 | 
					        foreach ($orderBy as $key => $sort) {
 | 
				
			||||||
            $qb->addOrderBy('ew.'.$key, $sort);
 | 
					            $qb->addOrderBy('ew.'.$key, $sort);
 | 
				
			||||||
@@ -312,7 +323,7 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
				
			|||||||
        return $qb;
 | 
					        return $qb;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function buildQueryBySubscriber(User $user): QueryBuilder
 | 
					    private function buildQueryBySubscriber(User $user, ?bool $isFinal): QueryBuilder
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $qb = $this->repository->createQueryBuilder('ew');
 | 
					        $qb = $this->repository->createQueryBuilder('ew');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -325,6 +336,14 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $qb->setParameter('user', $user);
 | 
					        $qb->setParameter('user', $user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (null !== $isFinal) {
 | 
				
			||||||
 | 
					            if ($isFinal) {
 | 
				
			||||||
 | 
					                $qb->andWhere(sprintf('EXISTS (SELECT 1 FROM %s step WHERE step.isFinal = true AND ew = step.entityWorkflow)', EntityWorkflowStep::class));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $qb->andWhere(sprintf('NOT EXISTS (SELECT 1 FROM %s step WHERE step.isFinal = true AND ew = step.entityWorkflow)', EntityWorkflowStep::class));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $qb;
 | 
					        return $qb;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Extracts the "returnPath" parameter from the current URL's query string and returns it.
 | 
				
			||||||
 | 
					 * If the parameter is not present, returns the provided fallback path.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} fallbackPath - The fallback path to use if "returnPath" is not found in the query string.
 | 
				
			||||||
 | 
					 * @return {string} The "returnPath" from the query string, or the fallback path if "returnPath" is not present.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function returnPathOr(fallbackPath: string): string {
 | 
				
			||||||
 | 
					    const urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					    const returnPath = urlParams.get("returnPath");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return returnPath ?? fallbackPath;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { EntityWorkflow } from "ChillMainAssets/types";
 | 
				
			||||||
 | 
					import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchWorkflow = async (
 | 
				
			||||||
 | 
					    workflowId: number,
 | 
				
			||||||
 | 
					): Promise<EntityWorkflow> => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        return await makeFetch<null, EntityWorkflow>(
 | 
				
			||||||
 | 
					            "GET",
 | 
				
			||||||
 | 
					            `/api/1.0/main/workflow/${workflowId}.json`,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.error(`Failed to fetch workflow ${workflowId}:`, error);
 | 
				
			||||||
 | 
					        throw error;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -1,5 +1,9 @@
 | 
				
			|||||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
 | 
					import {
 | 
				
			||||||
 | 
					    GenericDoc,
 | 
				
			||||||
 | 
					    isGenericDocWithStoredObject,
 | 
				
			||||||
 | 
					} from "ChillDocStoreAssets/types/generic_doc";
 | 
				
			||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
 | 
					import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
 | 
				
			||||||
 | 
					import { Person } from "../../../ChillPersonBundle/Resources/public/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface DateTime {
 | 
					export interface DateTime {
 | 
				
			||||||
    datetime: string;
 | 
					    datetime: string;
 | 
				
			||||||
@@ -202,6 +206,77 @@ export interface WorkflowAttachment {
 | 
				
			|||||||
    genericDoc: null | GenericDoc;
 | 
					    genericDoc: null | GenericDoc;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AttachmentWithDocAndStored = WorkflowAttachment & {
 | 
				
			||||||
 | 
					    genericDoc: GenericDoc & { storedObject: StoredObject };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isAttachmentWithDocAndStored(
 | 
				
			||||||
 | 
					    a: WorkflowAttachment,
 | 
				
			||||||
 | 
					): a is AttachmentWithDocAndStored {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        isWorkflowAttachmentWithGenericDoc(a) &&
 | 
				
			||||||
 | 
					        isGenericDocWithStoredObject(a.genericDoc)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isWorkflowAttachmentWithGenericDoc(
 | 
				
			||||||
 | 
					    attachment: WorkflowAttachment,
 | 
				
			||||||
 | 
					): attachment is WorkflowAttachment & { genericDoc: GenericDoc } {
 | 
				
			||||||
 | 
					    return attachment.genericDoc !== null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Workflow {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    text: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface EntityWorkflowStep {
 | 
				
			||||||
 | 
					    type: "entity_workflow_step";
 | 
				
			||||||
 | 
					    id: number;
 | 
				
			||||||
 | 
					    comment: string;
 | 
				
			||||||
 | 
					    currentStep: StepDefinition;
 | 
				
			||||||
 | 
					    isFinal: boolean;
 | 
				
			||||||
 | 
					    isFreezed: boolean;
 | 
				
			||||||
 | 
					    isFinalized: boolean;
 | 
				
			||||||
 | 
					    transitionPrevious: Transition | null;
 | 
				
			||||||
 | 
					    transitionAfter: Transition | null;
 | 
				
			||||||
 | 
					    previousId: number | null;
 | 
				
			||||||
 | 
					    nextId: number | null;
 | 
				
			||||||
 | 
					    transitionPreviousBy: User | null;
 | 
				
			||||||
 | 
					    transitionPreviousAt: DateTime | null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Transition {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    text: string;
 | 
				
			||||||
 | 
					    isForward: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface StepDefinition {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    text: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface EntityWorkflow {
 | 
				
			||||||
 | 
					    type: "entity_workflow";
 | 
				
			||||||
 | 
					    id: number;
 | 
				
			||||||
 | 
					    relatedEntityClass: string;
 | 
				
			||||||
 | 
					    relatedEntityId: number;
 | 
				
			||||||
 | 
					    workflow: Workflow;
 | 
				
			||||||
 | 
					    currentStep: EntityWorkflowStep;
 | 
				
			||||||
 | 
					    steps: EntityWorkflowStep[];
 | 
				
			||||||
 | 
					    datas: WorkflowData;
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					    isOnHoldAtCurrentStep: boolean;
 | 
				
			||||||
 | 
					    _permissions: {
 | 
				
			||||||
 | 
					        CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT: boolean;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface WorkflowData {
 | 
				
			||||||
 | 
					    persons: Person[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ExportGeneration {
 | 
					export interface ExportGeneration {
 | 
				
			||||||
    id: string;
 | 
					    id: string;
 | 
				
			||||||
    type: "export_generation";
 | 
					    type: "export_generation";
 | 
				
			||||||
@@ -215,3 +290,8 @@ export interface ExportGeneration {
 | 
				
			|||||||
export interface PrivateCommentEmbeddable {
 | 
					export interface PrivateCommentEmbeddable {
 | 
				
			||||||
    comments: Record<number, string>;
 | 
					    comments: Record<number, string>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Possible states for the WaitingScreen Component.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type WaitingScreenState = "pending" | "failure" | "stopped" | "ready";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,8 @@ import { computed, onMounted, ref } from "vue";
 | 
				
			|||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
 | 
					import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
 | 
				
			||||||
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
 | 
					import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
 | 
				
			||||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
					import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
				
			||||||
import { ExportGeneration } from "ChillMainAssets/types";
 | 
					import WaitingScreen from "../_components/WaitingScreen.vue";
 | 
				
			||||||
 | 
					import { ExportGeneration, WaitingScreenState } from "ChillMainAssets/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AppProps {
 | 
					interface AppProps {
 | 
				
			||||||
    exportGenerationId: string;
 | 
					    exportGenerationId: string;
 | 
				
			||||||
@@ -34,13 +35,16 @@ const storedObject = computed<null | StoredObject>(() => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isPending = computed<boolean>(() => status.value === "pending");
 | 
					const isPending = computed<boolean>(() => status.value === "pending");
 | 
				
			||||||
const isFetching = computed<boolean>(
 | 
					 | 
				
			||||||
    () => tryiesForReady.value < maxTryiesForReady,
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
const isReady = computed<boolean>(() => status.value === "ready");
 | 
					 | 
				
			||||||
const isFailure = computed<boolean>(() => status.value === "failure");
 | 
					 | 
				
			||||||
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
 | 
					const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = computed<WaitingScreenState>((): WaitingScreenState => {
 | 
				
			||||||
 | 
					    if (status.value === "empty") {
 | 
				
			||||||
 | 
					        return "pending";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return status.value;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * counter for the number of times that we check for a new status
 | 
					 * counter for the number of times that we check for a new status
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@@ -85,57 +89,36 @@ onMounted(() => {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div id="waiting-screen">
 | 
					    <WaitingScreen :state="state">
 | 
				
			||||||
        <div
 | 
					        <template v-slot:pending>
 | 
				
			||||||
            v-if="isPending && isFetching"
 | 
					            <p>
 | 
				
			||||||
            class="alert alert-danger text-center"
 | 
					                {{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
 | 
				
			||||||
        >
 | 
					            </p>
 | 
				
			||||||
            <div>
 | 
					        </template>
 | 
				
			||||||
                <p>
 | 
					 | 
				
			||||||
                    {{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
 | 
					 | 
				
			||||||
                </p>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div>
 | 
					        <template v-slot:stopped>
 | 
				
			||||||
                <i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
 | 
					            <p>
 | 
				
			||||||
                <span class="sr-only">Loading...</span>
 | 
					                {{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
 | 
				
			||||||
            </div>
 | 
					            </p>
 | 
				
			||||||
        </div>
 | 
					        </template>
 | 
				
			||||||
        <div v-if="isPending && !isFetching" class="alert alert-info">
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
                <p>
 | 
					 | 
				
			||||||
                    {{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
 | 
					 | 
				
			||||||
                </p>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div v-if="isFailure" class="alert alert-danger text-center">
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
                <p>
 | 
					 | 
				
			||||||
                    {{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
 | 
					 | 
				
			||||||
                </p>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div v-if="isReady" class="alert alert-success text-center">
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
                <p>
 | 
					 | 
				
			||||||
                    {{ trans(EXPORT_GENERATION_EXPORT_READY) }}
 | 
					 | 
				
			||||||
                </p>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <p v-if="storedObject !== null">
 | 
					        <template v-slot:failure>
 | 
				
			||||||
                    <document-action-buttons-group
 | 
					            <p>
 | 
				
			||||||
                        :stored-object="storedObject"
 | 
					                {{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
 | 
				
			||||||
                        :filename="filename"
 | 
					            </p>
 | 
				
			||||||
                    ></document-action-buttons-group>
 | 
					        </template>
 | 
				
			||||||
                </p>
 | 
					
 | 
				
			||||||
            </div>
 | 
					        <template v-slot:ready>
 | 
				
			||||||
        </div>
 | 
					            <p>
 | 
				
			||||||
    </div>
 | 
					                {{ trans(EXPORT_GENERATION_EXPORT_READY) }}
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <p v-if="storedObject !== null">
 | 
				
			||||||
 | 
					                <document-action-buttons-group
 | 
				
			||||||
 | 
					                    :stored-object="storedObject"
 | 
				
			||||||
 | 
					                    :filename="filename"
 | 
				
			||||||
 | 
					                ></document-action-buttons-group>
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					    </WaitingScreen>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped lang="scss">
 | 
					 | 
				
			||||||
#waiting-screen {
 | 
					 | 
				
			||||||
    > .alert {
 | 
					 | 
				
			||||||
        min-height: 350px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useIntervalFn } from "@vueuse/core";
 | 
				
			||||||
 | 
					import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
 | 
				
			||||||
 | 
					import { returnPathOr } from "ChillMainAssets/lib/return_path/returnPathHelper";
 | 
				
			||||||
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					import WaitingScreen from "ChillMainAssets/vuejs/_components/WaitingScreen.vue";
 | 
				
			||||||
 | 
					import { WaitingScreenState } from "ChillMainAssets/types";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    trans,
 | 
				
			||||||
 | 
					    WORKFLOW_WAIT_TITLE,
 | 
				
			||||||
 | 
					    WORKFLOW_WAIT_ERROR_WHILE_WAITING,
 | 
				
			||||||
 | 
					    WORKFLOW_WAIT_SUCCESS,
 | 
				
			||||||
 | 
					} from "translator";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface WaitPostProcessWorkflowComponentProps {
 | 
				
			||||||
 | 
					    workflowId: number;
 | 
				
			||||||
 | 
					    expectedStep: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<WaitPostProcessWorkflowComponentProps>();
 | 
				
			||||||
 | 
					const counter = ref<number>(0);
 | 
				
			||||||
 | 
					const MAX_TRYIES = 50;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = ref<WaitingScreenState>("pending");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { pause, resume } = useIntervalFn(
 | 
				
			||||||
 | 
					    async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const workflow = await fetchWorkflow(props.workflowId);
 | 
				
			||||||
 | 
					            counter.value++;
 | 
				
			||||||
 | 
					            if (workflow.currentStep.currentStep.name === props.expectedStep) {
 | 
				
			||||||
 | 
					                window.location.assign(
 | 
				
			||||||
 | 
					                    returnPathOr("/fr/main/workflow" + workflow.id + "/show"),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                resume();
 | 
				
			||||||
 | 
					                state.value = "ready";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (counter.value > MAX_TRYIES) {
 | 
				
			||||||
 | 
					                pause();
 | 
				
			||||||
 | 
					                state.value = "failure";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error(error);
 | 
				
			||||||
 | 
					            pause();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    2000,
 | 
				
			||||||
 | 
					    { immediate: true },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="container">
 | 
				
			||||||
 | 
					        <WaitingScreen :state="state">
 | 
				
			||||||
 | 
					            <template v-slot:pending>
 | 
				
			||||||
 | 
					                <p>
 | 
				
			||||||
 | 
					                    {{ trans(WORKFLOW_WAIT_TITLE) }}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					            <template v-slot:failure>
 | 
				
			||||||
 | 
					                <p>
 | 
				
			||||||
 | 
					                    {{ trans(WORKFLOW_WAIT_ERROR_WHILE_WAITING) }}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					            <template v-slot:ready>
 | 
				
			||||||
 | 
					                <p>
 | 
				
			||||||
 | 
					                    {{ trans(WORKFLOW_WAIT_SUCCESS) }}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </WaitingScreen>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped lang="scss"></style>
 | 
				
			||||||
@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					import { createApp } from "vue";
 | 
				
			||||||
 | 
					import App from "./App.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function mountApp(): void {
 | 
				
			||||||
 | 
					    const el = document.querySelector<HTMLDivElement>(".screen-wait");
 | 
				
			||||||
 | 
					    if (!el) {
 | 
				
			||||||
 | 
					        console.error(
 | 
				
			||||||
 | 
					            "WaitPostProcessWorkflow: mount element .screen-wait not found",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const workflowIdAttr = el.getAttribute("data-workflow-id");
 | 
				
			||||||
 | 
					    const expectedStep = el.getAttribute("data-expected-step") || "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!workflowIdAttr) {
 | 
				
			||||||
 | 
					        console.error(
 | 
				
			||||||
 | 
					            "WaitPostProcessWorkflow: data-workflow-id attribute missing on mount element",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!expectedStep) {
 | 
				
			||||||
 | 
					        console.error(
 | 
				
			||||||
 | 
					            "WaitPostProcessWorkflow: data-expected-step attribute missing on mount element",
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const workflowId = Number(workflowIdAttr);
 | 
				
			||||||
 | 
					    if (Number.isNaN(workflowId)) {
 | 
				
			||||||
 | 
					        console.error(
 | 
				
			||||||
 | 
					            "WaitPostProcessWorkflow: data-workflow-id is not a valid number:",
 | 
				
			||||||
 | 
					            workflowIdAttr,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const app = createApp(App, {
 | 
				
			||||||
 | 
					        workflowId,
 | 
				
			||||||
 | 
					        expectedStep,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app.mount(el);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (document.readyState === "loading") {
 | 
				
			||||||
 | 
					    document.addEventListener("DOMContentLoaded", mountApp);
 | 
				
			||||||
 | 
					} else {
 | 
				
			||||||
 | 
					    mountApp();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,10 +1,12 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { computed, useTemplateRef } from "vue";
 | 
					import { computed, onMounted, ref, useTemplateRef } from "vue";
 | 
				
			||||||
import type { WorkflowAttachment } from "ChillMainAssets/types";
 | 
					import type { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
 | 
				
			||||||
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
 | 
					import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
 | 
				
			||||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
					import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
				
			||||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
 | 
					import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
 | 
				
			||||||
import { GenericDoc } from "ChillDocStoreAssets/types";
 | 
					import { GenericDoc } from "ChillDocStoreAssets/types";
 | 
				
			||||||
 | 
					import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
 | 
				
			||||||
 | 
					import { trans, WORKFLOW_ATTACHMENTS_ADD_AN_ATTACHMENT } from "translator";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AppConfig {
 | 
					interface AppConfig {
 | 
				
			||||||
    workflowId: number;
 | 
					    workflowId: number;
 | 
				
			||||||
@@ -34,6 +36,13 @@ const attachedGenericDoc = computed<GenericDocForAccompanyingPeriod[]>(
 | 
				
			|||||||
            ) as GenericDocForAccompanyingPeriod[],
 | 
					            ) as GenericDocForAccompanyingPeriod[],
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const workflow = ref<EntityWorkflow | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(async () => {
 | 
				
			||||||
 | 
					    workflow.value = await fetchWorkflow(Number(props.workflowId));
 | 
				
			||||||
 | 
					    console.log("workflow", workflow.value);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const openModal = function () {
 | 
					const openModal = function () {
 | 
				
			||||||
    pickDocModal.value?.openModal();
 | 
					    pickDocModal.value?.openModal();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -49,23 +58,33 @@ const onPickGenericDoc = ({
 | 
				
			|||||||
const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
 | 
					const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
 | 
				
			||||||
    emit("removeAttachment", payload);
 | 
					    emit("removeAttachment", payload);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const canEditAttachement = computed<boolean>(() => {
 | 
				
			||||||
 | 
					    if (null === workflow.value) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return workflow.value._permissions.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <pick-generic-doc-modal
 | 
					    <pick-generic-doc-modal
 | 
				
			||||||
 | 
					        :workflow="workflow"
 | 
				
			||||||
        :accompanying-period-id="props.accompanyingPeriodId"
 | 
					        :accompanying-period-id="props.accompanyingPeriodId"
 | 
				
			||||||
        :to-remove="attachedGenericDoc"
 | 
					        :to-remove="attachedGenericDoc"
 | 
				
			||||||
        ref="pickDocModal"
 | 
					        ref="pickDocModal"
 | 
				
			||||||
        @pickGenericDoc="onPickGenericDoc"
 | 
					        @pickGenericDoc="onPickGenericDoc"
 | 
				
			||||||
    ></pick-generic-doc-modal>
 | 
					    ></pick-generic-doc-modal>
 | 
				
			||||||
    <attachment-list
 | 
					    <attachment-list
 | 
				
			||||||
 | 
					        :workflow="workflow"
 | 
				
			||||||
        :attachments="props.attachments"
 | 
					        :attachments="props.attachments"
 | 
				
			||||||
        @removeAttachment="onRemoveAttachment"
 | 
					        @removeAttachment="onRemoveAttachment"
 | 
				
			||||||
    ></attachment-list>
 | 
					    ></attachment-list>
 | 
				
			||||||
    <ul class="record_actions">
 | 
					    <ul v-if="canEditAttachement" class="record_actions">
 | 
				
			||||||
        <li>
 | 
					        <li>
 | 
				
			||||||
            <button type="button" class="btn btn-create" @click="openModal">
 | 
					            <button type="button" class="btn btn-create" @click="openModal">
 | 
				
			||||||
                Ajouter une pièce jointe
 | 
					                {{ trans(WORKFLOW_ATTACHMENTS_ADD_AN_ATTACHMENT) }}
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
    </ul>
 | 
					    </ul>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,18 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { WorkflowAttachment } from "ChillMainAssets/types";
 | 
					import {
 | 
				
			||||||
 | 
					    AttachmentWithDocAndStored,
 | 
				
			||||||
 | 
					    EntityWorkflow,
 | 
				
			||||||
 | 
					    isAttachmentWithDocAndStored,
 | 
				
			||||||
 | 
					    WorkflowAttachment,
 | 
				
			||||||
 | 
					} from "ChillMainAssets/types";
 | 
				
			||||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
 | 
					import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
 | 
				
			||||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
					import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
				
			||||||
 | 
					import { computed } from "vue";
 | 
				
			||||||
 | 
					import { trans, WORKFLOW_ATTACHMENTS_NO_ATTACHMENT } from "translator";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AttachmentListProps {
 | 
					interface AttachmentListProps {
 | 
				
			||||||
    attachments: WorkflowAttachment[];
 | 
					    attachments: WorkflowAttachment[];
 | 
				
			||||||
 | 
					    workflow: EntityWorkflow | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emit = defineEmits<{
 | 
					const emit = defineEmits<{
 | 
				
			||||||
@@ -13,30 +21,43 @@ const emit = defineEmits<{
 | 
				
			|||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<AttachmentListProps>();
 | 
					const props = defineProps<AttachmentListProps>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const notNullAttachments = computed<AttachmentWithDocAndStored[]>(() =>
 | 
				
			||||||
 | 
					    props.attachments.filter(
 | 
				
			||||||
 | 
					        (a: WorkflowAttachment): a is AttachmentWithDocAndStored =>
 | 
				
			||||||
 | 
					            isAttachmentWithDocAndStored(a),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const canRemove = computed<boolean>((): boolean => {
 | 
				
			||||||
 | 
					    if (null === props.workflow) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return props.workflow._permissions.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <p
 | 
					    <p
 | 
				
			||||||
        v-if="props.attachments.length === 0"
 | 
					        v-if="notNullAttachments.length === 0"
 | 
				
			||||||
        class="chill-no-data-statement text-center"
 | 
					        class="chill-no-data-statement text-center"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
        Aucune pièce jointe
 | 
					        {{ trans(WORKFLOW_ATTACHMENTS_NO_ATTACHMENT) }}
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
    <!-- TODO translate -->
 | 
					    <div v-else class="flex-table">
 | 
				
			||||||
    <div else class="flex-table">
 | 
					        <div v-for="a in notNullAttachments" :key="a.id" class="item-bloc">
 | 
				
			||||||
        <div v-for="a in props.attachments" :key="a.id" class="item-bloc">
 | 
					 | 
				
			||||||
            <generic-doc-item-box
 | 
					            <generic-doc-item-box
 | 
				
			||||||
                v-if="a.genericDoc !== null"
 | 
					 | 
				
			||||||
                :generic-doc="a.genericDoc"
 | 
					                :generic-doc="a.genericDoc"
 | 
				
			||||||
            ></generic-doc-item-box>
 | 
					            ></generic-doc-item-box>
 | 
				
			||||||
            <div class="item-row separator">
 | 
					            <div class="item-row separator">
 | 
				
			||||||
                <ul class="record_actions">
 | 
					                <ul class="record_actions">
 | 
				
			||||||
                    <li v-if="a.genericDoc?.storedObject !== null">
 | 
					                    <li>
 | 
				
			||||||
                        <document-action-buttons-group
 | 
					                        <document-action-buttons-group
 | 
				
			||||||
                            :stored-object="a.genericDoc.storedObject"
 | 
					                            :stored-object="a.genericDoc.storedObject"
 | 
				
			||||||
                        ></document-action-buttons-group>
 | 
					                        ></document-action-buttons-group>
 | 
				
			||||||
                    </li>
 | 
					                    </li>
 | 
				
			||||||
                    <li>
 | 
					                    <li v-if="canRemove">
 | 
				
			||||||
                        <button
 | 
					                        <button
 | 
				
			||||||
                            type="button"
 | 
					                            type="button"
 | 
				
			||||||
                            class="btn btn-delete"
 | 
					                            class="btn btn-delete"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
					import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface GenericDocItemBoxProps {
 | 
					interface GenericDocItemBoxProps {
 | 
				
			||||||
    genericDoc: GenericDocForAccompanyingPeriod;
 | 
					    genericDoc: GenericDoc;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<GenericDocItemBoxProps>();
 | 
					const props = defineProps<GenericDocItemBoxProps>();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,10 @@ import {
 | 
				
			|||||||
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
 | 
					import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
 | 
				
			||||||
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
 | 
					import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
 | 
				
			||||||
import { computed, onMounted, ref } from "vue";
 | 
					import { computed, onMounted, ref } from "vue";
 | 
				
			||||||
 | 
					import { EntityWorkflow } from "ChillMainAssets/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface PickGenericDocProps {
 | 
					interface PickGenericDocProps {
 | 
				
			||||||
 | 
					    workflow: EntityWorkflow | null;
 | 
				
			||||||
    accompanyingPeriodId: number;
 | 
					    accompanyingPeriodId: number;
 | 
				
			||||||
    pickedList: GenericDocForAccompanyingPeriod[];
 | 
					    pickedList: GenericDocForAccompanyingPeriod[];
 | 
				
			||||||
    toRemove: GenericDocForAccompanyingPeriod[];
 | 
					    toRemove: GenericDocForAccompanyingPeriod[];
 | 
				
			||||||
@@ -36,9 +38,21 @@ const isPicked = (genericDoc: GenericDocForAccompanyingPeriod): boolean =>
 | 
				
			|||||||
    ) !== -1;
 | 
					    ) !== -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(async () => {
 | 
					onMounted(async () => {
 | 
				
			||||||
    genericDocs.value = await fetch_generic_docs_by_accompanying_period(
 | 
					    const fetchedGenericDocs = await fetch_generic_docs_by_accompanying_period(
 | 
				
			||||||
        props.accompanyingPeriodId,
 | 
					        props.accompanyingPeriodId,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					    const documentClasses = [
 | 
				
			||||||
 | 
					        "Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument",
 | 
				
			||||||
 | 
					        "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
 | 
				
			||||||
 | 
					        "Chill\\DocStoreBundle\\Entity\\PersonDocument",
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    genericDocs.value = fetchedGenericDocs.filter(
 | 
				
			||||||
 | 
					        (doc) =>
 | 
				
			||||||
 | 
					            !documentClasses.includes(
 | 
				
			||||||
 | 
					                props.workflow?.relatedEntityClass || "",
 | 
				
			||||||
 | 
					            ) || props.workflow?.relatedEntityId !== doc.identifiers.id,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    loaded.value = true;
 | 
					    loaded.value = true;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,10 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
 | 
				
			|||||||
import { computed, ref, useTemplateRef } from "vue";
 | 
					import { computed, ref, useTemplateRef } from "vue";
 | 
				
			||||||
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
 | 
					import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
 | 
				
			||||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
					import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
				
			||||||
 | 
					import { EntityWorkflow } from "ChillMainAssets/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface PickGenericDocModalProps {
 | 
					interface PickGenericDocModalProps {
 | 
				
			||||||
 | 
					    workflow: EntityWorkflow | null;
 | 
				
			||||||
    accompanyingPeriodId: number;
 | 
					    accompanyingPeriodId: number;
 | 
				
			||||||
    toRemove: GenericDocForAccompanyingPeriod[];
 | 
					    toRemove: GenericDocForAccompanyingPeriod[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -80,6 +82,7 @@ defineExpose({ openModal, closeModal });
 | 
				
			|||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
        <template v-slot:body>
 | 
					        <template v-slot:body>
 | 
				
			||||||
            <pick-generic-doc
 | 
					            <pick-generic-doc
 | 
				
			||||||
 | 
					                :workflow="props.workflow"
 | 
				
			||||||
                :accompanying-period-id="props.accompanyingPeriodId"
 | 
					                :accompanying-period-id="props.accompanyingPeriodId"
 | 
				
			||||||
                :to-remove="props.toRemove"
 | 
					                :to-remove="props.toRemove"
 | 
				
			||||||
                :picked-list="pickeds"
 | 
					                :picked-list="pickeds"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { WaitingScreenState } from "ChillMainAssets/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					    state: WaitingScreenState;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<Props>();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div id="waiting-screen">
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            v-if="props.state === 'pending' && !!$slots.pending"
 | 
				
			||||||
 | 
					            class="alert alert-danger text-center"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <slot name="pending"></slot>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
 | 
				
			||||||
 | 
					                <span class="sr-only">Loading...</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            v-if="props.state === 'stopped' && !!$slots.stopped"
 | 
				
			||||||
 | 
					            class="alert alert-info"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <slot name="stopped"></slot>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            v-if="props.state === 'failure' && !!$slots.failure"
 | 
				
			||||||
 | 
					            class="alert alert-danger text-center"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <slot name="failure"></slot>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            v-if="props.state === 'ready' && !!$slots.ready"
 | 
				
			||||||
 | 
					            class="alert alert-success text-center"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                <slot name="ready"></slot>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped lang="scss">
 | 
				
			||||||
 | 
					#waiting-screen {
 | 
				
			||||||
 | 
					    > .alert {
 | 
				
			||||||
 | 
					        min-height: 350px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					{% if chill_main_config.top_banner is defined and chill_main_config.top_banner.text is defined %}
 | 
				
			||||||
 | 
					    {% set banner_text = '' %}
 | 
				
			||||||
 | 
					    {% set current_locale = app.request.locale %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {% if chill_main_config.top_banner.text[current_locale] is defined %}
 | 
				
			||||||
 | 
					        {% set banner_text = chill_main_config.top_banner.text[current_locale] %}
 | 
				
			||||||
 | 
					    {% else %}
 | 
				
			||||||
 | 
					        {% set banner_text = chill_main_config.top_banner.text|first %}
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {% if banner_text %}
 | 
				
			||||||
 | 
					        <div class="top-banner w-100 text-center py-2"
 | 
				
			||||||
 | 
					             style="{% if chill_main_config.top_banner.color is defined %}color: {{ chill_main_config.top_banner.color }};{% endif %}{% if chill_main_config.top_banner.background_color is defined %}background-color: {{ chill_main_config.top_banner.background_color }};{% endif %}">
 | 
				
			||||||
 | 
					            {{ banner_text }}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
@@ -21,8 +21,6 @@
 | 
				
			|||||||
    {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
 | 
					    {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
 | 
				
			||||||
    {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
 | 
					    {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {{ form_row(form.addressesEmails) }}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {% include handler.template(notification) with handler.templateData(notification) %}
 | 
					    {% include handler.template(notification) with handler.templateData(notification) %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="mb-3 row">
 | 
					    <div class="mb-3 row">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,12 +58,14 @@
 | 
				
			|||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {% if signatures|length > 0 %}
 | 
				
			||||||
 | 
					    <section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
 | 
					    <section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
 | 
					    <section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
 | 
				
			||||||
    {% if signatures|length > 0 %}
 | 
					    {% if entity_workflow.currentStep.sends|length > 0 %}
 | 
				
			||||||
        <section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
 | 
					 | 
				
			||||||
    {% elseif entity_workflow.currentStep.sends|length > 0 %}
 | 
					 | 
				
			||||||
        <section class="step my-4">
 | 
					        <section class="step my-4">
 | 
				
			||||||
            <h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
 | 
					            <h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
 | 
				
			||||||
            {% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
 | 
					            {% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					{% extends '@ChillMain/layout.html.twig' %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block title %}{{ 'workflow.signature.waiting_for'|trans }}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block css %}
 | 
				
			||||||
 | 
					    {{ encore_entry_link_tags('page_workflow_waiting_post_process') }}
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block js %}
 | 
				
			||||||
 | 
					    {{ encore_entry_script_tags('page_workflow_waiting_post_process') }}
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					    <h1>{{ block('title') }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="screen-wait" data-workflow-id="{{ workflow.id|e('html_attr') }}" data-expected-step="{{ expectedStep|e('html_attr') }}"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
@@ -26,6 +26,10 @@
 | 
				
			|||||||
</head>
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
 | 
					    {% if chill_main_config.top_banner is defined and chill_main_config.top_banner.visible is true %}
 | 
				
			||||||
 | 
					        {{ include('@ChillMain/Layout/_top_banner.html.twig') }}
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {% if responsive_debug is defined and responsive_debug == 1 %}
 | 
					    {% if responsive_debug is defined and responsive_debug == 1 %}
 | 
				
			||||||
        {{ include('@ChillMain/Layout/_debug.html.twig') }}
 | 
					        {{ include('@ChillMain/Layout/_debug.html.twig') }}
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Chill is a software for social workers
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * For the full copyright and license information, please view
 | 
				
			||||||
 | 
					 * the LICENSE file that was distributed with this source code.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Chill\MainBundle\Security\Authorization;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
 | 
					use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
				
			||||||
 | 
					use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\Registry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class EntityWorkflowAttachmentVoter extends Voter
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        private readonly Registry $registry,
 | 
				
			||||||
 | 
					    ) {}
 | 
				
			||||||
 | 
					    public const EDIT = 'CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected function supports(string $attribute, $subject): bool
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $subject instanceof EntityWorkflow && self::EDIT === $attribute;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (!$subject instanceof EntityWorkflow) {
 | 
				
			||||||
 | 
					            throw new \UnexpectedValueException('Subject must be an instance of EntityWorkflow');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($subject->isFinal()) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $workflow = $this->registry->get($subject, $subject->getWorkflowName());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $marking = $workflow->getMarking($subject);
 | 
				
			||||||
 | 
					        foreach ($marking->getPlaces() as $place => $int) {
 | 
				
			||||||
 | 
					            $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
 | 
				
			||||||
 | 
					            if ($placeMetadata['isSentExternal'] ?? false) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -12,18 +12,25 @@ declare(strict_types=1);
 | 
				
			|||||||
namespace Chill\MainBundle\Serializer\Normalizer;
 | 
					namespace Chill\MainBundle\Serializer\Normalizer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Security\Authorization\EntityWorkflowAttachmentVoter;
 | 
				
			||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
					use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
				
			||||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
 | 
					use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
 | 
				
			||||||
 | 
					use Symfony\Component\Security\Core\Security;
 | 
				
			||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
 | 
					use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
 | 
				
			||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
 | 
					use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
 | 
				
			||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
					use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
				
			||||||
use Symfony\Component\Workflow\Registry;
 | 
					use Symfony\Component\Workflow\Registry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
 | 
					final class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    use NormalizerAwareTrait;
 | 
					    use NormalizerAwareTrait;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry) {}
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        private readonly EntityWorkflowManager $entityWorkflowManager,
 | 
				
			||||||
 | 
					        private readonly MetadataExtractor $metadataExtractor,
 | 
				
			||||||
 | 
					        private readonly Registry $registry,
 | 
				
			||||||
 | 
					        private readonly Security $security,
 | 
				
			||||||
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @param EntityWorkflow $object
 | 
					     * @param EntityWorkflow $object
 | 
				
			||||||
@@ -46,6 +53,9 @@ class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareIn
 | 
				
			|||||||
            'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
 | 
					            'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
 | 
				
			||||||
            'title' => $handler->getEntityTitle($object),
 | 
					            'title' => $handler->getEntityTitle($object),
 | 
				
			||||||
            'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
 | 
					            'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
 | 
				
			||||||
 | 
					            '_permissions' => [
 | 
				
			||||||
 | 
					                EntityWorkflowAttachmentVoter::EDIT => $this->security->isGranted(EntityWorkflowAttachmentVoter::EDIT, $object),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@ class AddressReferenceBEFromBestAddress
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $uncompressedStream = gzopen($tmpname, 'r');
 | 
					        $uncompressedStream = gzopen($tmpname, 'r');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Reader::createFromStream($uncompressedStream);
 | 
					        $csv = Reader::from($uncompressedStream);
 | 
				
			||||||
        $csv->setDelimiter(',');
 | 
					        $csv->setDelimiter(',');
 | 
				
			||||||
        $csv->setHeaderOffset(0);
 | 
					        $csv->setHeaderOffset(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -287,7 +287,7 @@ final class AddressReferenceBaseImporter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            $filename = sprintf('%s-%s.csv', (new \DateTimeImmutable())->format('Ymd-His'), uniqid());
 | 
					            $filename = sprintf('%s-%s.csv', (new \DateTimeImmutable())->format('Ymd-His'), uniqid());
 | 
				
			||||||
            $path = Path::normalize(sprintf('%s%s%s', sys_get_temp_dir(), DIRECTORY_SEPARATOR, $filename));
 | 
					            $path = Path::normalize(sprintf('%s%s%s', sys_get_temp_dir(), DIRECTORY_SEPARATOR, $filename));
 | 
				
			||||||
            $writer = Writer::createFromPath($path, 'w+');
 | 
					            $writer = Writer::from($path, 'w+');
 | 
				
			||||||
            // insert headers
 | 
					            // insert headers
 | 
				
			||||||
            $writer->insertOne([
 | 
					            $writer->insertOne([
 | 
				
			||||||
                'postalcode',
 | 
					                'postalcode',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,7 +53,7 @@ class AddressReferenceFromBAN
 | 
				
			|||||||
        // re-open it to read it
 | 
					        // re-open it to read it
 | 
				
			||||||
        $csvDecompressed = gzopen($path, 'r');
 | 
					        $csvDecompressed = gzopen($path, 'r');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Reader::createFromStream($csvDecompressed);
 | 
					        $csv = Reader::from($csvDecompressed);
 | 
				
			||||||
        $csv->setDelimiter(';')->setHeaderOffset(0);
 | 
					        $csv->setDelimiter(';')->setHeaderOffset(0);
 | 
				
			||||||
        $stmt = new Statement();
 | 
					        $stmt = new Statement();
 | 
				
			||||||
        $stmt = $stmt->process($csv, [
 | 
					        $stmt = $stmt->process($csv, [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,7 @@ class AddressReferenceFromBano
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        fseek($file, 0);
 | 
					        fseek($file, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Reader::createFromStream($file);
 | 
					        $csv = Reader::from($file);
 | 
				
			||||||
        $csv->setDelimiter(',');
 | 
					        $csv->setDelimiter(',');
 | 
				
			||||||
        $stmt = new Statement();
 | 
					        $stmt = new Statement();
 | 
				
			||||||
        $stmt = $stmt->process($csv, [
 | 
					        $stmt = $stmt->process($csv, [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,7 +39,7 @@ class AddressReferenceLU
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        fseek($file, 0);
 | 
					        fseek($file, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Reader::createFromStream($file);
 | 
					        $csv = Reader::from($file);
 | 
				
			||||||
        $csv->setDelimiter(';');
 | 
					        $csv->setDelimiter(';');
 | 
				
			||||||
        $csv->setHeaderOffset(0);
 | 
					        $csv->setHeaderOffset(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,7 @@ class PostalCodeBEFromBestAddress
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $uncompressedStream = gzopen($tmpname, 'r');
 | 
					        $uncompressedStream = gzopen($tmpname, 'r');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Reader::createFromStream($uncompressedStream);
 | 
					        $csv = Reader::from($uncompressedStream);
 | 
				
			||||||
        $csv->setDelimiter(',');
 | 
					        $csv->setDelimiter(',');
 | 
				
			||||||
        $csv->setHeaderOffset(0);
 | 
					        $csv->setHeaderOffset(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,7 +47,7 @@ class PostalCodeFRFromOpenData
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        fseek($tmpfile, 0);
 | 
					        fseek($tmpfile, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Reader::createFromStream($tmpfile);
 | 
					        $csv = Reader::from($tmpfile);
 | 
				
			||||||
        $csv->setDelimiter(',');
 | 
					        $csv->setDelimiter(',');
 | 
				
			||||||
        $csv->setHeaderOffset(0);
 | 
					        $csv->setHeaderOffset(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@ use Symfony\Component\Notifier\Event\SentMessageEvent;
 | 
				
			|||||||
final readonly class SentMessageEventSubscriber implements EventSubscriberInterface
 | 
					final readonly class SentMessageEventSubscriber implements EventSubscriberInterface
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public function __construct(
 | 
					    public function __construct(
 | 
				
			||||||
        private LoggerInterface $logger,
 | 
					        private LoggerInterface $notifierLogger, // will be send to "notifierLogger" if it exists
 | 
				
			||||||
    ) {}
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static function getSubscribedEvents()
 | 
					    public static function getSubscribedEvents()
 | 
				
			||||||
@@ -33,9 +33,9 @@ final readonly class SentMessageEventSubscriber implements EventSubscriberInterf
 | 
				
			|||||||
        $message = $event->getMessage();
 | 
					        $message = $event->getMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (null === $message->getMessageId()) {
 | 
					        if (null === $message->getMessageId()) {
 | 
				
			||||||
            $this->logger->info('[sms] a sms message did not had any id after sending.', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId()]);
 | 
					            $this->notifierLogger->info('[sms] a sms message did not had any id after sending.', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId()]);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            $this->logger->warning('[sms] a sms was sent', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId(), 'idsI' => $message->getMessageId()]);
 | 
					            $this->notifierLogger->warning('[sms] a sms was sent', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId(), 'idsI' => $message->getMessageId()]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,98 @@
 | 
				
			|||||||
 | 
					<?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\Tests\DependencyInjection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\MainBundle\DependencyInjection\Configuration;
 | 
				
			||||||
 | 
					use PHPUnit\Framework\TestCase;
 | 
				
			||||||
 | 
					use Symfony\Component\Config\Definition\Processor;
 | 
				
			||||||
 | 
					use Symfony\Component\DependencyInjection\ContainerBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @internal
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @coversNothing
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class ConfigurationTest extends TestCase
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function testTopBannerConfiguration(): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $containerBuilder = new ContainerBuilder();
 | 
				
			||||||
 | 
					        $configuration = new Configuration([], $containerBuilder);
 | 
				
			||||||
 | 
					        $processor = new Processor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Test with top_banner configuration
 | 
				
			||||||
 | 
					        $config = [
 | 
				
			||||||
 | 
					            'chill_main' => [
 | 
				
			||||||
 | 
					                'top_banner' => [
 | 
				
			||||||
 | 
					                    'text' => [
 | 
				
			||||||
 | 
					                        'fr' => 'Vous travaillez actuellement avec la version de pré-production de Chill.',
 | 
				
			||||||
 | 
					                        'nl' => 'Je werkte momenteel in de pré-productie versie van Chill.',
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    'color' => 'white',
 | 
				
			||||||
 | 
					                    'background-color' => 'red',
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $processedConfig = $processor->processConfiguration($configuration, $config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::assertArrayHasKey('top_banner', $processedConfig);
 | 
				
			||||||
 | 
					        self::assertArrayHasKey('text', $processedConfig['top_banner']);
 | 
				
			||||||
 | 
					        self::assertArrayHasKey('fr', $processedConfig['top_banner']['text']);
 | 
				
			||||||
 | 
					        self::assertArrayHasKey('nl', $processedConfig['top_banner']['text']);
 | 
				
			||||||
 | 
					        self::assertSame('white', $processedConfig['top_banner']['color']);
 | 
				
			||||||
 | 
					        self::assertSame('red', $processedConfig['top_banner']['background_color']);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function testTopBannerConfigurationOptional(): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $containerBuilder = new ContainerBuilder();
 | 
				
			||||||
 | 
					        $configuration = new Configuration([], $containerBuilder);
 | 
				
			||||||
 | 
					        $processor = new Processor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Test without top_banner configuration
 | 
				
			||||||
 | 
					        $config = [
 | 
				
			||||||
 | 
					            'chill_main' => [],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $processedConfig = $processor->processConfiguration($configuration, $config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // top_banner should not be present when not configured
 | 
				
			||||||
 | 
					        self::assertArrayNotHasKey('top_banner', $processedConfig);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function testTopBannerWithMinimalConfiguration(): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $containerBuilder = new ContainerBuilder();
 | 
				
			||||||
 | 
					        $configuration = new Configuration([], $containerBuilder);
 | 
				
			||||||
 | 
					        $processor = new Processor();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Test with minimal top_banner configuration (only text)
 | 
				
			||||||
 | 
					        $config = [
 | 
				
			||||||
 | 
					            'chill_main' => [
 | 
				
			||||||
 | 
					                'top_banner' => [
 | 
				
			||||||
 | 
					                    'text' => [
 | 
				
			||||||
 | 
					                        'fr' => 'Test message',
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $processedConfig = $processor->processConfiguration($configuration, $config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::assertArrayHasKey('top_banner', $processedConfig);
 | 
				
			||||||
 | 
					        self::assertArrayHasKey('text', $processedConfig['top_banner']);
 | 
				
			||||||
 | 
					        self::assertSame('Test message', $processedConfig['top_banner']['text']['fr']);
 | 
				
			||||||
 | 
					        self::assertNull($processedConfig['top_banner']['color']);
 | 
				
			||||||
 | 
					        self::assertNull($processedConfig['top_banner']['background_color']);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -37,10 +37,5 @@ class DailyNotificationDigestCronJobFunctionalTest extends KernelTestCase
 | 
				
			|||||||
        $actual = $this->dailyNotificationDigestCronjob->run([]);
 | 
					        $actual = $this->dailyNotificationDigestCronjob->run([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self::assertArrayHasKey('last_execution', $actual);
 | 
					        self::assertArrayHasKey('last_execution', $actual);
 | 
				
			||||||
        self::assertInstanceOf(
 | 
					 | 
				
			||||||
            \DateTimeImmutable::class,
 | 
					 | 
				
			||||||
            \DateTimeImmutable::createFromFormat('Y-m-d-H:i:s.u e', $actual['last_execution']),
 | 
					 | 
				
			||||||
            'test that the string can be converted to a date'
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,16 +12,21 @@ declare(strict_types=1);
 | 
				
			|||||||
namespace Chill\MainBundle\Tests\Notification\Email;
 | 
					namespace Chill\MainBundle\Tests\Notification\Email;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Chill\MainBundle\Notification\Email\DailyNotificationDigestCronjob;
 | 
					use Chill\MainBundle\Notification\Email\DailyNotificationDigestCronjob;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
 | 
				
			||||||
use Doctrine\DBAL\Connection;
 | 
					use Doctrine\DBAL\Connection;
 | 
				
			||||||
 | 
					use Doctrine\DBAL\Result;
 | 
				
			||||||
 | 
					use Doctrine\DBAL\Statement;
 | 
				
			||||||
use PHPUnit\Framework\TestCase;
 | 
					use PHPUnit\Framework\TestCase;
 | 
				
			||||||
use Psr\Log\LoggerInterface;
 | 
					use Psr\Log\LoggerInterface;
 | 
				
			||||||
use Symfony\Component\Clock\ClockInterface;
 | 
					use Symfony\Component\Clock\ClockInterface;
 | 
				
			||||||
 | 
					use Symfony\Component\Clock\MockClock;
 | 
				
			||||||
 | 
					use Symfony\Component\Messenger\Envelope;
 | 
				
			||||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
					use Symfony\Component\Messenger\MessageBusInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @internal
 | 
					 * @internal
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @coversNothing
 | 
					 * @covers \DailyNotificationDigestCronjob
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class DailyNotificationDigestCronJobTest extends TestCase
 | 
					class DailyNotificationDigestCronJobTest extends TestCase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -30,6 +35,7 @@ class DailyNotificationDigestCronJobTest extends TestCase
 | 
				
			|||||||
    private MessageBusInterface $messageBus;
 | 
					    private MessageBusInterface $messageBus;
 | 
				
			||||||
    private LoggerInterface $logger;
 | 
					    private LoggerInterface $logger;
 | 
				
			||||||
    private DailyNotificationDigestCronjob $cronjob;
 | 
					    private DailyNotificationDigestCronjob $cronjob;
 | 
				
			||||||
 | 
					    private \DateTimeImmutable $firstNow;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected function setUp(): void
 | 
					    protected function setUp(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -38,6 +44,8 @@ class DailyNotificationDigestCronJobTest extends TestCase
 | 
				
			|||||||
        $this->messageBus = $this->createMock(MessageBusInterface::class);
 | 
					        $this->messageBus = $this->createMock(MessageBusInterface::class);
 | 
				
			||||||
        $this->logger = $this->createMock(LoggerInterface::class);
 | 
					        $this->logger = $this->createMock(LoggerInterface::class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->firstNow = new \DateTimeImmutable('2024-01-02T07:15:00+00:00');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->cronjob = new DailyNotificationDigestCronjob(
 | 
					        $this->cronjob = new DailyNotificationDigestCronjob(
 | 
				
			||||||
            $this->clock,
 | 
					            $this->clock,
 | 
				
			||||||
            $this->connection,
 | 
					            $this->connection,
 | 
				
			||||||
@@ -78,4 +86,129 @@ class DailyNotificationDigestCronJobTest extends TestCase
 | 
				
			|||||||
            'hour 23 - should not run' => [23, false],
 | 
					            'hour 23 - should not run' => [23, false],
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function testRunFirstExecutionReturnsStateAndDispatches(): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Use MockClock for deterministic time
 | 
				
			||||||
 | 
					        $firstNow = $this->firstNow;
 | 
				
			||||||
 | 
					        $clock = new MockClock($firstNow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Mock DBAL statement/result
 | 
				
			||||||
 | 
					        $statement = $this->createMock(Statement::class);
 | 
				
			||||||
 | 
					        $result = $this->createMock(Result::class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->connection->method('prepare')->willReturn($statement);
 | 
				
			||||||
 | 
					        $statement->method('bindValue')->willReturnSelf();
 | 
				
			||||||
 | 
					        $statement->method('executeQuery')->willReturn($result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $rows = [
 | 
				
			||||||
 | 
					            ['user_id' => 10],
 | 
				
			||||||
 | 
					            ['user_id' => 42],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        $result->method('fetchAllAssociative')->willReturn($rows);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $dispatched = [];
 | 
				
			||||||
 | 
					        $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$dispatched) {
 | 
				
			||||||
 | 
					            $dispatched[] = $message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return new Envelope($message);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger);
 | 
				
			||||||
 | 
					        $state = $cron->run([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Assert dispatch count and message contents
 | 
				
			||||||
 | 
					        self::assertCount(2, $dispatched);
 | 
				
			||||||
 | 
					        $expectedLast = $firstNow->sub(new \DateInterval('P1D'));
 | 
				
			||||||
 | 
					        foreach ($dispatched as $i => $msg) {
 | 
				
			||||||
 | 
					            self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg);
 | 
				
			||||||
 | 
					            self::assertTrue(in_array($msg->getUserId(), [10, 42], true));
 | 
				
			||||||
 | 
					            self::assertEquals($firstNow, $msg->getCurrentDateTime(), 'compare the current date');
 | 
				
			||||||
 | 
					            self::assertEquals($expectedLast, $msg->getLastExecutionDateTime(), 'compare the last execution date');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Assert returned state
 | 
				
			||||||
 | 
					        self::assertIsArray($state);
 | 
				
			||||||
 | 
					        self::assertArrayHasKey('last_execution', $state);
 | 
				
			||||||
 | 
					        self::assertSame($firstNow->format(\DateTimeInterface::ATOM), $state['last_execution']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $state;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @depends testRunFirstExecutionReturnsStateAndDispatches
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function testRunSecondExecutionUsesPreviousState(array $previousState): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $firstNow = $this->firstNow;
 | 
				
			||||||
 | 
					        $secondNow = $firstNow->add(new \DateInterval('P1D'));
 | 
				
			||||||
 | 
					        $clock = new MockClock($secondNow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Mock DBAL for a single user this time
 | 
				
			||||||
 | 
					        $statement = $this->createMock(Statement::class);
 | 
				
			||||||
 | 
					        $result = $this->createMock(Result::class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->connection->method('prepare')->willReturn($statement);
 | 
				
			||||||
 | 
					        $statement->method('bindValue')->willReturnSelf();
 | 
				
			||||||
 | 
					        $statement->method('executeQuery')->willReturn($result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $rows = [
 | 
				
			||||||
 | 
					            ['user_id' => 7],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        $result->method('fetchAllAssociative')->willReturn($rows);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $captured = [];
 | 
				
			||||||
 | 
					        $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$captured) {
 | 
				
			||||||
 | 
					            $captured[] = $message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return new Envelope($message);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger);
 | 
				
			||||||
 | 
					        $cron->run($previousState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::assertCount(1, $captured);
 | 
				
			||||||
 | 
					        $msg = $captured[0];
 | 
				
			||||||
 | 
					        self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg);
 | 
				
			||||||
 | 
					        self::assertEquals(7, $msg->getUserId());
 | 
				
			||||||
 | 
					        self::assertEquals($secondNow, $msg->getCurrentDateTime(), 'compare the current date');
 | 
				
			||||||
 | 
					        self::assertEquals($firstNow, $msg->getLastExecutionDateTime(), 'compare the last execution date');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function testRunWithInvalidExecutionState(): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $firstNow = new \DateTimeImmutable('2025-10-14T10:30:00 Europe/Brussels');
 | 
				
			||||||
 | 
					        $previousExpected = $firstNow->sub(new \DateInterval('P1D'));
 | 
				
			||||||
 | 
					        $clock = new MockClock($firstNow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Mock DBAL for a single user this time
 | 
				
			||||||
 | 
					        $statement = $this->createMock(Statement::class);
 | 
				
			||||||
 | 
					        $result = $this->createMock(Result::class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->connection->method('prepare')->willReturn($statement);
 | 
				
			||||||
 | 
					        $statement->method('bindValue')->willReturnSelf();
 | 
				
			||||||
 | 
					        $statement->method('executeQuery')->willReturn($result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $rows = [
 | 
				
			||||||
 | 
					            ['user_id' => 7],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        $result->method('fetchAllAssociative')->willReturn($rows);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $captured = [];
 | 
				
			||||||
 | 
					        $this->messageBus->method('dispatch')->willReturnCallback(function ($message) use (&$captured) {
 | 
				
			||||||
 | 
					            $captured[] = $message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return new Envelope($message);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $cron = new DailyNotificationDigestCronjob($clock, $this->connection, $this->messageBus, $this->logger);
 | 
				
			||||||
 | 
					        $cron->run(['last_execution' => 'invalid data']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::assertCount(1, $captured);
 | 
				
			||||||
 | 
					        $msg = $captured[0];
 | 
				
			||||||
 | 
					        self::assertInstanceOf(ScheduleDailyNotificationDigestMessage::class, $msg);
 | 
				
			||||||
 | 
					        self::assertEquals(7, $msg->getUserId());
 | 
				
			||||||
 | 
					        self::assertEquals($firstNow, $msg->getCurrentDateTime(), 'compare the current date');
 | 
				
			||||||
 | 
					        self::assertEquals($previousExpected, $msg->getLastExecutionDateTime(), 'compare the last execution date');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,173 @@
 | 
				
			|||||||
 | 
					<?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\Tests\Security\Authorization;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\User;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Security\Authorization\EntityWorkflowAttachmentVoter;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
				
			||||||
 | 
					use PHPUnit\Framework\TestCase;
 | 
				
			||||||
 | 
					use Prophecy\PhpUnit\ProphecyTrait;
 | 
				
			||||||
 | 
					use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
 | 
				
			||||||
 | 
					use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\DefinitionBuilder;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\Registry;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\Transition;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\Workflow;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\WorkflowInterface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @internal
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @coversNothing
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class EntityWorkflowAttachmentVoterTest extends TestCase
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    use ProphecyTrait;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @dataProvider dataVoteOnAttribute
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function testVoteOnAttribute(EntityWorkflow $entityWorkflow, int $expected): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $voter = new EntityWorkflowAttachmentVoter($this->buildRegistry());
 | 
				
			||||||
 | 
					        $actual = $voter->vote(
 | 
				
			||||||
 | 
					            new UsernamePasswordToken(new User(), 'default'),
 | 
				
			||||||
 | 
					            $entityWorkflow,
 | 
				
			||||||
 | 
					            ['CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT'],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        $this->assertEquals($expected, $actual);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function dataVoteOnAttribute(): iterable
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $entity = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $entity->setWorkflowName('dummy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $workflow = static::buildRegistry()->get($entity, 'dummy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entity);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = new User();
 | 
				
			||||||
 | 
					        $workflow->apply(
 | 
				
			||||||
 | 
					            $entity,
 | 
				
			||||||
 | 
					            'to_final_positive',
 | 
				
			||||||
 | 
					            ['context' => $dto,
 | 
				
			||||||
 | 
					                'byUser' => new User(),
 | 
				
			||||||
 | 
					                'transition' => 'to_final_positive',
 | 
				
			||||||
 | 
					                'transitionAt' => new \DateTimeImmutable()],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        // we need to mark manually as final, as the listener is not registered
 | 
				
			||||||
 | 
					        $entity->getCurrentStep()->setIsFinal(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield 'on final positive' => [
 | 
				
			||||||
 | 
					            $entity,
 | 
				
			||||||
 | 
					            VoterInterface::ACCESS_DENIED,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entity = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $entity->setWorkflowName('dummy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $workflow = static::buildRegistry()->get($entity, 'dummy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entity);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = new User();
 | 
				
			||||||
 | 
					        $workflow->apply(
 | 
				
			||||||
 | 
					            $entity,
 | 
				
			||||||
 | 
					            'to_final_negative',
 | 
				
			||||||
 | 
					            ['context' => $dto,
 | 
				
			||||||
 | 
					                'byUser' => new User(),
 | 
				
			||||||
 | 
					                'transition' => 'to_final_negative',
 | 
				
			||||||
 | 
					                'transitionAt' => new \DateTimeImmutable()],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        // we need to mark manually as final, as the listener is not registered
 | 
				
			||||||
 | 
					        $entity->getCurrentStep()->setIsFinal(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield 'on final negative' => [
 | 
				
			||||||
 | 
					            $entity,
 | 
				
			||||||
 | 
					            VoterInterface::ACCESS_DENIED,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entity = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $entity->setWorkflowName('dummy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $workflow = static::buildRegistry()->get($entity, 'dummy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entity);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = new User();
 | 
				
			||||||
 | 
					        $workflow->apply(
 | 
				
			||||||
 | 
					            $entity,
 | 
				
			||||||
 | 
					            'to_sent_external',
 | 
				
			||||||
 | 
					            ['context' => $dto,
 | 
				
			||||||
 | 
					                'byUser' => new User(),
 | 
				
			||||||
 | 
					                'transition' => 'to_sent_external',
 | 
				
			||||||
 | 
					                'transitionAt' => new \DateTimeImmutable()],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield 'on sent_external' => [
 | 
				
			||||||
 | 
					            $entity,
 | 
				
			||||||
 | 
					            VoterInterface::ACCESS_DENIED,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entity = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $entity->setWorkflowName('dummy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield 'on initial' => [
 | 
				
			||||||
 | 
					            $entity,
 | 
				
			||||||
 | 
					            VoterInterface::ACCESS_GRANTED,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static function buildRegistry(): Registry
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $builder = new DefinitionBuilder();
 | 
				
			||||||
 | 
					        $builder
 | 
				
			||||||
 | 
					            ->setInitialPlaces(['initial'])
 | 
				
			||||||
 | 
					            ->addPlaces(['initial', 'sent_external', 'final_positive', 'final_negative'])
 | 
				
			||||||
 | 
					            ->addTransitions([
 | 
				
			||||||
 | 
					                new Transition('to_final_positive', ['initial'], 'final_positive'),
 | 
				
			||||||
 | 
					                new Transition('to_sent_external', ['initial'], 'sent_external'),
 | 
				
			||||||
 | 
					                new Transition('to_final_negative', ['initial'], 'final_negative'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ])
 | 
				
			||||||
 | 
					            ->setMetadataStore(
 | 
				
			||||||
 | 
					                new InMemoryMetadataStore(
 | 
				
			||||||
 | 
					                    placesMetadata: [
 | 
				
			||||||
 | 
					                        'sent_external' => [
 | 
				
			||||||
 | 
					                            'isSentExternal' => true,
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        'final_positive' => [
 | 
				
			||||||
 | 
					                            'isFinal' => true,
 | 
				
			||||||
 | 
					                            'isFinalPositive' => true,
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        'final_negative' => [
 | 
				
			||||||
 | 
					                            'isFinal' => true,
 | 
				
			||||||
 | 
					                            'isFinalPositive' => false,
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
 | 
				
			||||||
 | 
					        $registry = new Registry();
 | 
				
			||||||
 | 
					        $registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
 | 
				
			||||||
 | 
					            public function supports(WorkflowInterface $workflow, object $subject): bool
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $registry;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -11,6 +11,9 @@ declare(strict_types=1);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Chill\MainBundle\Tests\Workflow\Helper;
 | 
					namespace Chill\MainBundle\Tests\Workflow\Helper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\DocStoreBundle\Entity\StoredObject;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
 | 
				
			||||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
 | 
					use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
 | 
				
			||||||
use Chill\MainBundle\Entity\User;
 | 
					use Chill\MainBundle\Entity\User;
 | 
				
			||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
@@ -148,8 +151,11 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
				
			|||||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
					        yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
            'abstain because the user is not present as a dest user'];
 | 
					            'abstain because there is no workflow'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'force deny because the user is not present as a dest user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $entityWorkflow = new EntityWorkflow();
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
@@ -171,6 +177,9 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
				
			|||||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
				
			||||||
            'force grant because the user was a previous user'];
 | 
					            'force grant because the user was a previous user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'force denied because the user was not a previous user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $entityWorkflow = new EntityWorkflow();
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
        $dto->futureDestUsers[] = $user = new User();
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
@@ -232,6 +241,13 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
            'abstain: there is a signature on a canceled workflow'];
 | 
					            'abstain: there is a signature on a canceled workflow'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('sent_external', $dto, 'to_sent_external', new \DateTimeImmutable(), $user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(), 'force denied: the workflow is sent to an external user'];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testNoWorkflow(): void
 | 
					    public function testNoWorkflow(): void
 | 
				
			||||||
@@ -253,7 +269,217 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
				
			|||||||
        $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
 | 
					        $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
 | 
				
			||||||
        $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
 | 
					        $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
 | 
					        $repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
 | 
				
			||||||
 | 
					        $repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @dataProvider provideDataAllowedByWorkflowReadOperationByAttachment
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param list<EntityWorkflow> $entityWorkflows
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function testAllowedByWorkflowReadByAttachment(
 | 
				
			||||||
 | 
					        array $entityWorkflows,
 | 
				
			||||||
 | 
					        User $user,
 | 
				
			||||||
 | 
					        string $expected,
 | 
				
			||||||
 | 
					        ?\DateTimeImmutable $atDate,
 | 
				
			||||||
 | 
					        string $message,
 | 
				
			||||||
 | 
					    ): void {
 | 
				
			||||||
 | 
					        // all entities must have this workflow name, so we are ok to set it here
 | 
				
			||||||
 | 
					        foreach ($entityWorkflows as $entityWorkflow) {
 | 
				
			||||||
 | 
					            $entityWorkflow->setWorkflowName('dummy');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $helper = $this->buildHelperForAttachment($entityWorkflows, $user, $atDate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new StoredObject()), $message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function provideDataAllowedByWorkflowReadOperationByAttachment(): iterable
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'abstain because the user is not present as a dest user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'force grant because the user is a current user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'force grant because the user was a previous user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futurePersonSignatures[] = new Person();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'Abstain: there is a signature for person, but the attachment is not concerned'];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @dataProvider provideDataAllowedByWorkflowWriteOperationByAttachment
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param list<EntityWorkflow> $entityWorkflows
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function testAllowedByWorkflowWriteByAttachment(
 | 
				
			||||||
 | 
					        array $entityWorkflows,
 | 
				
			||||||
 | 
					        User $user,
 | 
				
			||||||
 | 
					        string $expected,
 | 
				
			||||||
 | 
					        ?\DateTimeImmutable $atDate,
 | 
				
			||||||
 | 
					        string $message,
 | 
				
			||||||
 | 
					    ): void {
 | 
				
			||||||
 | 
					        // all entities must have this workflow name, so we are ok to set it here
 | 
				
			||||||
 | 
					        foreach ($entityWorkflows as $entityWorkflow) {
 | 
				
			||||||
 | 
					            $entityWorkflow->setWorkflowName('dummy');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $helper = $this->buildHelperForAttachment($entityWorkflows, $user, $atDate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new StoredObject()), $message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function provideDataAllowedByWorkflowWriteOperationByAttachment(): iterable
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'abstain because there is no workflow'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'abstain because the user is not present as a dest user (and attachment)'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'force grant because the user is a current user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'force grant because the user was a previous user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'abstain because the user was not a previous user'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					        $entityWorkflow->getCurrentStep()->setIsFinal(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'force denied: user was a previous user, but it is finalized positive'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable());
 | 
				
			||||||
 | 
					        $entityWorkflow->getCurrentStep()->setIsFinal(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'abstain: user was a previous user, it is finalized, but finalized negative'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futurePersonSignatures[] = new Person();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					        $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
 | 
				
			||||||
 | 
					        $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'abstain: there is a signature, but not on the attachment'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futurePersonSignatures[] = new Person();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'abstain: there is a signature, but the signature is not on the attachment'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futurePersonSignatures[] = new Person();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					        $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
 | 
				
			||||||
 | 
					        $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					        $entityWorkflow->getCurrentStep()->setIsFinal(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'abstain: there is a signature on a canceled workflow'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = new EntityWorkflow();
 | 
				
			||||||
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $dto->futureDestUsers[] = $user = new User();
 | 
				
			||||||
 | 
					        $entityWorkflow->setStep('sent_external', $dto, 'to_sent_external', new \DateTimeImmutable(), $user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
 | 
				
			||||||
 | 
					            'force denied: the workflow is sent to an external user'];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param list<EntityWorkflow> $entityWorkflows
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private function buildHelperForAttachment(array $entityWorkflows, User $user, ?\DateTimeImmutable $atDateTime): WorkflowRelatedEntityPermissionHelper
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $security = $this->prophesize(Security::class);
 | 
				
			||||||
 | 
					        $security->getUser()->willReturn($user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
 | 
				
			||||||
 | 
					        $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->shouldNotBeCalled();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
 | 
				
			||||||
 | 
					        $attachments = [];
 | 
				
			||||||
 | 
					        foreach ($entityWorkflows as $entityWorkflow) {
 | 
				
			||||||
 | 
					            $attachments[] = new EntityWorkflowAttachment('dummy', ['id' => 1], $entityWorkflow, new StoredObject());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static function buildRegistry(): Registry
 | 
					    private static function buildRegistry(): Registry
 | 
				
			||||||
@@ -261,10 +487,13 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
				
			|||||||
        $builder = new DefinitionBuilder();
 | 
					        $builder = new DefinitionBuilder();
 | 
				
			||||||
        $builder
 | 
					        $builder
 | 
				
			||||||
            ->setInitialPlaces(['initial'])
 | 
					            ->setInitialPlaces(['initial'])
 | 
				
			||||||
            ->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
 | 
					            ->addPlaces(['initial', 'test', 'sent_external', 'final_positive', 'final_negative'])
 | 
				
			||||||
            ->setMetadataStore(
 | 
					            ->setMetadataStore(
 | 
				
			||||||
                new InMemoryMetadataStore(
 | 
					                new InMemoryMetadataStore(
 | 
				
			||||||
                    placesMetadata: [
 | 
					                    placesMetadata: [
 | 
				
			||||||
 | 
					                        'sent_external' => [
 | 
				
			||||||
 | 
					                            'isSentExternal' => true,
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
                        'final_positive' => [
 | 
					                        'final_positive' => [
 | 
				
			||||||
                            'isFinal' => true,
 | 
					                            'isFinal' => true,
 | 
				
			||||||
                            'isFinalPositive' => true,
 | 
					                            'isFinalPositive' => true,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,8 +11,11 @@ declare(strict_types=1);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Chill\MainBundle\Tests\Workflow\Messenger;
 | 
					namespace Chill\MainBundle\Tests\Workflow\Messenger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\DocStoreBundle\Entity\StoredObject;
 | 
				
			||||||
 | 
					use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
 | 
				
			||||||
use Chill\MainBundle\Entity\User;
 | 
					use Chill\MainBundle\Entity\User;
 | 
				
			||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
 | 
				
			||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
					use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
				
			||||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
 | 
					use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
 | 
				
			||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
					use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
				
			||||||
@@ -23,6 +26,7 @@ use Chill\ThirdPartyBundle\Entity\ThirdParty;
 | 
				
			|||||||
use PHPUnit\Framework\TestCase;
 | 
					use PHPUnit\Framework\TestCase;
 | 
				
			||||||
use Prophecy\Argument;
 | 
					use Prophecy\Argument;
 | 
				
			||||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
					use Prophecy\PhpUnit\ProphecyTrait;
 | 
				
			||||||
 | 
					use Psr\Log\LoggerInterface;
 | 
				
			||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 | 
					use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 | 
				
			||||||
use Symfony\Component\Mailer\MailerInterface;
 | 
					use Symfony\Component\Mailer\MailerInterface;
 | 
				
			||||||
use Symfony\Component\Mime\Address;
 | 
					use Symfony\Component\Mime\Address;
 | 
				
			||||||
@@ -39,24 +43,54 @@ class PostSendExternalMessageHandlerTest extends TestCase
 | 
				
			|||||||
    public function testSendMessageHappyScenario(): void
 | 
					    public function testSendMessageHappyScenario(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $entityWorkflow = $this->buildEntityWorkflow();
 | 
					        $entityWorkflow = $this->buildEntityWorkflow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Prepare attachments (2 attachments)
 | 
				
			||||||
 | 
					        $attachmentStoredObject1 = new StoredObject();
 | 
				
			||||||
 | 
					        $attachmentStoredObject2 = new StoredObject();
 | 
				
			||||||
 | 
					        new EntityWorkflowAttachment('generic_doc', ['id' => 1], $entityWorkflow, $attachmentStoredObject1);
 | 
				
			||||||
 | 
					        new EntityWorkflowAttachment('generic_doc', ['id' => 2], $entityWorkflow, $attachmentStoredObject2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Prepare transition DTO and sends
 | 
				
			||||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
					        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
        $dto->futureDestineeEmails = ['external@example.com'];
 | 
					        $dto->futureDestineeEmails = ['external@example.com'];
 | 
				
			||||||
        $dto->futureDestineeThirdParties = [(new ThirdParty())->setEmail('3party@example.com')];
 | 
					        $dto->futureDestineeThirdParties = [(new ThirdParty())->setEmail('3party@example.com')];
 | 
				
			||||||
        $entityWorkflow->setStep('send_external', $dto, 'to_send_external', new \DateTimeImmutable(), new User());
 | 
					        $entityWorkflow->setStep('send_external', $dto, 'to_send_external', new \DateTimeImmutable(), new User());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Repository returns our workflow
 | 
				
			||||||
        $repository = $this->prophesize(EntityWorkflowRepository::class);
 | 
					        $repository = $this->prophesize(EntityWorkflowRepository::class);
 | 
				
			||||||
        $repository->find(1)->willReturn($entityWorkflow);
 | 
					        $repository->find(1)->willReturn($entityWorkflow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Mailer must send to both recipients
 | 
				
			||||||
        $mailer = $this->prophesize(MailerInterface::class);
 | 
					        $mailer = $this->prophesize(MailerInterface::class);
 | 
				
			||||||
        $mailer->send(Argument::that($this->buildCheckAddressCallback('3party@example.com')))->shouldBeCalledOnce();
 | 
					        $mailer->send(Argument::that($this->buildCheckAddressCallback('3party@example.com')))->shouldBeCalledOnce();
 | 
				
			||||||
        $mailer->send(Argument::that($this->buildCheckAddressCallback('external@example.com')))->shouldBeCalledOnce();
 | 
					        $mailer->send(Argument::that($this->buildCheckAddressCallback('external@example.com')))->shouldBeCalledOnce();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Workflow manager and handler
 | 
				
			||||||
        $workflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
 | 
					        $workflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
 | 
				
			||||||
        $workflowHandler->getEntityTitle($entityWorkflow, Argument::any())->willReturn('title');
 | 
					        $workflowHandler->getEntityTitle($entityWorkflow, Argument::any())->willReturn('title');
 | 
				
			||||||
        $workflowManager = $this->prophesize(EntityWorkflowManager::class);
 | 
					        $workflowManager = $this->prophesize(EntityWorkflowManager::class);
 | 
				
			||||||
        $workflowManager->getHandler($entityWorkflow)->willReturn($workflowHandler->reveal());
 | 
					        $workflowManager->getHandler($entityWorkflow)->willReturn($workflowHandler->reveal());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $handler = new PostSendExternalMessageHandler($repository->reveal(), $mailer->reveal(), $workflowManager->reveal());
 | 
					        // Associated stored object for the workflow
 | 
				
			||||||
 | 
					        $associatedStoredObject = new StoredObject();
 | 
				
			||||||
 | 
					        $workflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($associatedStoredObject);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Converter should be called for each attachment and the associated stored object
 | 
				
			||||||
 | 
					        $converter = $this->prophesize(StoredObjectToPdfConverter::class);
 | 
				
			||||||
 | 
					        $converter->addConvertedVersion($attachmentStoredObject1, 'fr')->shouldBeCalledOnce();
 | 
				
			||||||
 | 
					        $converter->addConvertedVersion($attachmentStoredObject2, 'fr')->shouldBeCalledOnce();
 | 
				
			||||||
 | 
					        $converter->addConvertedVersion($associatedStoredObject, 'fr')->shouldBeCalledOnce();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Logger (not used in happy path, but required by handler)
 | 
				
			||||||
 | 
					        $logger = $this->prophesize(LoggerInterface::class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $handler = new PostSendExternalMessageHandler(
 | 
				
			||||||
 | 
					            $repository->reveal(),
 | 
				
			||||||
 | 
					            $mailer->reveal(),
 | 
				
			||||||
 | 
					            $workflowManager->reveal(),
 | 
				
			||||||
 | 
					            $converter->reveal(),
 | 
				
			||||||
 | 
					            $logger->reveal(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $handler(new PostSendExternalMessage(1, 'fr'));
 | 
					        $handler(new PostSendExternalMessage(1, 'fr'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,9 +11,12 @@ declare(strict_types=1);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Chill\MainBundle\Workflow\Helper;
 | 
					namespace Chill\MainBundle\Workflow\Helper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\DocStoreBundle\Entity\StoredObject;
 | 
				
			||||||
use Chill\MainBundle\Entity\User;
 | 
					use Chill\MainBundle\Entity\User;
 | 
				
			||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
 | 
				
			||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
 | 
				
			||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
					use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
				
			||||||
use Symfony\Component\Clock\ClockInterface;
 | 
					use Symfony\Component\Clock\ClockInterface;
 | 
				
			||||||
use Symfony\Component\Security\Core\Security;
 | 
					use Symfony\Component\Security\Core\Security;
 | 
				
			||||||
@@ -58,21 +61,39 @@ class WorkflowRelatedEntityPermissionHelper
 | 
				
			|||||||
    public function __construct(
 | 
					    public function __construct(
 | 
				
			||||||
        private readonly Security $security,
 | 
					        private readonly Security $security,
 | 
				
			||||||
        private readonly EntityWorkflowManager $entityWorkflowManager,
 | 
					        private readonly EntityWorkflowManager $entityWorkflowManager,
 | 
				
			||||||
 | 
					        private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
 | 
				
			||||||
        private readonly Registry $registry,
 | 
					        private readonly Registry $registry,
 | 
				
			||||||
        private readonly ClockInterface $clock,
 | 
					        private readonly ClockInterface $clock,
 | 
				
			||||||
    ) {}
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param object $entity The entity may be an
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
     * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
 | 
					     * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function isAllowedByWorkflowForReadOperation(object $entity): string
 | 
					    public function isAllowedByWorkflowForReadOperation(object $entity): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
					        if ($entity instanceof StoredObject) {
 | 
				
			||||||
 | 
					            $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
 | 
				
			||||||
 | 
					            $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
 | 
				
			||||||
 | 
					            $isAttached = true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
				
			||||||
 | 
					            $isAttached = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ([] === $entityWorkflows) {
 | 
				
			||||||
 | 
					            return self::ABSTAIN;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
 | 
					        if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
 | 
				
			||||||
            return self::FORCE_GRANT;
 | 
					            return self::FORCE_GRANT;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($isAttached) {
 | 
				
			||||||
 | 
					            return self::ABSTAIN;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // give a view permission if there is a Person signature pending, or in the 12 hours following
 | 
					        // give a view permission if there is a Person signature pending, or in the 12 hours following
 | 
				
			||||||
        // the signature last state
 | 
					        // the signature last state
 | 
				
			||||||
        foreach ($entityWorkflows as $workflow) {
 | 
					        foreach ($entityWorkflows as $workflow) {
 | 
				
			||||||
@@ -100,33 +121,51 @@ class WorkflowRelatedEntityPermissionHelper
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    public function isAllowedByWorkflowForWriteOperation(object $entity): string
 | 
					    public function isAllowedByWorkflowForWriteOperation(object $entity): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
					        if ($entity instanceof StoredObject) {
 | 
				
			||||||
        $runningWorkflows = [];
 | 
					            $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
 | 
				
			||||||
 | 
					            $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
 | 
				
			||||||
 | 
					            $isAttached = true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
				
			||||||
 | 
					            $isAttached = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // if a workflow is finalized positive, we are not allowed to edit to document any more
 | 
					        if ([] === $entityWorkflows) {
 | 
				
			||||||
 | 
					            return self::ABSTAIN;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // if a workflow is finalized positive, anyone is allowed to edit the document anymore
 | 
				
			||||||
        foreach ($entityWorkflows as $entityWorkflow) {
 | 
					        foreach ($entityWorkflows as $entityWorkflow) {
 | 
				
			||||||
            if ($entityWorkflow->isFinal()) {
 | 
					            $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
				
			||||||
                $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
					            $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
 | 
				
			||||||
                $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
 | 
					            foreach ($marking->getPlaces() as $place => $int) {
 | 
				
			||||||
                foreach ($marking->getPlaces() as $place => $int) {
 | 
					                $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
 | 
				
			||||||
                    $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
 | 
					                if (
 | 
				
			||||||
                    if (true === ($placeMetadata['isFinalPositive'] ?? false)) {
 | 
					                    ($entityWorkflow->isFinal() && ($placeMetadata['isFinalPositive'] ?? false))
 | 
				
			||||||
                        // the workflow is final, and final positive, so we stop here.
 | 
					                    || ($placeMetadata['isSentExternal'] ?? false)
 | 
				
			||||||
                        return self::FORCE_DENIED;
 | 
					                ) {
 | 
				
			||||||
                    }
 | 
					                    // the workflow is final, and final positive, or is sentExternal, so we stop here.
 | 
				
			||||||
 | 
					                    return self::FORCE_DENIED;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    // if not finalized positive
 | 
				
			||||||
 | 
					                    $entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false)
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                    return self::ABSTAIN;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                $runningWorkflows[] = $entityWorkflow;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // if there is a signature on a **running workflow**, no one can edit the workflow any more
 | 
					        $runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal());
 | 
				
			||||||
        foreach ($runningWorkflows as $entityWorkflow) {
 | 
					
 | 
				
			||||||
            foreach ($entityWorkflow->getSteps() as $step) {
 | 
					        // if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore
 | 
				
			||||||
                foreach ($step->getSignatures() as $signature) {
 | 
					        if (!$isAttached) {
 | 
				
			||||||
                    if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
 | 
					            foreach ($runningWorkflows as $entityWorkflow) {
 | 
				
			||||||
                        return self::FORCE_DENIED;
 | 
					                foreach ($entityWorkflow->getSteps() as $step) {
 | 
				
			||||||
 | 
					                    foreach ($step->getSignatures() as $signature) {
 | 
				
			||||||
 | 
					                        if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
 | 
				
			||||||
 | 
					                            return self::FORCE_DENIED;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -137,7 +176,11 @@ class WorkflowRelatedEntityPermissionHelper
 | 
				
			|||||||
            return self::FORCE_GRANT;
 | 
					            return self::FORCE_GRANT;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self::ABSTAIN;
 | 
					        if ($isAttached) {
 | 
				
			||||||
 | 
					            return self::ABSTAIN;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self::FORCE_DENIED;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,9 +11,14 @@ declare(strict_types=1);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Chill\MainBundle\Workflow\Messenger;
 | 
					namespace Chill\MainBundle\Workflow\Messenger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException;
 | 
				
			||||||
 | 
					use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
 | 
				
			||||||
 | 
					use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
				
			||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
 | 
					use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
 | 
				
			||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
					use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
				
			||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
					use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
				
			||||||
 | 
					use Psr\Log\LoggerInterface;
 | 
				
			||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 | 
					use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 | 
				
			||||||
use Symfony\Component\Mailer\MailerInterface;
 | 
					use Symfony\Component\Mailer\MailerInterface;
 | 
				
			||||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
 | 
					use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
 | 
				
			||||||
@@ -25,6 +30,8 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
 | 
				
			|||||||
        private EntityWorkflowRepository $entityWorkflowRepository,
 | 
					        private EntityWorkflowRepository $entityWorkflowRepository,
 | 
				
			||||||
        private MailerInterface $mailer,
 | 
					        private MailerInterface $mailer,
 | 
				
			||||||
        private EntityWorkflowManager $workflowManager,
 | 
					        private EntityWorkflowManager $workflowManager,
 | 
				
			||||||
 | 
					        private StoredObjectToPdfConverter $storedObjectToPdfConverter,
 | 
				
			||||||
 | 
					        private LoggerInterface $logger,
 | 
				
			||||||
    ) {}
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function __invoke(PostSendExternalMessage $message): void
 | 
					    public function __invoke(PostSendExternalMessage $message): void
 | 
				
			||||||
@@ -35,11 +42,38 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
 | 
				
			|||||||
            throw new UnrecoverableMessageHandlingException(sprintf('Entity workflow with id %d not found', $message->entityWorkflowId));
 | 
					            throw new UnrecoverableMessageHandlingException(sprintf('Entity workflow with id %d not found', $message->entityWorkflowId));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->convertToPdf($entityWorkflow, $message->lang);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach ($entityWorkflow->getCurrentStep()->getSends() as $send) {
 | 
					        foreach ($entityWorkflow->getCurrentStep()->getSends() as $send) {
 | 
				
			||||||
            $this->sendEmailToDestinee($send, $message);
 | 
					            $this->sendEmailToDestinee($send, $message);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function convertToPdf(EntityWorkflow $entityWorkflow, string $locale): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        foreach ($entityWorkflow->getAttachments() as $attachment) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                $this->storedObjectToPdfConverter->addConvertedVersion($attachment->getProxyStoredObject(), $locale);
 | 
				
			||||||
 | 
					            } catch (StoredObjectManagerException $e) {
 | 
				
			||||||
 | 
					                $this->logger->error('Error converting attachment to PDF', ['backtrace' => $e->getTraceAsString(), 'attachment_id' => $attachment->getId()]);
 | 
				
			||||||
 | 
					            } catch (ConversionWithSameMimeTypeException $e) {
 | 
				
			||||||
 | 
					                $this->logger->error('Error converting attachment to PDF: already at the same MIME type', ['backtrace' => $e->getTraceAsString(), 'attachment_id' => $attachment->getId()]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $storedObject = $this->workflowManager->getAssociatedStoredObject($entityWorkflow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (null !== $storedObject) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                $this->storedObjectToPdfConverter->addConvertedVersion($storedObject, $locale);
 | 
				
			||||||
 | 
					            } catch (StoredObjectManagerException $e) {
 | 
				
			||||||
 | 
					                $this->logger->error('Error converting stored object to PDF', ['backtrace' => $e->getTraceAsString(), 'stored_object_id' => $storedObject->getId(), 'workflow_id' => $entityWorkflow->getId()]);
 | 
				
			||||||
 | 
					            } catch (ConversionWithSameMimeTypeException $e) {
 | 
				
			||||||
 | 
					                $this->logger->error('Error converting stored object to PDF: already at the same MIME type', ['backtrace' => $e->getTraceAsString(), 'stored_object_id' => $storedObject->getId(), 'workflow_id' => $entityWorkflow->getId()]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function sendEmailToDestinee(EntityWorkflowSend $send, PostSendExternalMessage $message): void
 | 
					    private function sendEmailToDestinee(EntityWorkflowSend $send, PostSendExternalMessage $message): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
 | 
					        $entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ use Psr\Log\LoggerInterface;
 | 
				
			|||||||
use Symfony\Component\Clock\ClockInterface;
 | 
					use Symfony\Component\Clock\ClockInterface;
 | 
				
			||||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
					use Symfony\Component\Messenger\MessageBusInterface;
 | 
				
			||||||
use Symfony\Component\Workflow\Registry;
 | 
					use Symfony\Component\Workflow\Registry;
 | 
				
			||||||
 | 
					use Symfony\Component\Workflow\Transition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Handles state changes for signature steps within a workflow.
 | 
					 * Handles state changes for signature steps within a workflow.
 | 
				
			||||||
@@ -50,8 +51,10 @@ class SignatureStepStateChanger
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
 | 
					     * @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
 | 
				
			||||||
     * @param int|null                    $atIndex   optional index position for the signature within the zone
 | 
					     * @param int|null                    $atIndex   optional index position for the signature within the zone
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return string The expected new workflow's step, after transition is applyied
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): void
 | 
					    public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
					        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -60,7 +63,14 @@ class SignatureStepStateChanger
 | 
				
			|||||||
            ->setZoneSignatureIndex($atIndex)
 | 
					            ->setZoneSignatureIndex($atIndex)
 | 
				
			||||||
            ->setStateDate($this->clock->now());
 | 
					            ->setStateDate($this->clock->now());
 | 
				
			||||||
        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as signed', ['signatureId' => $signature->getId(), 'index' => (string) $atIndex]);
 | 
					        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as signed', ['signatureId' => $signature->getId(), 'index' => (string) $atIndex]);
 | 
				
			||||||
 | 
					        ['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
					        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
				
			||||||
 | 
					        if (null === $transition) {
 | 
				
			||||||
 | 
					            return $signature->getStep()->getEntityWorkflow()->getStep();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $transition->getTos()[0];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -71,8 +81,10 @@ class SignatureStepStateChanger
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * This method updates the signature state to 'canceled' and logs the action.
 | 
					     * This method updates the signature state to 'canceled' and logs the action.
 | 
				
			||||||
     * It also dispatches a message to notify about the state change.
 | 
					     * It also dispatches a message to notify about the state change.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return string The expected new workflow's step, after transition is applyied
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): void
 | 
					    public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
					        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -80,7 +92,15 @@ class SignatureStepStateChanger
 | 
				
			|||||||
            ->setState(EntityWorkflowSignatureStateEnum::CANCELED)
 | 
					            ->setState(EntityWorkflowSignatureStateEnum::CANCELED)
 | 
				
			||||||
            ->setStateDate($this->clock->now());
 | 
					            ->setStateDate($this->clock->now());
 | 
				
			||||||
        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as canceled', ['signatureId' => $signature->getId()]);
 | 
					        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as canceled', ['signatureId' => $signature->getId()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
					        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
				
			||||||
 | 
					        if (null === $transition) {
 | 
				
			||||||
 | 
					            return $signature->getStep()->getEntityWorkflow()->getStep();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $transition->getTos()[0];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -93,8 +113,10 @@ class SignatureStepStateChanger
 | 
				
			|||||||
     * a state change has occurred.
 | 
					     * a state change has occurred.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param EntityWorkflowStepSignature $signature the signature entity to be marked as rejected
 | 
					     * @param EntityWorkflowStepSignature $signature the signature entity to be marked as rejected
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return string The expected new workflow's step, after transition is applyied
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): void
 | 
					    public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
					        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -102,7 +124,16 @@ class SignatureStepStateChanger
 | 
				
			|||||||
            ->setState(EntityWorkflowSignatureStateEnum::REJECTED)
 | 
					            ->setState(EntityWorkflowSignatureStateEnum::REJECTED)
 | 
				
			||||||
            ->setStateDate($this->clock->now());
 | 
					            ->setStateDate($this->clock->now());
 | 
				
			||||||
        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as rejected', ['signatureId' => $signature->getId()]);
 | 
					        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as rejected', ['signatureId' => $signature->getId()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
					        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (null === $transition) {
 | 
				
			||||||
 | 
					            return $signature->getStep()->getEntityWorkflow()->getStep();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $transition->getTos()[0];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -117,10 +148,35 @@ class SignatureStepStateChanger
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_READ);
 | 
					        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_READ);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (null === $transition) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $entityWorkflow = $signature->getStep()->getEntityWorkflow();
 | 
				
			||||||
 | 
					        $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
				
			||||||
 | 
					        $transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
				
			||||||
 | 
					        $transitionDto->futureDestUsers[] = $futureUser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $workflow->apply($entityWorkflow, $transition->getName(), [
 | 
				
			||||||
 | 
					            'context' => $transitionDto,
 | 
				
			||||||
 | 
					            'transitionAt' => $this->clock->now(),
 | 
				
			||||||
 | 
					            'transition' => $transition->getName(),
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @return array{transition: Transition|null, futureUser: User|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private function decideTransition(EntityWorkflowStepSignature $signature): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
        if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
 | 
					        if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
 | 
				
			||||||
            $this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
 | 
					            $this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return;
 | 
					            return ['transition' => null, 'futureUser' => null];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
 | 
					        $this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
 | 
				
			||||||
@@ -144,7 +200,7 @@ class SignatureStepStateChanger
 | 
				
			|||||||
        if (null === $transition) {
 | 
					        if (null === $transition) {
 | 
				
			||||||
            $this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
 | 
					            $this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return;
 | 
					            return ['transition' => null, 'futureUser' => null];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ('person' === $signature->getSignerKind()) {
 | 
					        if ('person' === $signature->getSignerKind()) {
 | 
				
			||||||
@@ -156,19 +212,16 @@ class SignatureStepStateChanger
 | 
				
			|||||||
        if (null === $futureUser) {
 | 
					        if (null === $futureUser) {
 | 
				
			||||||
            $this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
 | 
					            $this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return;
 | 
					            return ['transition' => null, 'futureUser' => null];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
					        foreach ($workflow->getDefinition()->getTransitions() as $transitionObj) {
 | 
				
			||||||
        $transitionDto->futureDestUsers[] = $futureUser;
 | 
					            if ($transitionObj->getName() === $transition) {
 | 
				
			||||||
 | 
					                return ['transition' => $transitionObj, 'futureUser' => $futureUser];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $workflow->apply($entityWorkflow, $transition, [
 | 
					        throw new \RuntimeException('Transition not found');
 | 
				
			||||||
            'context' => $transitionDto,
 | 
					 | 
				
			||||||
            'transitionAt' => $this->clock->now(),
 | 
					 | 
				
			||||||
            'transition' => $transition,
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
 | 
					    private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -965,6 +965,31 @@ paths:
 | 
				
			|||||||
                        application/json:
 | 
					                        application/json:
 | 
				
			||||||
                            schema:
 | 
					                            schema:
 | 
				
			||||||
                                $ref: "#/components/schemas/UserJob"
 | 
					                                $ref: "#/components/schemas/UserJob"
 | 
				
			||||||
 | 
					    /1.0/main/workflow/{id}.json:
 | 
				
			||||||
 | 
					        get:
 | 
				
			||||||
 | 
					            tags:
 | 
				
			||||||
 | 
					                - workflow
 | 
				
			||||||
 | 
					            summary: Return a workflow
 | 
				
			||||||
 | 
					            parameters:
 | 
				
			||||||
 | 
					                -   name: id
 | 
				
			||||||
 | 
					                    in: path
 | 
				
			||||||
 | 
					                    required: true
 | 
				
			||||||
 | 
					                    description: The workflow id
 | 
				
			||||||
 | 
					                    schema:
 | 
				
			||||||
 | 
					                        type: integer
 | 
				
			||||||
 | 
					                        format: integer
 | 
				
			||||||
 | 
					                        minimum: 1
 | 
				
			||||||
 | 
					            responses:
 | 
				
			||||||
 | 
					                200:
 | 
				
			||||||
 | 
					                    description: "ok"
 | 
				
			||||||
 | 
					                    content:
 | 
				
			||||||
 | 
					                        application/json:
 | 
				
			||||||
 | 
					                            schema:
 | 
				
			||||||
 | 
					                                $ref: "#/components/schemas/Workflow"
 | 
				
			||||||
 | 
					                404:
 | 
				
			||||||
 | 
					                    description: "not found"
 | 
				
			||||||
 | 
					                401:
 | 
				
			||||||
 | 
					                    description: "Unauthorized"
 | 
				
			||||||
    /1.0/main/workflow/my:
 | 
					    /1.0/main/workflow/my:
 | 
				
			||||||
        get:
 | 
					        get:
 | 
				
			||||||
            tags:
 | 
					            tags:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -120,5 +120,8 @@ module.exports = function (encore, entries) {
 | 
				
			|||||||
    "vue_onthefly",
 | 
					    "vue_onthefly",
 | 
				
			||||||
    __dirname + "/Resources/public/vuejs/OnTheFly/index.js",
 | 
					    __dirname + "/Resources/public/vuejs/OnTheFly/index.js",
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					  encore.addEntry(
 | 
				
			||||||
 | 
					    "page_workflow_waiting_post_process",
 | 
				
			||||||
 | 
					    __dirname + "/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts"
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -666,9 +666,18 @@ workflow:
 | 
				
			|||||||
        cancel_are_you_sure: Êtes-vous sûr de vouloir annuler la signature de %signer%
 | 
					        cancel_are_you_sure: Êtes-vous sûr de vouloir annuler la signature de %signer%
 | 
				
			||||||
        reject_signature_of: Rejet de la signature de %signer%
 | 
					        reject_signature_of: Rejet de la signature de %signer%
 | 
				
			||||||
        reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer%
 | 
					        reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer%
 | 
				
			||||||
 | 
					        waiting_for: En attente de modification de l'état de la signature
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    attachments:
 | 
					    attachments:
 | 
				
			||||||
        title: Pièces jointes
 | 
					        title: Pièces jointes
 | 
				
			||||||
 | 
					        no_attachment: Aucune pièce jointe
 | 
				
			||||||
 | 
					        Add_an_attachment: Ajouter une pièce jointe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    wait:
 | 
				
			||||||
 | 
					        title: En attente de traitement
 | 
				
			||||||
 | 
					        error_while_waiting: Le traitement a échoué
 | 
				
			||||||
 | 
					        success: Traitement terminé. Redirection en cours...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Subscribe final: Recevoir une notification à l'étape finale
 | 
					Subscribe final: Recevoir une notification à l'étape finale
 | 
				
			||||||
Subscribe all steps: Recevoir une notification à chaque étape
 | 
					Subscribe all steps: Recevoir une notification à chaque étape
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,7 +49,7 @@ final class ImportSocialWorkMetadata extends Command
 | 
				
			|||||||
        $filepath = $input->getOption('filepath');
 | 
					        $filepath = $input->getOption('filepath');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $csv = Reader::createFromPath($filepath);
 | 
					            $csv = Reader::from($filepath);
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            throw new \Exception('Error while loading CSV.', 0, $e);
 | 
					            throw new \Exception('Error while loading CSV.', 0, $e);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,15 +11,19 @@ declare(strict_types=1);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Chill\PersonBundle\DataFixtures\ORM;
 | 
					namespace Chill\PersonBundle\DataFixtures\ORM;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Chill\MainBundle\DataFixtures\ORM\LoadAdministrativeLocation;
 | 
				
			||||||
use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes;
 | 
					use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes;
 | 
				
			||||||
 | 
					use Chill\MainBundle\DataFixtures\ORM\LoadUserJob;
 | 
				
			||||||
use Chill\MainBundle\Entity\Address;
 | 
					use Chill\MainBundle\Entity\Address;
 | 
				
			||||||
use Chill\MainBundle\Entity\Center;
 | 
					use Chill\MainBundle\Entity\Center;
 | 
				
			||||||
use Chill\MainBundle\Entity\Country;
 | 
					use Chill\MainBundle\Entity\Country;
 | 
				
			||||||
use Chill\MainBundle\Entity\Gender;
 | 
					use Chill\MainBundle\Entity\Gender;
 | 
				
			||||||
use Chill\MainBundle\Entity\GenderEnum;
 | 
					use Chill\MainBundle\Entity\GenderEnum;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\Location;
 | 
				
			||||||
use Chill\MainBundle\Entity\PostalCode;
 | 
					use Chill\MainBundle\Entity\PostalCode;
 | 
				
			||||||
use Chill\MainBundle\Entity\Scope;
 | 
					use Chill\MainBundle\Entity\Scope;
 | 
				
			||||||
use Chill\MainBundle\Entity\User;
 | 
					use Chill\MainBundle\Entity\User;
 | 
				
			||||||
 | 
					use Chill\MainBundle\Entity\UserJob;
 | 
				
			||||||
use Chill\MainBundle\Repository\CenterRepository;
 | 
					use Chill\MainBundle\Repository\CenterRepository;
 | 
				
			||||||
use Chill\MainBundle\Repository\CountryRepository;
 | 
					use Chill\MainBundle\Repository\CountryRepository;
 | 
				
			||||||
use Chill\MainBundle\Repository\GenderRepository;
 | 
					use Chill\MainBundle\Repository\GenderRepository;
 | 
				
			||||||
@@ -362,6 +366,10 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
 | 
				
			|||||||
            $origin = $this->getReference(LoadAccompanyingPeriodOrigin::ACCOMPANYING_PERIOD_ORIGIN, AccompanyingPeriod\Origin::class);
 | 
					            $origin = $this->getReference(LoadAccompanyingPeriodOrigin::ACCOMPANYING_PERIOD_ORIGIN, AccompanyingPeriod\Origin::class);
 | 
				
			||||||
            $accompanyingPeriod->setOrigin($origin);
 | 
					            $accompanyingPeriod->setOrigin($origin);
 | 
				
			||||||
            $accompanyingPeriod->setIntensity('regular');
 | 
					            $accompanyingPeriod->setIntensity('regular');
 | 
				
			||||||
 | 
					            $userJob = $this->getReference(LoadUserJob::USER_JOB, UserJob::class);
 | 
				
			||||||
 | 
					            $accompanyingPeriod->setJob($userJob);
 | 
				
			||||||
 | 
					            $administrativeLocation = $this->getReference(LoadAdministrativeLocation::ADMINISTRATIVE_LOCATION, Location::class);
 | 
				
			||||||
 | 
					            $accompanyingPeriod->setAdministrativeLocation($administrativeLocation);
 | 
				
			||||||
            $accompanyingPeriod->setAddressLocation($this->createAddress());
 | 
					            $accompanyingPeriod->setAddressLocation($this->createAddress());
 | 
				
			||||||
            $manager->persist($accompanyingPeriod->getAddressLocation());
 | 
					            $manager->persist($accompanyingPeriod->getAddressLocation());
 | 
				
			||||||
            $workflow = $this->workflowRegistry->get($accompanyingPeriod);
 | 
					            $workflow = $this->workflowRegistry->get($accompanyingPeriod);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@ class LoadSocialWorkMetadata extends Fixture implements OrderedFixtureInterface
 | 
				
			|||||||
    public function load(ObjectManager $manager): void
 | 
					    public function load(ObjectManager $manager): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $csv = Reader::createFromPath(__DIR__.'/data/social_work_metadata.csv');
 | 
					            $csv = Reader::from(__DIR__.'/data/social_work_metadata.csv');
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            throw new \Exception('Error while loading CSV.', 0, $e);
 | 
					            throw new \Exception('Error while loading CSV.', 0, $e);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,6 +60,7 @@ import {
 | 
				
			|||||||
    EVALUATION_DOCUMENT_MOVE_SUCCESS,
 | 
					    EVALUATION_DOCUMENT_MOVE_SUCCESS,
 | 
				
			||||||
} from "translator";
 | 
					} from "translator";
 | 
				
			||||||
import { useToast } from "vue-toast-notification";
 | 
					import { useToast } from "vue-toast-notification";
 | 
				
			||||||
 | 
					import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps(["evaluation", "docAnchorId"]);
 | 
					const props = defineProps(["evaluation", "docAnchorId"]);
 | 
				
			||||||
const store = useStore();
 | 
					const store = useStore();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,7 +38,7 @@ final readonly class SocialActionCSVExportService
 | 
				
			|||||||
            array_keys($this->formatRow(new SocialAction()))
 | 
					            array_keys($this->formatRow(new SocialAction()))
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $csv = Writer::createFromPath('php://temp', 'w+');
 | 
					        $csv = Writer::from('php://temp', 'w+');
 | 
				
			||||||
        $csv->insertOne($headers);
 | 
					        $csv->insertOne($headers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach ($actions as $action) {
 | 
					        foreach ($actions as $action) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,7 +36,7 @@ readonly class SocialIssueCSVExportService
 | 
				
			|||||||
    public function generateCsv(array $issues): Writer
 | 
					    public function generateCsv(array $issues): Writer
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        // CSV headers
 | 
					        // CSV headers
 | 
				
			||||||
        $csv = Writer::createFromPath('php://temp', 'r+');
 | 
					        $csv = Writer::from('php://temp', 'r+');
 | 
				
			||||||
        $csv->insertOne(
 | 
					        $csv->insertOne(
 | 
				
			||||||
            array_map(
 | 
					            array_map(
 | 
				
			||||||
                fn (string $e) => $this->translator->trans($e),
 | 
					                fn (string $e) => $this->translator->trans($e),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,7 +52,7 @@ class ThirdpartyCSVExportController extends AbstractController
 | 
				
			|||||||
            fwrite($output, "\xEF\xBB\xBF");
 | 
					            fwrite($output, "\xEF\xBB\xBF");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Create CSV writer
 | 
					            // Create CSV writer
 | 
				
			||||||
            $csv = Writer::createFromStream($output);
 | 
					            $csv = Writer::from($output);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Write header row
 | 
					            // Write header row
 | 
				
			||||||
            $header = array_map(
 | 
					            $header = array_map(
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user