mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-04 11:18:25 +00:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			456-doc-ge
			...
			405-aside-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 80ea99cb9e | |||
| 48df0d49d3 | |||
| bf6375a0b5 | |||
| 21383ddbe7 | |||
| d451d87cdf | |||
| ac6336d197 | |||
| a46b301e44 | |||
| 05f0443011 | |||
| 7f8d8f891e | |||
| ddb932a4fa | |||
| 3a02f15bcd | 
@@ -1,7 +0,0 @@
 | 
			
		||||
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/Fixed-20251003-224044.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20251003-224044.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Fixed
 | 
			
		||||
body: Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted
 | 
			
		||||
time: 2025-10-03T22:40:44.685474863+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: ""
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20251006-121315.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20251006-121315.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
kind: Fixed
 | 
			
		||||
body: 'Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists'
 | 
			
		||||
time: 2025-10-06T12:13:15.45905994+02:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "434"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
kind: UX
 | 
			
		||||
body: Display whether doc generation template is active or not in admin and order templates alphabetically
 | 
			
		||||
time: 2025-11-03T16:19:10.051947925+01:00
 | 
			
		||||
custom:
 | 
			
		||||
    Issue: "456"
 | 
			
		||||
    SchemaChange: No schema change
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
## v4.6.0 - 2025-10-15
 | 
			
		||||
### Feature
 | 
			
		||||
* ([#423](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/423)) Create environment banner that can be activated and configured depending on the image deployed   
 | 
			
		||||
* ([#394](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/394)) Only show active workflow on the page "my tracked workflow"   
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix loading of classLists in SocialIssuesAcc.vue, ensure elements are present   
 | 
			
		||||
* Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted   
 | 
			
		||||
* ([#434](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/434)) Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists   
 | 
			
		||||
* Fix loading of social issues and social actions within vue component   
 | 
			
		||||
* ([#446](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/446)) Add unique condition on stored object filename, with cleaning step on existing duplicate filenames   
 | 
			
		||||
 | 
			
		||||
  **Schema Change**: Drop or rename table or columns, or enforce new constraint that must be manually fixed
 | 
			
		||||
* [workflow] take permissions into account to delete the workflow attachment   
 | 
			
		||||
* ([#448](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/448)) Fix the execution of daily cronjob notification, when the previous last execution storage was invalid   
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
## v4.6.1 - 2025-10-27
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix export case where no 'reason' is picked within the PersonHavingActivityBetweenDateFilter.php   
 | 
			
		||||
@@ -240,6 +240,9 @@ The tests are run from the project's root (not from the bundle's root).
 | 
			
		||||
# Run all tests
 | 
			
		||||
vendor/bin/phpunit
 | 
			
		||||
 | 
			
		||||
# Run tests for a specific bundle
 | 
			
		||||
vendor/bin/phpunit --testsuite NameBundle
 | 
			
		||||
 | 
			
		||||
# Run a specific test file
 | 
			
		||||
vendor/bin/phpunit path/to/TestFile.php
 | 
			
		||||
 | 
			
		||||
@@ -247,9 +250,6 @@ vendor/bin/phpunit path/to/TestFile.php
 | 
			
		||||
vendor/bin/phpunit --filter methodName path/to/TestFile.php
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
When writing tests, only test specific files. Do not run all tests or the full
 | 
			
		||||
test suite.
 | 
			
		||||
 | 
			
		||||
#### Test Structure
 | 
			
		||||
 | 
			
		||||
Tests are organized by bundle and follow the same structure as the bundle itself:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -6,25 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
 | 
			
		||||
and is generated by [Changie](https://github.com/miniscruff/changie).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## v4.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   
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
        "ext-openssl": "*",
 | 
			
		||||
        "ext-redis": "*",
 | 
			
		||||
        "ext-zlib": "*",
 | 
			
		||||
        "champs-libres/wopi-bundle": "dev-master#1be045ee95310d2037683859ecefdbf3a10f7be6 as 0.4.x-dev",
 | 
			
		||||
        "champs-libres/wopi-bundle": "dev-master@dev",
 | 
			
		||||
        "champs-libres/wopi-lib": "dev-master@dev",
 | 
			
		||||
        "doctrine/data-fixtures": "^1.8",
 | 
			
		||||
        "doctrine/doctrine-bundle": "^2.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,6 @@
 | 
			
		||||
chill_main:
 | 
			
		||||
    available_languages: [ '%env(resolve:LOCALE)%', 'en' ]
 | 
			
		||||
    available_countries: ['BE', 'FR']
 | 
			
		||||
    top_banner:
 | 
			
		||||
        visible: false
 | 
			
		||||
        text:
 | 
			
		||||
            fr: 'Vous travaillez actuellement avec la version de PRÉ-PRODUCTION.'
 | 
			
		||||
            nl: 'Je werkt momenteel in de PRE-PRODUCTIE versie'
 | 
			
		||||
        color: '#353535'
 | 
			
		||||
        background_color: '#d8bb48'
 | 
			
		||||
    notifications:
 | 
			
		||||
        from_email: '%env(resolve:NOTIFICATION_FROM_EMAIL)%'
 | 
			
		||||
        from_name: '%env(resolve:NOTIFICATION_FROM_NAME)%'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								config/packages/chill_aside_activity.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								config/packages/chill_aside_activity.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
chill_aside_activity:
 | 
			
		||||
    show_concerned_persons_count: hidden
 | 
			
		||||
@@ -90,9 +90,7 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt
 | 
			
		||||
 | 
			
		||||
    public function getFormDefaultData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'reasons' => [],
 | 
			
		||||
        ];
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array
 | 
			
		||||
 
 | 
			
		||||
@@ -42,8 +42,6 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
 | 
			
		||||
    {
 | 
			
		||||
        error_log('alterQuery called with data: '.json_encode(array_keys($data)));
 | 
			
		||||
 | 
			
		||||
        // create a subquery for activity
 | 
			
		||||
        $sqb = $qb->getEntityManager()->createQueryBuilder();
 | 
			
		||||
        $sqb->select('1')
 | 
			
		||||
@@ -61,6 +59,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
			
		||||
        if (\in_array('activity', $qb->getAllAliases(), true)) {
 | 
			
		||||
            $sqb->andWhere('activity_person_having_activity.id = activity.id');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isset($data['reasons']) && [] !== $data['reasons']) {
 | 
			
		||||
            // add clause activity reason
 | 
			
		||||
            $sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity');
 | 
			
		||||
@@ -125,38 +124,12 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
			
		||||
 | 
			
		||||
    public function normalizeFormData(array $formData): array
 | 
			
		||||
    {
 | 
			
		||||
        $normalized = [
 | 
			
		||||
            'date_from_rolling' => $formData['date_from_rolling']->normalize(),
 | 
			
		||||
            'date_to_rolling' => $formData['date_to_rolling']->normalize(),
 | 
			
		||||
            'reasons' => [],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (isset($formData['reasons']) && [] !== $formData['reasons']) {
 | 
			
		||||
            $normalized['reasons'] = array_map(
 | 
			
		||||
                fn (ActivityReason $reason) => $reason->getId(),
 | 
			
		||||
                $formData['reasons']
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $normalized;
 | 
			
		||||
        return ['date_from_rolling' => $formData['date_from_rolling']->normalize(), 'date_to_rolling' => $formData['date_to_rolling']->normalize()];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function denormalizeFormData(array $formData, int $fromVersion): array
 | 
			
		||||
    {
 | 
			
		||||
        $denormalized = [
 | 
			
		||||
            'date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']),
 | 
			
		||||
            'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling']),
 | 
			
		||||
            'reasons' => [],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (isset($formData['reasons']) && [] !== $formData['reasons']) {
 | 
			
		||||
            $denormalized['reasons'] = array_map(
 | 
			
		||||
                fn ($id) => $this->activityReasonRepository->find($id),
 | 
			
		||||
                $formData['reasons']
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $denormalized;
 | 
			
		||||
        return ['date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']), 'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling'])];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getFormDefaultData(): array
 | 
			
		||||
@@ -170,12 +143,10 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
			
		||||
 | 
			
		||||
    public function describeAction($data, ExportGenerationContext $context): array
 | 
			
		||||
    {
 | 
			
		||||
        $reasons = $data['reasons'] ?? [];
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            [] === $reasons ?
 | 
			
		||||
                'export.filter.activity.describe_action_with_no_subject'
 | 
			
		||||
                : 'export.filter.activity.describe_action_with_subject',
 | 
			
		||||
            [] === $data['reasons'] ?
 | 
			
		||||
                'export.filter.person_between_dates.describe_action_with_no_subject'
 | 
			
		||||
                : 'export.filter.person_between_dates.describe_action_with_subject',
 | 
			
		||||
            [
 | 
			
		||||
                'date_from' => $this->rollingDateConverter->convert($data['date_from_rolling']),
 | 
			
		||||
                'date_to' => $this->rollingDateConverter->convert($data['date_to_rolling']),
 | 
			
		||||
@@ -183,7 +154,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
			
		||||
                    ', ',
 | 
			
		||||
                    array_map(
 | 
			
		||||
                        fn (ActivityReason $r): string => '"'.$this->translatableStringHelper->localize($r->getName()).'"',
 | 
			
		||||
                        $reasons
 | 
			
		||||
                        $data['reasons']
 | 
			
		||||
                    )
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
@@ -197,7 +168,6 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
 | 
			
		||||
 | 
			
		||||
    public function validateForm($data, ExecutionContextInterface $context): void
 | 
			
		||||
    {
 | 
			
		||||
        error_log('validateForm called with data: '.json_encode(array_keys($data)));
 | 
			
		||||
        if ($this->rollingDateConverter->convert($data['date_from_rolling'])
 | 
			
		||||
            >= $this->rollingDateConverter->convert($data['date_to_rolling'])) {
 | 
			
		||||
            $context->buildViolation('export.filter.activity.person_between_dates.date mismatch')
 | 
			
		||||
 
 | 
			
		||||
@@ -136,14 +136,8 @@ export default {
 | 
			
		||||
            issueIsLoading: false,
 | 
			
		||||
            actionIsLoading: false,
 | 
			
		||||
            actionAreLoaded: false,
 | 
			
		||||
            socialIssuesClassList: {
 | 
			
		||||
                "col-form-label": true,
 | 
			
		||||
                required: false,
 | 
			
		||||
            },
 | 
			
		||||
            socialActionsClassList: {
 | 
			
		||||
                "col-form-label": true,
 | 
			
		||||
                required: false,
 | 
			
		||||
            },
 | 
			
		||||
            socialIssuesClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialIssues").getAttribute("required") ? "required" : ""}`,
 | 
			
		||||
            socialActionsClassList: `col-form-label ${document.querySelector("input#chill_activitybundle_activity_socialActions").getAttribute("required") ? "required" : ""}`,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
@@ -164,21 +158,6 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        /* Load classNames after element is present */
 | 
			
		||||
        const socialActionsEl = document.querySelector(
 | 
			
		||||
            "input#chill_activitybundle_activity_socialActions",
 | 
			
		||||
        );
 | 
			
		||||
        if (socialActionsEl && socialActionsEl.hasAttribute("required")) {
 | 
			
		||||
            this.socialActionsClassList.required = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const socialIssuesEl = document.querySelector(
 | 
			
		||||
            "input#chill_activitybundle_activity_socialIssues",
 | 
			
		||||
        );
 | 
			
		||||
        if (socialIssuesEl && socialIssuesEl.hasAttribute("required")) {
 | 
			
		||||
            this.socialIssuesClassList.required = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* Load other issues in multiselect */
 | 
			
		||||
        this.issueIsLoading = true;
 | 
			
		||||
        this.actionAreLoaded = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
 | 
			
		||||
        $config = $this->processConfiguration($configuration, $configs);
 | 
			
		||||
 | 
			
		||||
        $container->setParameter('chill_aside_activity.form.time_duration', $config['form']['time_duration']);
 | 
			
		||||
        $container->setParameter('chill_aside_activity.show_concerned_persons_count', 'visible' === $config['show_concerned_persons_count']);
 | 
			
		||||
 | 
			
		||||
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
 | 
			
		||||
        $loader->load('services.yaml');
 | 
			
		||||
@@ -38,6 +39,24 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
 | 
			
		||||
    {
 | 
			
		||||
        $this->prependRoute($container);
 | 
			
		||||
        $this->prependCruds($container);
 | 
			
		||||
        $this->prependTwigConfig($container);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function prependTwigConfig(ContainerBuilder $container)
 | 
			
		||||
    {
 | 
			
		||||
        // Get the configuration for this bundle
 | 
			
		||||
        $chillAsideActivityConfig = $container->getExtensionConfig($this->getAlias());
 | 
			
		||||
        $config = $this->processConfiguration($this->getConfiguration($chillAsideActivityConfig, $container), $chillAsideActivityConfig);
 | 
			
		||||
 | 
			
		||||
        // Add configuration to twig globals
 | 
			
		||||
        $twigConfig = [
 | 
			
		||||
            'globals' => [
 | 
			
		||||
                'chill_aside_activity_config' => [
 | 
			
		||||
                    'show_concerned_persons_count' => 'visible' === $config['show_concerned_persons_count'],
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
        $container->prependExtensionConfig('twig', $twigConfig);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function prependCruds(ContainerBuilder $container)
 | 
			
		||||
 
 | 
			
		||||
@@ -141,6 +141,12 @@ class Configuration implements ConfigurationInterface
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->enumNode('show_concerned_persons_count')
 | 
			
		||||
            ->values(['hidden', 'visible'])
 | 
			
		||||
            ->defaultValue('hidden')
 | 
			
		||||
            ->info('Show the concerned persons count field in aside activity forms and views')
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end();
 | 
			
		||||
 | 
			
		||||
        return $treeBuilder;
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,10 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    private User $updatedBy;
 | 
			
		||||
 | 
			
		||||
    #[Assert\GreaterThanOrEqual(0)]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true)]
 | 
			
		||||
    private ?int $concernedPersonsCount = 0;
 | 
			
		||||
 | 
			
		||||
    public function getAgent(): ?User
 | 
			
		||||
    {
 | 
			
		||||
        return $this->agent;
 | 
			
		||||
@@ -186,4 +190,16 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getConcernedPersonsCount(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->concernedPersonsCount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setConcernedPersonsCount(?int $concernedPersonsCount): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->concernedPersonsCount = $concernedPersonsCount;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,86 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\AsideActivityBundle\Export\Aggregator;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Declarations;
 | 
			
		||||
use Chill\MainBundle\Export\AggregatorInterface;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class ByConcernedPersonsCountAggregator implements AggregatorInterface
 | 
			
		||||
{
 | 
			
		||||
    public function addRole(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
 | 
			
		||||
    {
 | 
			
		||||
        $qb->addSelect('aside.concernedPersonsCount AS by_concerned_persons_count_aggregator')
 | 
			
		||||
            ->addGroupBy('by_concerned_persons_count_aggregator');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function applyOn(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder): void
 | 
			
		||||
    {
 | 
			
		||||
        // No form needed
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getNormalizationVersion(): int
 | 
			
		||||
    {
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function normalizeFormData(array $formData): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function denormalizeFormData(array $formData, int $fromVersion): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getFormDefaultData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabels($key, array $values, $data): callable
 | 
			
		||||
    {
 | 
			
		||||
        return function ($value): string {
 | 
			
		||||
            if ('_header' === $value) {
 | 
			
		||||
                return 'export.aggregator.Concerned persons count';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (null === $value) {
 | 
			
		||||
                return 'export.aggregator.No concerned persons count specified';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (string) $value;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getQueryKeys($data): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['by_concerned_persons_count_aggregator'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.aggregator.Group by concerned persons count';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,116 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\AsideActivityBundle\Export\Export;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Declarations;
 | 
			
		||||
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
 | 
			
		||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
 | 
			
		||||
use Chill\MainBundle\Export\ExportInterface;
 | 
			
		||||
use Chill\MainBundle\Export\FormatterInterface;
 | 
			
		||||
use Chill\MainBundle\Export\GroupedExportInterface;
 | 
			
		||||
use Doctrine\ORM\Query;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class SumConcernedPersonsCountAsideActivity implements ExportInterface, GroupedExportInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly AsideActivityRepository $repository) {}
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder) {}
 | 
			
		||||
 | 
			
		||||
    public function getNormalizationVersion(): int
 | 
			
		||||
    {
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function normalizeFormData(array $formData): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function denormalizeFormData(array $formData, int $fromVersion): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getFormDefaultData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAllowedFormattersTypes(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [FormatterInterface::TYPE_TABULAR];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Sum concerned persons count for aside activities';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getGroup(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Exports of aside activities';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabels($key, array $values, $data)
 | 
			
		||||
    {
 | 
			
		||||
        if ('export_sum_concerned_persons_count' !== $key) {
 | 
			
		||||
            throw new \LogicException("the key {$key} is not used by this export");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $labels = array_combine($values, $values);
 | 
			
		||||
        $labels['_header'] = $this->getTitle();
 | 
			
		||||
 | 
			
		||||
        return static fn ($value) => $labels[$value];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getQueryKeys($data): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['export_sum_concerned_persons_count'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array
 | 
			
		||||
    {
 | 
			
		||||
        return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'export.Sum concerned persons count for aside activities';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getType(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Declarations::ASIDE_ACTIVITY_TYPE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('aside');
 | 
			
		||||
 | 
			
		||||
        $qb->select('SUM(COALESCE(aside.concernedPersonsCount, 0)) as export_sum_concerned_persons_count');
 | 
			
		||||
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function requiredRole(): string
 | 
			
		||||
    {
 | 
			
		||||
        return AsideActivityVoter::STATS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsModifiers(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            Declarations::ASIDE_ACTIVITY_TYPE,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\Form\FormEvent;
 | 
			
		||||
use Symfony\Component\Form\FormEvents;
 | 
			
		||||
@@ -29,11 +30,13 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
final class AsideActivityFormType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    private readonly array $timeChoices;
 | 
			
		||||
    private readonly bool $showConcernedPersonsCount;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        ParameterBagInterface $parameterBag,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration');
 | 
			
		||||
        $this->showConcernedPersonsCount = $parameterBag->get('chill_aside_activity.show_concerned_persons_count');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options)
 | 
			
		||||
@@ -76,6 +79,16 @@ final class AsideActivityFormType extends AbstractType
 | 
			
		||||
            ->add('location', PickUserLocationType::class)
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        if ($this->showConcernedPersonsCount) {
 | 
			
		||||
            $builder->add('concernedPersonsCount', IntegerType::class, [
 | 
			
		||||
                'label' => 'Concerned persons count',
 | 
			
		||||
                'required' => false,
 | 
			
		||||
                'attr' => [
 | 
			
		||||
                    'min' => 0,
 | 
			
		||||
                ],
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (['duration'] as $fieldName) {
 | 
			
		||||
            $builder->get($fieldName)
 | 
			
		||||
                ->addModelTransformer($durationTimeTransformer);
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,11 @@
 | 
			
		||||
                                {%- if entity.location.name is defined -%}
 | 
			
		||||
                                    <div><i class="fa fa-fw fa-map-marker"></i>{{ entity.location.name }}</div>
 | 
			
		||||
                                {%- endif -%}
 | 
			
		||||
 | 
			
		||||
                                {%- if entity.concernedPersonsCount > 0 -%}
 | 
			
		||||
                                    <div><i class="fa fa-fw fa-user"></i>{{ entity.concernedPersonsCount }}</div>
 | 
			
		||||
                                {%- endif -%}
 | 
			
		||||
 | 
			
		||||
							</div>
 | 
			
		||||
							<div class="item-col" style="justify-content: flex-end;">
 | 
			
		||||
								<div class="box">
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,11 @@
 | 
			
		||||
				<dt class="inline">{{ 'Duration'|trans }}</dt>
 | 
			
		||||
				<dd>{{ entity.duration|date('H:i') }}</dd>
 | 
			
		||||
 | 
			
		||||
                {% if chill_aside_activity_config.show_concerned_persons_count == 'visible' %}
 | 
			
		||||
                    <dt class="inline">{{ 'Concerned persons count'|trans }}</dt>
 | 
			
		||||
                    <dd>{{ entity.concernedPersonsCount }}</dd>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
				<dt class="inline">{{ 'Remark'|trans }}</dt>
 | 
			
		||||
				{%- if entity.note is empty -%}
 | 
			
		||||
					<dd>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\AsideActivityBundle\Tests\Export\Aggregator;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Entity\AsideActivity;
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator;
 | 
			
		||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class ByConcernedPersonsCountAggregatorTest extends AbstractAggregatorTest
 | 
			
		||||
{
 | 
			
		||||
    public function getAggregator()
 | 
			
		||||
    {
 | 
			
		||||
        return new ByConcernedPersonsCountAggregator();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getFormData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            [],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getQueryBuilders(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
        $em = self::getContainer()->get(EntityManagerInterface::class);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            $em->createQueryBuilder()
 | 
			
		||||
                ->select('count(aside.id)')
 | 
			
		||||
                ->from(AsideActivity::class, 'aside'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\AsideActivityBundle\Tests\Export\Export;
 | 
			
		||||
 | 
			
		||||
use Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity;
 | 
			
		||||
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
 | 
			
		||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
final class SumConcernedPersonsCountAsideActivityTest extends AbstractExportTest
 | 
			
		||||
{
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getExport()
 | 
			
		||||
    {
 | 
			
		||||
        $repository = self::getContainer()->get(AsideActivityRepository::class);
 | 
			
		||||
 | 
			
		||||
        yield new SumConcernedPersonsCountAsideActivity($repository);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getFormData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            [],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getModifiersCombination(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            ['aside_activity'],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -20,6 +20,10 @@ services:
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export, alias: 'avg_aside_activity_duration' }
 | 
			
		||||
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity:
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export, alias: 'sum_aside_activity_concerned_persons_count' }
 | 
			
		||||
 | 
			
		||||
  ## Filters
 | 
			
		||||
  chill.aside_activity.export.date_filter:
 | 
			
		||||
    class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
 | 
			
		||||
@@ -70,3 +74,7 @@ services:
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Aggregator\ByLocationAggregator:
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export_aggregator, alias: 'aside_activity_location_aggregator' }
 | 
			
		||||
 | 
			
		||||
  Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator:
 | 
			
		||||
      tags:
 | 
			
		||||
          - { name: chill.export_aggregator, alias: 'aside_activity_concerned_persons_count_aggregator' }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\AsideActivity;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20251006113048 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add concernedPersonsCount property to AsideActivity entity';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_asideactivity.asideactivity ADD concernedPersonsCount INT DEFAULT 0');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_asideactivity.AsideActivity DROP concernedPersonsCount');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -27,6 +27,7 @@ Emergency: Urgent
 | 
			
		||||
by: "Par "
 | 
			
		||||
location: Lieu
 | 
			
		||||
Asideactivity location: Localisation de l'activité
 | 
			
		||||
Concerned persons count: Nombre d'usager concernés
 | 
			
		||||
 | 
			
		||||
# Crud
 | 
			
		||||
crud:
 | 
			
		||||
@@ -190,6 +191,7 @@ export:
 | 
			
		||||
    Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
 | 
			
		||||
    Average aside activities duration: Durée moyenne des activités annexes
 | 
			
		||||
    Sum aside activities duration: Durée des activités annexes
 | 
			
		||||
    Sum concerned persons count for aside activities: Nombre d'usager concernés par les activités annexes
 | 
			
		||||
    filter:
 | 
			
		||||
        Filter by aside activity date: Filtrer les activités annexes par date
 | 
			
		||||
        Filter by aside activity type: Filtrer les activités annexes par type d'activité
 | 
			
		||||
@@ -210,6 +212,8 @@ export:
 | 
			
		||||
        'Filtered by aside activity location: only %location%': "Filtré par localisation: uniquement %location%"
 | 
			
		||||
    aggregator:
 | 
			
		||||
        Group by aside activity type: Grouper les activités annexes par type d'activité
 | 
			
		||||
        Group by concerned persons count: Grouper les activités annexes par nombre d'usagers conernés
 | 
			
		||||
        Concerned persons count: Nombre d'usagers concernés
 | 
			
		||||
        Aside activity type: Type d'activité annexe
 | 
			
		||||
        by_user_job:
 | 
			
		||||
            Aggregate by user job: Grouper les activités annexes par métier des utilisateurs
 | 
			
		||||
 
 | 
			
		||||
@@ -25,24 +25,12 @@
 | 
			
		||||
                            <div class="item-bloc">
 | 
			
		||||
                                <div class="item-row">
 | 
			
		||||
                                    <div class="item-col" style="flex-basis:100%;">
 | 
			
		||||
                                        <h2>{{ entity.name|localize_translatable_string }} </h2>
 | 
			
		||||
                                        <p style="margin-left: 1rem;"><span class="badge bg-chill-gray">
 | 
			
		||||
                                            {% if entity.active %}
 | 
			
		||||
                                                {{ 'admin.active'|trans }}
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                                {{ 'admin.not active'|trans }}
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                        </span></p>
 | 
			
		||||
                                        <h2>{{ entity.name|localize_translatable_string }}</h2>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div class="item-row">
 | 
			
		||||
                                    <p><span class="badge bg-chill-green-dark">{{ contextManager.getContextByKey(entity.context).name|trans }}</span></p>
 | 
			
		||||
                                </div>
 | 
			
		||||
{#                                <div class="item-row">#}
 | 
			
		||||
{#                                    <div class="item-col" style="flex-basis:100%;">#}
 | 
			
		||||
{##}
 | 
			
		||||
{#                                    </div>#}
 | 
			
		||||
{#                                </div>#}
 | 
			
		||||
                                <div class="item-row">
 | 
			
		||||
                                    <div class="item-col"></div>
 | 
			
		||||
                                    <ul class="record_actions item-col flex-shrink-1">
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,3 @@ crud:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Template file: Fichier modèle
 | 
			
		||||
 | 
			
		||||
admin:
 | 
			
		||||
    active: Actif
 | 
			
		||||
    not active: Non-actif
 | 
			
		||||
 
 | 
			
		||||
@@ -23,14 +23,10 @@ use Random\RandomException;
 | 
			
		||||
 * Store each version of StoredObject's.
 | 
			
		||||
 *
 | 
			
		||||
 * A version should not be created manually: use the method @see{StoredObject::registerVersion} instead.
 | 
			
		||||
 *
 | 
			
		||||
 * Each filename must be unique within the same StoredObject. We add a condition on id to apply this condition only for
 | 
			
		||||
 * newly created versions when this new index is applied.
 | 
			
		||||
 */
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table('chill_doc.stored_object_version')]
 | 
			
		||||
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])]
 | 
			
		||||
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_filename', columns: ['filename'], options: ['where' => '(id > 0)'])]
 | 
			
		||||
class StoredObjectVersion implements TrackCreationInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
 
 | 
			
		||||
@@ -36,18 +36,6 @@ export interface GenericDocForAccompanyingPeriod extends GenericDoc {
 | 
			
		||||
    context: "accompanying-period";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isGenericDocForAccompanyingPeriod(
 | 
			
		||||
    doc: GenericDoc,
 | 
			
		||||
): doc is GenericDocForAccompanyingPeriod {
 | 
			
		||||
    return doc.context === "accompanying-period";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isGenericDocWithStoredObject(
 | 
			
		||||
    doc: GenericDoc,
 | 
			
		||||
): doc is GenericDoc & { storedObject: StoredObject } {
 | 
			
		||||
    return doc.storedObject !== null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface BaseMetadataWithHtml extends BaseMetadata {
 | 
			
		||||
    html: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -56,33 +44,28 @@ export interface GenericDocForAccompanyingCourseDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "accompanying_course_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
    storedObject: StoredObject;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCourseActivityDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "accompanying_course_activity_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
    storedObject: StoredObject;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCourseCalendarDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "accompanying_course_calendar_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
    storedObject: StoredObject;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCoursePersonDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "person_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
    storedObject: StoredObject;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GenericDocForAccompanyingCourseWorkEvaluationDocument
 | 
			
		||||
    extends GenericDocForAccompanyingPeriod {
 | 
			
		||||
    key: "accompanying_period_work_evaluation_document";
 | 
			
		||||
    metadata: BaseMetadataWithHtml;
 | 
			
		||||
    storedObject: StoredObject;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\DocStore;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20251013094414 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'DocStore: Enforce filename uniqueness on chill_doc.stored_object_version; clean duplicates and add partial unique index on filename (for new rows only).';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // 1) Clean duplicates: for each (stored_object_id, filename, key, iv), keep only the last inserted row
 | 
			
		||||
        //    and delete all others. Use ROW_NUMBER over id DESC to define the last one.
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            WITH ranked AS (
 | 
			
		||||
                SELECT id,
 | 
			
		||||
                       rank() OVER (
 | 
			
		||||
                           PARTITION BY stored_object_id, filename, "key"::jsonb, iv::jsonb
 | 
			
		||||
                           ORDER BY id DESC
 | 
			
		||||
                       ) AS rn
 | 
			
		||||
                FROM chill_doc.stored_object_version
 | 
			
		||||
            )
 | 
			
		||||
            DELETE FROM chill_doc.stored_object_version sov
 | 
			
		||||
            USING ranked r
 | 
			
		||||
            WHERE sov.id = r.id
 | 
			
		||||
              AND r.rn > 1
 | 
			
		||||
        SQL);
 | 
			
		||||
 | 
			
		||||
        // 2) Create a partial unique index on filename that applies only to subsequently inserted rows.
 | 
			
		||||
        //    Per user's instruction, compute the cutoff using the stored_object_id sequence value.
 | 
			
		||||
        $nextVal = (int) $this->connection->fetchOne("SELECT nextval('chill_doc.stored_object_version_id_seq')");
 | 
			
		||||
 | 
			
		||||
        // Safety: if somehow sequence is not available, fallback to current max id from the table
 | 
			
		||||
        if ($nextVal <= 0) {
 | 
			
		||||
            $nextVal = (int) $this->connection->fetchOne('SELECT COALESCE(MAX(id), 0) FROM chill_doc.stored_object_version');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->addSql(sprintf(
 | 
			
		||||
            'CREATE UNIQUE INDEX chill_doc_stored_object_version_unique_by_filename ON chill_doc.stored_object_version (filename) WHERE id > %d',
 | 
			
		||||
            $nextVal
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // Drop the partial unique index; data cleanup is irreversible.
 | 
			
		||||
        $this->addSql('DROP INDEX IF EXISTS chill_doc_stored_object_version_unique_by_filename');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -334,7 +334,7 @@ class ChillImportUsersCommand extends Command
 | 
			
		||||
 | 
			
		||||
    protected function loadUsers()
 | 
			
		||||
    {
 | 
			
		||||
        $reader = Reader::from($this->tempInput->getArgument('csvfile'));
 | 
			
		||||
        $reader = Reader::createFromPath($this->tempInput->getArgument('csvfile'));
 | 
			
		||||
        $reader->setHeaderOffset(0);
 | 
			
		||||
 | 
			
		||||
        foreach ($reader->getRecords() as $line => $r) {
 | 
			
		||||
@@ -362,7 +362,7 @@ class ChillImportUsersCommand extends Command
 | 
			
		||||
 | 
			
		||||
    protected function prepareGroupingCenters()
 | 
			
		||||
    {
 | 
			
		||||
        $reader = Reader::from($this->tempInput->getOption('grouping-centers'));
 | 
			
		||||
        $reader = Reader::createFromPath($this->tempInput->getOption('grouping-centers'));
 | 
			
		||||
        $reader->setHeaderOffset(0);
 | 
			
		||||
 | 
			
		||||
        foreach ($reader->getRecords() as $r) {
 | 
			
		||||
@@ -378,7 +378,7 @@ class ChillImportUsersCommand extends Command
 | 
			
		||||
 | 
			
		||||
    protected function prepareWriter()
 | 
			
		||||
    {
 | 
			
		||||
        $this->output = $output = Writer::from($this->tempInput
 | 
			
		||||
        $this->output = $output = Writer::createFromPath($this->tempInput
 | 
			
		||||
            ->getOption('csv-dump'), 'a+');
 | 
			
		||||
 | 
			
		||||
        $output->insertOne([
 | 
			
		||||
 
 | 
			
		||||
@@ -119,7 +119,7 @@ class ChillUserSendRenewPasswordCodeCommand extends Command
 | 
			
		||||
    protected function getReader()
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $reader = Reader::from($this->input->getArgument('csvfile'));
 | 
			
		||||
            $reader = Reader::createFromPath($this->input->getArgument('csvfile'));
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            $this->logger->error('The csv file could not be read', [
 | 
			
		||||
                'path' => $this->input->getArgument('csvfile'),
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ final readonly class UserExportController
 | 
			
		||||
 | 
			
		||||
        $users = $this->userRepository->findAllAsArray($request->getLocale());
 | 
			
		||||
 | 
			
		||||
        $csv = Writer::from('php://temp', 'r+');
 | 
			
		||||
        $csv = Writer::createFromPath('php://temp', 'r+');
 | 
			
		||||
        $csv->insertOne(
 | 
			
		||||
            array_map(
 | 
			
		||||
                fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
 | 
			
		||||
@@ -104,7 +104,7 @@ final readonly class UserExportController
 | 
			
		||||
 | 
			
		||||
        $userPermissions = $this->userRepository->findAllUserACLAsArray();
 | 
			
		||||
 | 
			
		||||
        $csv = Writer::from('php://temp', 'r+');
 | 
			
		||||
        $csv = Writer::createFromPath('php://temp', 'r+');
 | 
			
		||||
        $csv->insertOne(
 | 
			
		||||
            array_map(
 | 
			
		||||
                fn (string $e) => $this->translator->trans('admin.users.export.'.$e),
 | 
			
		||||
 
 | 
			
		||||
@@ -264,12 +264,11 @@ class WorkflowController extends AbstractController
 | 
			
		||||
    {
 | 
			
		||||
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
 | 
			
		||||
 | 
			
		||||
        $total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser(), false);
 | 
			
		||||
        $total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser());
 | 
			
		||||
        $paginator = $this->paginatorFactory->create($total);
 | 
			
		||||
 | 
			
		||||
        $workflows = $this->entityWorkflowRepository->findBySubscriber(
 | 
			
		||||
            $this->security->getUser(),
 | 
			
		||||
            false,
 | 
			
		||||
            ['createdAt' => 'DESC'],
 | 
			
		||||
            $paginator->getItemsPerPage(),
 | 
			
		||||
            $paginator->getCurrentPageFirstItemNumber()
 | 
			
		||||
 
 | 
			
		||||
@@ -205,11 +205,6 @@ class ChillMainExtension extends Extension implements
 | 
			
		||||
                []
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $container->setParameter(
 | 
			
		||||
            'chill_main.top_banner',
 | 
			
		||||
            $config['top_banner'] ?? []
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
 | 
			
		||||
        $loader->load('services.yaml');
 | 
			
		||||
        $loader->load('services/doctrine.yaml');
 | 
			
		||||
@@ -255,7 +250,6 @@ class ChillMainExtension extends Extension implements
 | 
			
		||||
                    'name' => $config['installation_name'], ],
 | 
			
		||||
                'available_languages' => $config['available_languages'],
 | 
			
		||||
                'add_address' => $config['add_address'],
 | 
			
		||||
                'chill_main_config' => $config,
 | 
			
		||||
            ],
 | 
			
		||||
            'form_themes' => ['@ChillMain/Form/fields.html.twig'],
 | 
			
		||||
        ];
 | 
			
		||||
 
 | 
			
		||||
@@ -168,20 +168,6 @@ class Configuration implements ConfigurationInterface
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->arrayNode('top_banner')
 | 
			
		||||
            ->canBeUnset()
 | 
			
		||||
            ->children()
 | 
			
		||||
            ->booleanNode('visible')
 | 
			
		||||
            ->defaultFalse()
 | 
			
		||||
            ->end()
 | 
			
		||||
            ->arrayNode('text')
 | 
			
		||||
            ->useAttributeAsKey('lang')
 | 
			
		||||
            ->scalarPrototype()->end()
 | 
			
		||||
            ->end() // end of text
 | 
			
		||||
            ->scalarNode('color')->defaultNull()->end()
 | 
			
		||||
            ->scalarNode('background_color')->defaultNull()->end()
 | 
			
		||||
            ->end() // end of top_banner children
 | 
			
		||||
            ->end() // end of top_banner
 | 
			
		||||
            ->arrayNode('widgets')
 | 
			
		||||
            ->canBeEnabled()
 | 
			
		||||
            ->canBeUnset()
 | 
			
		||||
 
 | 
			
		||||
@@ -53,16 +53,11 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
 | 
			
		||||
    public function run(array $lastExecutionData): ?array
 | 
			
		||||
    {
 | 
			
		||||
        $now = $this->clock->now();
 | 
			
		||||
 | 
			
		||||
        if (isset($lastExecutionData['last_execution'])) {
 | 
			
		||||
            $lastExecution = \DateTimeImmutable::createFromFormat(
 | 
			
		||||
                \DateTimeImmutable::ATOM,
 | 
			
		||||
                $lastExecutionData['last_execution']
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (false === $lastExecution) {
 | 
			
		||||
                $lastExecution = $now->sub(new \DateInterval('P1D'));
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            $lastExecution = $now->sub(new \DateInterval('P1D'));
 | 
			
		||||
        }
 | 
			
		||||
@@ -101,7 +96,7 @@ readonly class DailyNotificationDigestCronjob implements CronJobInterface
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'last_execution' => $now->format(\DateTimeInterface::ATOM),
 | 
			
		||||
            'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -57,15 +57,9 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
			
		||||
        return (int) $qb->getQuery()->getSingleScalarResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @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
 | 
			
		||||
    public function countBySubscriber(User $user): int
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->buildQueryBySubscriber($user, $isFinal)->select('count(ew)');
 | 
			
		||||
        $qb = $this->buildQueryBySubscriber($user)->select('count(ew)');
 | 
			
		||||
 | 
			
		||||
        return (int) $qb->getQuery()->getSingleScalarResult();
 | 
			
		||||
    }
 | 
			
		||||
@@ -188,14 +182,9 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
			
		||||
        return $qb->getQuery()->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @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
 | 
			
		||||
    public function findBySubscriber(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->buildQueryBySubscriber($user, $isFinal)->select('ew');
 | 
			
		||||
        $qb = $this->buildQueryBySubscriber($user)->select('ew');
 | 
			
		||||
 | 
			
		||||
        foreach ($orderBy as $key => $sort) {
 | 
			
		||||
            $qb->addOrderBy('ew.'.$key, $sort);
 | 
			
		||||
@@ -323,7 +312,7 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildQueryBySubscriber(User $user, ?bool $isFinal): QueryBuilder
 | 
			
		||||
    private function buildQueryBySubscriber(User $user): QueryBuilder
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('ew');
 | 
			
		||||
 | 
			
		||||
@@ -336,14 +325,6 @@ class EntityWorkflowRepository implements ObjectRepository
 | 
			
		||||
 | 
			
		||||
        $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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,4 @@
 | 
			
		||||
import {
 | 
			
		||||
    GenericDoc,
 | 
			
		||||
    isGenericDocWithStoredObject,
 | 
			
		||||
} from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
 | 
			
		||||
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
 | 
			
		||||
 | 
			
		||||
@@ -206,25 +203,6 @@ export interface WorkflowAttachment {
 | 
			
		||||
    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;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/gener
 | 
			
		||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
 | 
			
		||||
import { GenericDoc } from "ChillDocStoreAssets/types";
 | 
			
		||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
 | 
			
		||||
import { trans, WORKFLOW_ATTACHMENTS_ADD_AN_ATTACHMENT } from "translator";
 | 
			
		||||
 | 
			
		||||
interface AppConfig {
 | 
			
		||||
    workflowId: number;
 | 
			
		||||
@@ -84,7 +83,7 @@ const canEditAttachement = computed<boolean>(() => {
 | 
			
		||||
    <ul v-if="canEditAttachement" class="record_actions">
 | 
			
		||||
        <li>
 | 
			
		||||
            <button type="button" class="btn btn-create" @click="openModal">
 | 
			
		||||
                {{ trans(WORKFLOW_ATTACHMENTS_ADD_AN_ATTACHMENT) }}
 | 
			
		||||
                Ajouter une pièce jointe
 | 
			
		||||
            </button>
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,7 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {
 | 
			
		||||
    AttachmentWithDocAndStored,
 | 
			
		||||
    EntityWorkflow,
 | 
			
		||||
    isAttachmentWithDocAndStored,
 | 
			
		||||
    WorkflowAttachment,
 | 
			
		||||
} from "ChillMainAssets/types";
 | 
			
		||||
import { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
 | 
			
		||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
 | 
			
		||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
import { trans, WORKFLOW_ATTACHMENTS_NO_ATTACHMENT } from "translator";
 | 
			
		||||
 | 
			
		||||
interface AttachmentListProps {
 | 
			
		||||
    attachments: WorkflowAttachment[];
 | 
			
		||||
@@ -21,43 +14,35 @@ const emit = defineEmits<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <p
 | 
			
		||||
        v-if="notNullAttachments.length === 0"
 | 
			
		||||
        v-if="props.attachments.length === 0"
 | 
			
		||||
        class="chill-no-data-statement text-center"
 | 
			
		||||
    >
 | 
			
		||||
        {{ trans(WORKFLOW_ATTACHMENTS_NO_ATTACHMENT) }}
 | 
			
		||||
        Aucune pièce jointe
 | 
			
		||||
    </p>
 | 
			
		||||
    <div v-else class="flex-table">
 | 
			
		||||
        <div v-for="a in notNullAttachments" :key="a.id" class="item-bloc">
 | 
			
		||||
    <!-- TODO translate -->
 | 
			
		||||
    <div else class="flex-table">
 | 
			
		||||
        <div v-for="a in props.attachments" :key="a.id" class="item-bloc">
 | 
			
		||||
            <generic-doc-item-box
 | 
			
		||||
                v-if="a.genericDoc !== null"
 | 
			
		||||
                :generic-doc="a.genericDoc"
 | 
			
		||||
            ></generic-doc-item-box>
 | 
			
		||||
            <div class="item-row separator">
 | 
			
		||||
                <ul class="record_actions">
 | 
			
		||||
                    <li>
 | 
			
		||||
                    <li v-if="a.genericDoc?.storedObject !== null">
 | 
			
		||||
                        <document-action-buttons-group
 | 
			
		||||
                            :stored-object="a.genericDoc.storedObject"
 | 
			
		||||
                        ></document-action-buttons-group>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li v-if="canRemove">
 | 
			
		||||
                    <li
 | 
			
		||||
                        v-if="
 | 
			
		||||
                            !workflow?._permissions
 | 
			
		||||
                                .CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT
 | 
			
		||||
                        "
 | 
			
		||||
                    >
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            class="btn btn-delete"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
 | 
			
		||||
interface GenericDocItemBoxProps {
 | 
			
		||||
    genericDoc: GenericDoc;
 | 
			
		||||
    genericDoc: GenericDocForAccompanyingPeriod;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<GenericDocItemBoxProps>();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
{% 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 %}
 | 
			
		||||
@@ -26,10 +26,6 @@
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<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 %}
 | 
			
		||||
        {{ include('@ChillMain/Layout/_debug.html.twig') }}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ class AddressReferenceBEFromBestAddress
 | 
			
		||||
 | 
			
		||||
        $uncompressedStream = gzopen($tmpname, 'r');
 | 
			
		||||
 | 
			
		||||
        $csv = Reader::from($uncompressedStream);
 | 
			
		||||
        $csv = Reader::createFromStream($uncompressedStream);
 | 
			
		||||
        $csv->setDelimiter(',');
 | 
			
		||||
        $csv->setHeaderOffset(0);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -287,7 +287,7 @@ final class AddressReferenceBaseImporter
 | 
			
		||||
 | 
			
		||||
            $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));
 | 
			
		||||
            $writer = Writer::from($path, 'w+');
 | 
			
		||||
            $writer = Writer::createFromPath($path, 'w+');
 | 
			
		||||
            // insert headers
 | 
			
		||||
            $writer->insertOne([
 | 
			
		||||
                'postalcode',
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@ class AddressReferenceFromBAN
 | 
			
		||||
        // re-open it to read it
 | 
			
		||||
        $csvDecompressed = gzopen($path, 'r');
 | 
			
		||||
 | 
			
		||||
        $csv = Reader::from($csvDecompressed);
 | 
			
		||||
        $csv = Reader::createFromStream($csvDecompressed);
 | 
			
		||||
        $csv->setDelimiter(';')->setHeaderOffset(0);
 | 
			
		||||
        $stmt = new Statement();
 | 
			
		||||
        $stmt = $stmt->process($csv, [
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ class AddressReferenceFromBano
 | 
			
		||||
 | 
			
		||||
        fseek($file, 0);
 | 
			
		||||
 | 
			
		||||
        $csv = Reader::from($file);
 | 
			
		||||
        $csv = Reader::createFromStream($file);
 | 
			
		||||
        $csv->setDelimiter(',');
 | 
			
		||||
        $stmt = new Statement();
 | 
			
		||||
        $stmt = $stmt->process($csv, [
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ class AddressReferenceLU
 | 
			
		||||
 | 
			
		||||
        fseek($file, 0);
 | 
			
		||||
 | 
			
		||||
        $csv = Reader::from($file);
 | 
			
		||||
        $csv = Reader::createFromStream($file);
 | 
			
		||||
        $csv->setDelimiter(';');
 | 
			
		||||
        $csv->setHeaderOffset(0);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ class PostalCodeBEFromBestAddress
 | 
			
		||||
 | 
			
		||||
        $uncompressedStream = gzopen($tmpname, 'r');
 | 
			
		||||
 | 
			
		||||
        $csv = Reader::from($uncompressedStream);
 | 
			
		||||
        $csv = Reader::createFromStream($uncompressedStream);
 | 
			
		||||
        $csv->setDelimiter(',');
 | 
			
		||||
        $csv->setHeaderOffset(0);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ class PostalCodeFRFromOpenData
 | 
			
		||||
 | 
			
		||||
        fseek($tmpfile, 0);
 | 
			
		||||
 | 
			
		||||
        $csv = Reader::from($tmpfile);
 | 
			
		||||
        $csv = Reader::createFromStream($tmpfile);
 | 
			
		||||
        $csv->setDelimiter(',');
 | 
			
		||||
        $csv->setHeaderOffset(0);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ use Symfony\Component\Notifier\Event\SentMessageEvent;
 | 
			
		||||
final readonly class SentMessageEventSubscriber implements EventSubscriberInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private LoggerInterface $notifierLogger, // will be send to "notifierLogger" if it exists
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public static function getSubscribedEvents()
 | 
			
		||||
@@ -33,9 +33,9 @@ final readonly class SentMessageEventSubscriber implements EventSubscriberInterf
 | 
			
		||||
        $message = $event->getMessage();
 | 
			
		||||
 | 
			
		||||
        if (null === $message->getMessageId()) {
 | 
			
		||||
            $this->notifierLogger->info('[sms] a sms message did not had any id after sending.', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId()]);
 | 
			
		||||
            $this->logger->info('[sms] a sms message did not had any id after sending.', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId()]);
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->notifierLogger->warning('[sms] a sms was sent', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId(), 'idsI' => $message->getMessageId()]);
 | 
			
		||||
            $this->logger->warning('[sms] a sms was sent', ['validReceiversI' => $message->getOriginalMessage()->getRecipientId(), 'idsI' => $message->getMessageId()]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,98 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\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,5 +37,10 @@ class DailyNotificationDigestCronJobFunctionalTest extends KernelTestCase
 | 
			
		||||
        $actual = $this->dailyNotificationDigestCronjob->run([]);
 | 
			
		||||
 | 
			
		||||
        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,21 +12,16 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\MainBundle\Tests\Notification\Email;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Notification\Email\DailyNotificationDigestCronjob;
 | 
			
		||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
 | 
			
		||||
use Doctrine\DBAL\Connection;
 | 
			
		||||
use Doctrine\DBAL\Result;
 | 
			
		||||
use Doctrine\DBAL\Statement;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Component\Clock\MockClock;
 | 
			
		||||
use Symfony\Component\Messenger\Envelope;
 | 
			
		||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @covers \DailyNotificationDigestCronjob
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class DailyNotificationDigestCronJobTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
@@ -35,7 +30,6 @@ class DailyNotificationDigestCronJobTest extends TestCase
 | 
			
		||||
    private MessageBusInterface $messageBus;
 | 
			
		||||
    private LoggerInterface $logger;
 | 
			
		||||
    private DailyNotificationDigestCronjob $cronjob;
 | 
			
		||||
    private \DateTimeImmutable $firstNow;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
@@ -44,8 +38,6 @@ class DailyNotificationDigestCronJobTest extends TestCase
 | 
			
		||||
        $this->messageBus = $this->createMock(MessageBusInterface::class);
 | 
			
		||||
        $this->logger = $this->createMock(LoggerInterface::class);
 | 
			
		||||
 | 
			
		||||
        $this->firstNow = new \DateTimeImmutable('2024-01-02T07:15:00+00:00');
 | 
			
		||||
 | 
			
		||||
        $this->cronjob = new DailyNotificationDigestCronjob(
 | 
			
		||||
            $this->clock,
 | 
			
		||||
            $this->connection,
 | 
			
		||||
@@ -86,129 +78,4 @@ class DailyNotificationDigestCronJobTest extends TestCase
 | 
			
		||||
            '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');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -670,8 +670,6 @@ workflow:
 | 
			
		||||
 | 
			
		||||
    attachments:
 | 
			
		||||
        title: Pièces jointes
 | 
			
		||||
        no_attachment: Aucune pièce jointe
 | 
			
		||||
        Add_an_attachment: Ajouter une pièce jointe
 | 
			
		||||
 | 
			
		||||
    wait:
 | 
			
		||||
        title: En attente de traitement
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ final class ImportSocialWorkMetadata extends Command
 | 
			
		||||
        $filepath = $input->getOption('filepath');
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $csv = Reader::from($filepath);
 | 
			
		||||
            $csv = Reader::createFromPath($filepath);
 | 
			
		||||
        } catch (\Throwable $e) {
 | 
			
		||||
            throw new \Exception('Error while loading CSV.', 0, $e);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ class LoadSocialWorkMetadata extends Fixture implements OrderedFixtureInterface
 | 
			
		||||
    public function load(ObjectManager $manager): void
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $csv = Reader::from(__DIR__.'/data/social_work_metadata.csv');
 | 
			
		||||
            $csv = Reader::createFromPath(__DIR__.'/data/social_work_metadata.csv');
 | 
			
		||||
        } catch (\Throwable $e) {
 | 
			
		||||
            throw new \Exception('Error while loading CSV.', 0, $e);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,6 @@ import {
 | 
			
		||||
    EVALUATION_DOCUMENT_MOVE_SUCCESS,
 | 
			
		||||
} from "translator";
 | 
			
		||||
import { useToast } from "vue-toast-notification";
 | 
			
		||||
import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
 | 
			
		||||
 | 
			
		||||
const props = defineProps(["evaluation", "docAnchorId"]);
 | 
			
		||||
const store = useStore();
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ final readonly class SocialActionCSVExportService
 | 
			
		||||
            array_keys($this->formatRow(new SocialAction()))
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $csv = Writer::from('php://temp', 'w+');
 | 
			
		||||
        $csv = Writer::createFromPath('php://temp', 'w+');
 | 
			
		||||
        $csv->insertOne($headers);
 | 
			
		||||
 | 
			
		||||
        foreach ($actions as $action) {
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ readonly class SocialIssueCSVExportService
 | 
			
		||||
    public function generateCsv(array $issues): Writer
 | 
			
		||||
    {
 | 
			
		||||
        // CSV headers
 | 
			
		||||
        $csv = Writer::from('php://temp', 'r+');
 | 
			
		||||
        $csv = Writer::createFromPath('php://temp', 'r+');
 | 
			
		||||
        $csv->insertOne(
 | 
			
		||||
            array_map(
 | 
			
		||||
                fn (string $e) => $this->translator->trans($e),
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ class ThirdpartyCSVExportController extends AbstractController
 | 
			
		||||
            fwrite($output, "\xEF\xBB\xBF");
 | 
			
		||||
 | 
			
		||||
            // Create CSV writer
 | 
			
		||||
            $csv = Writer::from($output);
 | 
			
		||||
            $csv = Writer::createFromStream($output);
 | 
			
		||||
 | 
			
		||||
            // Write header row
 | 
			
		||||
            $header = array_map(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user