mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-03 18:58:24 +00:00 
			
		
		
		
	Compare commits
	
		
			27 Commits
		
	
	
		
			v4.4.1
			...
			405-aside-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 80ea99cb9e | |||
| 48df0d49d3 | |||
| bf6375a0b5 | |||
| 21383ddbe7 | |||
| d451d87cdf | |||
| ac6336d197 | |||
| a46b301e44 | |||
| 05f0443011 | |||
| 7f8d8f891e | |||
| ddb932a4fa | |||
| 3a02f15bcd | |||
| bc2fbee5c6 | |||
| ebd10ca522 | |||
| 
						
						
							
						
						d3a31be412
	
				 | 
					
					
						|||
| 
						
						
							
						
						d159a82f88
	
				 | 
					
					
						|||
| 
						
						
							
						
						c2d9c73fd4
	
				 | 
					
					
						|||
| 0d6d15fcf7 | |||
| 
						
						
							
						
						f9ad96c78b
	
				 | 
					
					
						|||
| 
						
						
							
						
						fcc9529a20
	
				 | 
					
					
						|||
| 
						
						
							
						
						955cb817c4
	
				 | 
					
					
						|||
| 823f9546b9 | |||
| be39fa16e7 | |||
| c8bb7575e7 | |||
| 
						 | 
					80a3734171 | ||
| 
						
						
							
						
						ab98f3a102
	
				 | 
					
					
						|||
| 7516e68d77 | |||
| 7b60b7a8af | 
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										3
									
								
								.changes/v4.4.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v4.4.2.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
## v4.4.2 - 2025-09-12
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix document generation and workflow generation do not work on accompanying period work documents   
 | 
			
		||||
							
								
								
									
										13
									
								
								.changes/v4.5.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.changes/v4.5.0.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
## v4.5.0 - 2025-10-03
 | 
			
		||||
### Feature
 | 
			
		||||
* Only allow delete of attachment on workflows that are not final   
 | 
			
		||||
* Move up signature buttons on index workflow page for easier access   
 | 
			
		||||
* Filter out document from attachment list if it is the same as the workflow document   
 | 
			
		||||
* Block edition on attached document on workflow, if the workflow is finalized or sent external   
 | 
			
		||||
* Convert workflow's attached document to pdf while sending them external   
 | 
			
		||||
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition   
 | 
			
		||||
### Fixed
 | 
			
		||||
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance   
 | 
			
		||||
* Fix permissions on storedObject which are subject by a workflow   
 | 
			
		||||
### DX
 | 
			
		||||
* Introduce a WaitingScreen component to display a waiting screen   
 | 
			
		||||
							
								
								
									
										4
									
								
								.changes/v4.5.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.changes/v4.5.1.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
## v4.5.1 - 2025-10-03
 | 
			
		||||
### Fixed
 | 
			
		||||
* Add missing javascript dependency   
 | 
			
		||||
* Add exception handling for conversion of attachment on sending external, when documens are already in pdf   
 | 
			
		||||
							
								
								
									
										23
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -6,6 +6,29 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
 | 
			
		||||
and is generated by [Changie](https://github.com/miniscruff/changie).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## v4.5.1 - 2025-10-03
 | 
			
		||||
### Fixed
 | 
			
		||||
* Add missing javascript dependency   
 | 
			
		||||
* Add exception handling for conversion of attachment on sending external, when documens are already in pdf   
 | 
			
		||||
 | 
			
		||||
## v4.5.0 - 2025-10-03
 | 
			
		||||
### Feature
 | 
			
		||||
* Only allow delete of attachment on workflows that are not final   
 | 
			
		||||
* Move up signature buttons on index workflow page for easier access   
 | 
			
		||||
* Filter out document from attachment list if it is the same as the workflow document   
 | 
			
		||||
* Block edition on attached document on workflow, if the workflow is finalized or sent external   
 | 
			
		||||
* Convert workflow's attached document to pdf while sending them external   
 | 
			
		||||
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition   
 | 
			
		||||
### Fixed
 | 
			
		||||
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance   
 | 
			
		||||
* Fix permissions on storedObject which are subject by a workflow   
 | 
			
		||||
### DX
 | 
			
		||||
* Introduce a WaitingScreen component to display a waiting screen   
 | 
			
		||||
 | 
			
		||||
## v4.4.2 - 2025-09-12
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix document generation and workflow generation do not work on accompanying period work documents   
 | 
			
		||||
 | 
			
		||||
## v4.4.1 - 2025-09-11
 | 
			
		||||
### Fixed
 | 
			
		||||
* fix translations in duplicate evaluation document modal and realign close modal button   
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
@@ -55,6 +55,7 @@
 | 
			
		||||
    "@tsconfig/node20": "^20.1.4",
 | 
			
		||||
    "@types/dompurify": "^3.0.5",
 | 
			
		||||
    "@types/leaflet": "^1.9.3",
 | 
			
		||||
    "@vueuse/core": "^13.9.0",
 | 
			
		||||
    "bootstrap-icons": "^1.11.3",
 | 
			
		||||
    "dropzone": "^5.7.6",
 | 
			
		||||
    "es6-promise": "^4.2.8",
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize(
 | 
			
		||||
                new Collection($items, $paginator),
 | 
			
		||||
                new Collection(array_values($items->toArray()), $paginator),
 | 
			
		||||
                'json',
 | 
			
		||||
                [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Exception;
 | 
			
		||||
 | 
			
		||||
class ConversionWithSameMimeTypeException extends \RuntimeException
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(string $mimeType, ?\Throwable $previous = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct("Conversion to same MIME type '{$mimeType}' is not allowed: already at the same MIME type", 0, $previous);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -25,7 +25,7 @@ export interface GenericDoc {
 | 
			
		||||
    type: "doc_store_generic_doc";
 | 
			
		||||
    uniqueKey: string;
 | 
			
		||||
    key: string;
 | 
			
		||||
    identifiers: object;
 | 
			
		||||
    identifiers: { id: number };
 | 
			
		||||
    context: "person" | "accompanying-period";
 | 
			
		||||
    doc_date: DateTime;
 | 
			
		||||
    metadata: GenericDocMetadata;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { StoredObject, StoredObjectVersion } from "../../types";
 | 
			
		||||
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
 | 
			
		||||
import { computed, reactive } from "vue";
 | 
			
		||||
import { useToast } from "vue-toast-notification";
 | 
			
		||||
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
 | 
			
		||||
import { DOCUMENT_ADD, trans } from "translator";
 | 
			
		||||
 | 
			
		||||
interface DropFileConfig {
 | 
			
		||||
    allowRemove: boolean;
 | 
			
		||||
@@ -78,9 +78,7 @@ function closeModal(): void {
 | 
			
		||||
    >
 | 
			
		||||
        {{ trans(DOCUMENT_ADD) }}
 | 
			
		||||
    </button>
 | 
			
		||||
    <button v-else @click="openModal" class="dropdown-item">
 | 
			
		||||
        {{ trans(DOCUMENT_REPLACE) }}
 | 
			
		||||
    </button>
 | 
			
		||||
    <button v-else @click="openModal" class="btn btn-edit"></button>
 | 
			
		||||
    <modal
 | 
			
		||||
        v-if="state.showModal"
 | 
			
		||||
        :modal-dialog-class="modalClasses"
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@ import {
 | 
			
		||||
    StoredObject,
 | 
			
		||||
    StoredObjectPointInTime,
 | 
			
		||||
    StoredObjectVersionWithPointInTime,
 | 
			
		||||
} from "./../../../types";
 | 
			
		||||
} from "ChillDocStoreAssets/types";
 | 
			
		||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
 | 
			
		||||
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
 | 
			
		||||
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
 | 
			
		||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
 | 
			
		||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
 | 
			
		||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,16 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
 | 
			
		||||
 | 
			
		||||
    public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
 | 
			
		||||
    {
 | 
			
		||||
        // we first try to get the permission from the workflow, as attachement (this is the less intensive query)
 | 
			
		||||
        $workflowPermissionAsAttachment = match ($attribute) {
 | 
			
		||||
            StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject),
 | 
			
		||||
            StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Retrieve the related entity
 | 
			
		||||
        $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
 | 
			
		||||
 | 
			
		||||
@@ -66,7 +76,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
 | 
			
		||||
        return match ($workflowPermission) {
 | 
			
		||||
            WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
 | 
			
		||||
            WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
 | 
			
		||||
            WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
 | 
			
		||||
            WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,12 @@ namespace Chill\DocStoreBundle\Security\Authorization;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface for voting on stored object permissions.
 | 
			
		||||
 *
 | 
			
		||||
 * Each time a stored object is attached to a document, the voter is responsible for determining
 | 
			
		||||
 * whether the user has the necessary permissions to access or modify the stored object.
 | 
			
		||||
 */
 | 
			
		||||
interface StoredObjectVoterInterface
 | 
			
		||||
{
 | 
			
		||||
    public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
 | 
			
		||||
use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException;
 | 
			
		||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
 | 
			
		||||
use Chill\WopiBundle\Service\WopiConverter;
 | 
			
		||||
use Symfony\Component\Mime\MimeTypesInterface;
 | 
			
		||||
@@ -41,9 +42,10 @@ class StoredObjectToPdfConverter
 | 
			
		||||
     *
 | 
			
		||||
     * @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true
 | 
			
		||||
     *
 | 
			
		||||
     * @throws \UnexpectedValueException    if the preferred mime type for the conversion is not found
 | 
			
		||||
     * @throws \RuntimeException            if the conversion or storage of the new version fails
 | 
			
		||||
     * @throws \UnexpectedValueException           if the preferred mime type for the conversion is not found
 | 
			
		||||
     * @throws \RuntimeException                   if the conversion or storage of the new version fails
 | 
			
		||||
     * @throws StoredObjectManagerException
 | 
			
		||||
     * @throws ConversionWithSameMimeTypeException if the document has already the same mime type79*
 | 
			
		||||
     */
 | 
			
		||||
    public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
 | 
			
		||||
    {
 | 
			
		||||
@@ -56,7 +58,7 @@ class StoredObjectToPdfConverter
 | 
			
		||||
        $currentVersion = $storedObject->getCurrentVersion();
 | 
			
		||||
 | 
			
		||||
        if ($currentVersion->getType() === $newMimeType) {
 | 
			
		||||
            throw new \UnexpectedValueException('Already at the same mime type');
 | 
			
		||||
            throw new ConversionWithSameMimeTypeException($newMimeType);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $content = $this->storedObjectManager->read($currentVersion);
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,10 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
 | 
			
		||||
            $storedObject->registerVersion();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // remove one version in the history
 | 
			
		||||
        $v5 = $storedObject->getVersions()->get(5);
 | 
			
		||||
        $storedObject->removeVersion($v5);
 | 
			
		||||
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
 | 
			
		||||
            ->willReturn(true)
 | 
			
		||||
@@ -53,6 +57,7 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
 | 
			
		||||
        self::assertEquals($response->getStatusCode(), 200);
 | 
			
		||||
        self::assertIsArray($body);
 | 
			
		||||
        self::assertArrayHasKey('results', $body);
 | 
			
		||||
        self::assertIsList($body['results']);
 | 
			
		||||
        self::assertCount(10, $body['results']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -86,9 +86,165 @@ class AbstractStoredObjectVoterTest extends TestCase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider dataProviderVoteOnAttribute
 | 
			
		||||
     * @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
 | 
			
		||||
     */
 | 
			
		||||
    public function testVoteOnAttribute(
 | 
			
		||||
    public function testVoteOnAttributeWithStoredObjectPermission(
 | 
			
		||||
        StoredObjectRoleEnum $attribute,
 | 
			
		||||
        bool $expected,
 | 
			
		||||
        bool $isGrantedRegularPermission,
 | 
			
		||||
        string $isGrantedWorkflowPermission,
 | 
			
		||||
        string $isGrantedStoredObjectAttachment,
 | 
			
		||||
    ): void {
 | 
			
		||||
        $storedObject = new StoredObject();
 | 
			
		||||
        $repository = new DummyRepository($related = new \stdClass());
 | 
			
		||||
        $token = new UsernamePasswordToken(new User(), 'dummy');
 | 
			
		||||
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
 | 
			
		||||
 | 
			
		||||
        $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
 | 
			
		||||
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
 | 
			
		||||
 | 
			
		||||
        if (StoredObjectRoleEnum::SEE === $attribute) {
 | 
			
		||||
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
 | 
			
		||||
                ->shouldBeCalled()
 | 
			
		||||
                ->willReturn($isGrantedStoredObjectAttachment);
 | 
			
		||||
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
 | 
			
		||||
                ->willReturn($isGrantedWorkflowPermission);
 | 
			
		||||
        } elseif (StoredObjectRoleEnum::EDIT === $attribute) {
 | 
			
		||||
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
 | 
			
		||||
                ->shouldBeCalled()
 | 
			
		||||
                ->willReturn($isGrantedStoredObjectAttachment);
 | 
			
		||||
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
 | 
			
		||||
                ->willReturn($isGrantedWorkflowPermission);
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new \LogicException('Invalid attribute for StoredObjectVoter');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter {
 | 
			
		||||
            public function __construct(private $repository, $helper, $security)
 | 
			
		||||
            {
 | 
			
		||||
                parent::__construct($security, $helper);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            protected function getRepository(): AssociatedEntityToStoredObjectInterface
 | 
			
		||||
            {
 | 
			
		||||
                return $this->repository;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            protected function getClass(): string
 | 
			
		||||
            {
 | 
			
		||||
                return \stdClass::class;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            protected function attributeToRole(StoredObjectRoleEnum $attribute): string
 | 
			
		||||
            {
 | 
			
		||||
                return 'SOME_ROLE';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            protected function canBeAssociatedWithWorkflow(): bool
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $actual = $storedObjectVoter->voteOnAttribute($attribute, $storedObject, $token);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expected, $actual);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
 | 
			
		||||
            yield 'Not related to any workflow nor attachment ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                true,
 | 
			
		||||
                true,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                false,
 | 
			
		||||
                false,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                false,
 | 
			
		||||
                true,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                false,
 | 
			
		||||
                true,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                false,
 | 
			
		||||
                true,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                false,
 | 
			
		||||
                true,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                false,
 | 
			
		||||
                false,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                false,
 | 
			
		||||
                false,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                true,
 | 
			
		||||
                false,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            yield 'Force grant inverse the regular permission (so) ('.$action.')' => [
 | 
			
		||||
                $attribute,
 | 
			
		||||
                true,
 | 
			
		||||
                false,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::ABSTAIN,
 | 
			
		||||
                WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission
 | 
			
		||||
     */
 | 
			
		||||
    public function testVoteOnAttributeWithoutStoredObjectPermission(
 | 
			
		||||
        StoredObjectRoleEnum $attribute,
 | 
			
		||||
        bool $expected,
 | 
			
		||||
        bool $canBeAssociatedWithWorkflow,
 | 
			
		||||
@@ -105,6 +261,10 @@ class AbstractStoredObjectVoterTest extends TestCase
 | 
			
		||||
        $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
 | 
			
		||||
 | 
			
		||||
        $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
 | 
			
		||||
 | 
			
		||||
        $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
 | 
			
		||||
        $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
 | 
			
		||||
 | 
			
		||||
        if (null !== $isGrantedWorkflowPermissionRead) {
 | 
			
		||||
            $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
 | 
			
		||||
                ->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
 | 
			
		||||
@@ -123,7 +283,7 @@ class AbstractStoredObjectVoterTest extends TestCase
 | 
			
		||||
        self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function dataProviderVoteOnAttribute(): iterable
 | 
			
		||||
    public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        // not associated on a workflow
 | 
			
		||||
        yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\CRUD\Controller\ApiController;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
@@ -27,7 +28,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
 | 
			
		||||
class WorkflowApiController
 | 
			
		||||
class WorkflowApiController extends ApiController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ final readonly class WorkflowSignatureStateChangeController
 | 
			
		||||
            $signature,
 | 
			
		||||
            $request,
 | 
			
		||||
            EntityWorkflowStepSignatureVoter::CANCEL,
 | 
			
		||||
            function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
 | 
			
		||||
            fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsCanceled($signature),
 | 
			
		||||
            '@ChillMain/WorkflowSignature/cancel.html.twig',
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
@@ -56,11 +56,18 @@ final readonly class WorkflowSignatureStateChangeController
 | 
			
		||||
            $signature,
 | 
			
		||||
            $request,
 | 
			
		||||
            EntityWorkflowStepSignatureVoter::REJECT,
 | 
			
		||||
            function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
 | 
			
		||||
            fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsRejected($signature),
 | 
			
		||||
            '@ChillMain/WorkflowSignature/reject.html.twig',
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param callable(EntityWorkflowStepSignature): string $markSignature
 | 
			
		||||
     *
 | 
			
		||||
     * @throws \Twig\Error\LoaderError
 | 
			
		||||
     * @throws \Twig\Error\RuntimeError
 | 
			
		||||
     * @throws \Twig\Error\SyntaxError
 | 
			
		||||
     */
 | 
			
		||||
    private function markSignatureAction(
 | 
			
		||||
        EntityWorkflowStepSignature $signature,
 | 
			
		||||
        Request $request,
 | 
			
		||||
@@ -79,12 +86,13 @@ final readonly class WorkflowSignatureStateChangeController
 | 
			
		||||
        $form->handleRequest($request);
 | 
			
		||||
 | 
			
		||||
        if ($form->isSubmitted() && $form->isValid()) {
 | 
			
		||||
            $this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) {
 | 
			
		||||
                $markSignature($signature);
 | 
			
		||||
            });
 | 
			
		||||
            $expectedStep = $this->entityManager->wrapInTransaction(fn () => $markSignature($signature));
 | 
			
		||||
 | 
			
		||||
            return new RedirectResponse(
 | 
			
		||||
                $this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
 | 
			
		||||
                $this->chillUrlGenerator->forwardReturnPath(
 | 
			
		||||
                    'chill_main_workflow_wait',
 | 
			
		||||
                    ['id' => $signature->getStep()->getEntityWorkflow()->getId(), 'expectedStep' => $expectedStep]
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\RedirectResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
final readonly class WorkflowWaitStepChangeController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private ChillUrlGeneratorInterface $chillUrlGenerator,
 | 
			
		||||
        private Environment $twig,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/{_locale}/main/workflow/{id}/wait/{expectedStep}', name: 'chill_main_workflow_wait', methods: ['GET'])]
 | 
			
		||||
    public function waitForSignatureChange(EntityWorkflow $entityWorkflow, string $expectedStep): Response
 | 
			
		||||
    {
 | 
			
		||||
        if ($entityWorkflow->getStep() === $expectedStep) {
 | 
			
		||||
            return new RedirectResponse(
 | 
			
		||||
                $this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new Response(
 | 
			
		||||
            $this->twig->render('@ChillMain/Workflow/waiting.html.twig', ['workflow' => $entityWorkflow, 'expectedStep' => $expectedStep])
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -30,6 +30,7 @@ use Chill\MainBundle\Controller\UserGroupAdminController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserGroupApiController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserJobApiController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserJobController;
 | 
			
		||||
use Chill\MainBundle\Controller\WorkflowApiController;
 | 
			
		||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\DQL\Age;
 | 
			
		||||
use Chill\MainBundle\Doctrine\DQL\Extract;
 | 
			
		||||
@@ -66,6 +67,7 @@ use Chill\MainBundle\Entity\Regroupment;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\MainBundle\Entity\UserJob;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Form\CenterType;
 | 
			
		||||
use Chill\MainBundle\Form\CivilityType;
 | 
			
		||||
use Chill\MainBundle\Form\CountryType;
 | 
			
		||||
@@ -79,6 +81,7 @@ use Chill\MainBundle\Form\UserGroupType;
 | 
			
		||||
use Chill\MainBundle\Form\UserJobType;
 | 
			
		||||
use Chill\MainBundle\Form\UserType;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
 | 
			
		||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
 | 
			
		||||
use Ramsey\Uuid\Doctrine\UuidType;
 | 
			
		||||
use Symfony\Component\Config\FileLocator;
 | 
			
		||||
@@ -940,6 +943,21 @@ class ChillMainExtension extends Extension implements
 | 
			
		||||
                        ],
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
                    'class' => EntityWorkflow::class,
 | 
			
		||||
                    'name' => 'workflow',
 | 
			
		||||
                    'base_path' => '/api/1.0/main/workflow',
 | 
			
		||||
                    'base_role' => EntityWorkflowVoter::SEE,
 | 
			
		||||
                    'controller' => WorkflowApiController::class,
 | 
			
		||||
                    'actions' => [
 | 
			
		||||
                        '_entity' => [
 | 
			
		||||
                            'methods' => [
 | 
			
		||||
                                Request::METHOD_GET => true,
 | 
			
		||||
                                Request::METHOD_HEAD => true,
 | 
			
		||||
                            ],
 | 
			
		||||
                        ],
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ class UserPasswordType extends AbstractType
 | 
			
		||||
                'invalid_message' => 'The password fields must match',
 | 
			
		||||
                'constraints' => [
 | 
			
		||||
                    new Length([
 | 
			
		||||
                        'min' => 9,
 | 
			
		||||
                        'min' => 14,
 | 
			
		||||
                        'minMessage' => 'The password must be greater than {{ limit }} characters',
 | 
			
		||||
                    ]),
 | 
			
		||||
                    new NotBlank(),
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Extracts the "returnPath" parameter from the current URL's query string and returns it.
 | 
			
		||||
 * If the parameter is not present, returns the provided fallback path.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} fallbackPath - The fallback path to use if "returnPath" is not found in the query string.
 | 
			
		||||
 * @return {string} The "returnPath" from the query string, or the fallback path if "returnPath" is not present.
 | 
			
		||||
 */
 | 
			
		||||
export function returnPathOr(fallbackPath: string): string {
 | 
			
		||||
    const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
    const returnPath = urlParams.get("returnPath");
 | 
			
		||||
 | 
			
		||||
    return returnPath ?? fallbackPath;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
import { EntityWorkflow } from "ChillMainAssets/types";
 | 
			
		||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
 | 
			
		||||
 | 
			
		||||
export const fetchWorkflow = async (
 | 
			
		||||
    workflowId: number,
 | 
			
		||||
): Promise<EntityWorkflow> => {
 | 
			
		||||
    try {
 | 
			
		||||
        return await makeFetch<null, EntityWorkflow>(
 | 
			
		||||
            "GET",
 | 
			
		||||
            `/api/1.0/main/workflow/${workflowId}.json`,
 | 
			
		||||
        );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error(`Failed to fetch workflow ${workflowId}:`, error);
 | 
			
		||||
        throw error;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
 | 
			
		||||
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
 | 
			
		||||
 | 
			
		||||
export interface DateTime {
 | 
			
		||||
    datetime: string;
 | 
			
		||||
@@ -202,6 +203,58 @@ export interface WorkflowAttachment {
 | 
			
		||||
    genericDoc: null | GenericDoc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Workflow {
 | 
			
		||||
    name: string;
 | 
			
		||||
    text: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntityWorkflowStep {
 | 
			
		||||
    type: "entity_workflow_step";
 | 
			
		||||
    id: number;
 | 
			
		||||
    comment: string;
 | 
			
		||||
    currentStep: StepDefinition;
 | 
			
		||||
    isFinal: boolean;
 | 
			
		||||
    isFreezed: boolean;
 | 
			
		||||
    isFinalized: boolean;
 | 
			
		||||
    transitionPrevious: Transition | null;
 | 
			
		||||
    transitionAfter: Transition | null;
 | 
			
		||||
    previousId: number | null;
 | 
			
		||||
    nextId: number | null;
 | 
			
		||||
    transitionPreviousBy: User | null;
 | 
			
		||||
    transitionPreviousAt: DateTime | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Transition {
 | 
			
		||||
    name: string;
 | 
			
		||||
    text: string;
 | 
			
		||||
    isForward: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StepDefinition {
 | 
			
		||||
    name: string;
 | 
			
		||||
    text: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EntityWorkflow {
 | 
			
		||||
    type: "entity_workflow";
 | 
			
		||||
    id: number;
 | 
			
		||||
    relatedEntityClass: string;
 | 
			
		||||
    relatedEntityId: number;
 | 
			
		||||
    workflow: Workflow;
 | 
			
		||||
    currentStep: EntityWorkflowStep;
 | 
			
		||||
    steps: EntityWorkflowStep[];
 | 
			
		||||
    datas: WorkflowData;
 | 
			
		||||
    title: string;
 | 
			
		||||
    isOnHoldAtCurrentStep: boolean;
 | 
			
		||||
    _permissions: {
 | 
			
		||||
        CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT: boolean;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WorkflowData {
 | 
			
		||||
    persons: Person[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ExportGeneration {
 | 
			
		||||
    id: string;
 | 
			
		||||
    type: "export_generation";
 | 
			
		||||
@@ -215,3 +268,8 @@ export interface ExportGeneration {
 | 
			
		||||
export interface PrivateCommentEmbeddable {
 | 
			
		||||
    comments: Record<number, string>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Possible states for the WaitingScreen Component.
 | 
			
		||||
 */
 | 
			
		||||
export type WaitingScreenState = "pending" | "failure" | "stopped" | "ready";
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,8 @@ import { computed, onMounted, ref } from "vue";
 | 
			
		||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
 | 
			
		||||
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
 | 
			
		||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
 | 
			
		||||
import { ExportGeneration } from "ChillMainAssets/types";
 | 
			
		||||
import WaitingScreen from "../_components/WaitingScreen.vue";
 | 
			
		||||
import { ExportGeneration, WaitingScreenState } from "ChillMainAssets/types";
 | 
			
		||||
 | 
			
		||||
interface AppProps {
 | 
			
		||||
    exportGenerationId: string;
 | 
			
		||||
@@ -34,13 +35,16 @@ const storedObject = computed<null | StoredObject>(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const isPending = computed<boolean>(() => status.value === "pending");
 | 
			
		||||
const isFetching = computed<boolean>(
 | 
			
		||||
    () => tryiesForReady.value < maxTryiesForReady,
 | 
			
		||||
);
 | 
			
		||||
const isReady = computed<boolean>(() => status.value === "ready");
 | 
			
		||||
const isFailure = computed<boolean>(() => status.value === "failure");
 | 
			
		||||
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
 | 
			
		||||
 | 
			
		||||
const state = computed<WaitingScreenState>((): WaitingScreenState => {
 | 
			
		||||
    if (status.value === "empty") {
 | 
			
		||||
        return "pending";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return status.value;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * counter for the number of times that we check for a new status
 | 
			
		||||
 */
 | 
			
		||||
@@ -85,57 +89,36 @@ onMounted(() => {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div id="waiting-screen">
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="isPending && isFetching"
 | 
			
		||||
            class="alert alert-danger text-center"
 | 
			
		||||
        >
 | 
			
		||||
            <div>
 | 
			
		||||
                <p>
 | 
			
		||||
                    {{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
    <WaitingScreen :state="state">
 | 
			
		||||
        <template v-slot:pending>
 | 
			
		||||
            <p>
 | 
			
		||||
                {{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
 | 
			
		||||
            </p>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
            <div>
 | 
			
		||||
                <i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
 | 
			
		||||
                <span class="sr-only">Loading...</span>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="isPending && !isFetching" class="alert alert-info">
 | 
			
		||||
            <div>
 | 
			
		||||
                <p>
 | 
			
		||||
                    {{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="isFailure" class="alert alert-danger text-center">
 | 
			
		||||
            <div>
 | 
			
		||||
                <p>
 | 
			
		||||
                    {{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="isReady" class="alert alert-success text-center">
 | 
			
		||||
            <div>
 | 
			
		||||
                <p>
 | 
			
		||||
                    {{ trans(EXPORT_GENERATION_EXPORT_READY) }}
 | 
			
		||||
                </p>
 | 
			
		||||
        <template v-slot:stopped>
 | 
			
		||||
            <p>
 | 
			
		||||
                {{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
 | 
			
		||||
            </p>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
                <p v-if="storedObject !== null">
 | 
			
		||||
                    <document-action-buttons-group
 | 
			
		||||
                        :stored-object="storedObject"
 | 
			
		||||
                        :filename="filename"
 | 
			
		||||
                    ></document-action-buttons-group>
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
        <template v-slot:failure>
 | 
			
		||||
            <p>
 | 
			
		||||
                {{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
 | 
			
		||||
            </p>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <template v-slot:ready>
 | 
			
		||||
            <p>
 | 
			
		||||
                {{ trans(EXPORT_GENERATION_EXPORT_READY) }}
 | 
			
		||||
            </p>
 | 
			
		||||
 | 
			
		||||
            <p v-if="storedObject !== null">
 | 
			
		||||
                <document-action-buttons-group
 | 
			
		||||
                    :stored-object="storedObject"
 | 
			
		||||
                    :filename="filename"
 | 
			
		||||
                ></document-action-buttons-group>
 | 
			
		||||
            </p>
 | 
			
		||||
        </template>
 | 
			
		||||
    </WaitingScreen>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
#waiting-screen {
 | 
			
		||||
    > .alert {
 | 
			
		||||
        min-height: 350px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,75 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useIntervalFn } from "@vueuse/core";
 | 
			
		||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
 | 
			
		||||
import { returnPathOr } from "ChillMainAssets/lib/return_path/returnPathHelper";
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import WaitingScreen from "ChillMainAssets/vuejs/_components/WaitingScreen.vue";
 | 
			
		||||
import { WaitingScreenState } from "ChillMainAssets/types";
 | 
			
		||||
import {
 | 
			
		||||
    trans,
 | 
			
		||||
    WORKFLOW_WAIT_TITLE,
 | 
			
		||||
    WORKFLOW_WAIT_ERROR_WHILE_WAITING,
 | 
			
		||||
    WORKFLOW_WAIT_SUCCESS,
 | 
			
		||||
} from "translator";
 | 
			
		||||
 | 
			
		||||
interface WaitPostProcessWorkflowComponentProps {
 | 
			
		||||
    workflowId: number;
 | 
			
		||||
    expectedStep: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<WaitPostProcessWorkflowComponentProps>();
 | 
			
		||||
const counter = ref<number>(0);
 | 
			
		||||
const MAX_TRYIES = 50;
 | 
			
		||||
 | 
			
		||||
const state = ref<WaitingScreenState>("pending");
 | 
			
		||||
 | 
			
		||||
const { pause, resume } = useIntervalFn(
 | 
			
		||||
    async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const workflow = await fetchWorkflow(props.workflowId);
 | 
			
		||||
            counter.value++;
 | 
			
		||||
            if (workflow.currentStep.currentStep.name === props.expectedStep) {
 | 
			
		||||
                window.location.assign(
 | 
			
		||||
                    returnPathOr("/fr/main/workflow" + workflow.id + "/show"),
 | 
			
		||||
                );
 | 
			
		||||
                resume();
 | 
			
		||||
                state.value = "ready";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (counter.value > MAX_TRYIES) {
 | 
			
		||||
                pause();
 | 
			
		||||
                state.value = "failure";
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error(error);
 | 
			
		||||
            pause();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    2000,
 | 
			
		||||
    { immediate: true },
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <WaitingScreen :state="state">
 | 
			
		||||
            <template v-slot:pending>
 | 
			
		||||
                <p>
 | 
			
		||||
                    {{ trans(WORKFLOW_WAIT_TITLE) }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template v-slot:failure>
 | 
			
		||||
                <p>
 | 
			
		||||
                    {{ trans(WORKFLOW_WAIT_ERROR_WHILE_WAITING) }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template v-slot:ready>
 | 
			
		||||
                <p>
 | 
			
		||||
                    {{ trans(WORKFLOW_WAIT_SUCCESS) }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </template>
 | 
			
		||||
        </WaitingScreen>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
import App from "./App.vue";
 | 
			
		||||
 | 
			
		||||
function mountApp(): void {
 | 
			
		||||
    const el = document.querySelector<HTMLDivElement>(".screen-wait");
 | 
			
		||||
    if (!el) {
 | 
			
		||||
        console.error(
 | 
			
		||||
            "WaitPostProcessWorkflow: mount element .screen-wait not found",
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const workflowIdAttr = el.getAttribute("data-workflow-id");
 | 
			
		||||
    const expectedStep = el.getAttribute("data-expected-step") || "";
 | 
			
		||||
 | 
			
		||||
    if (!workflowIdAttr) {
 | 
			
		||||
        console.error(
 | 
			
		||||
            "WaitPostProcessWorkflow: data-workflow-id attribute missing on mount element",
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!expectedStep) {
 | 
			
		||||
        console.error(
 | 
			
		||||
            "WaitPostProcessWorkflow: data-expected-step attribute missing on mount element",
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const workflowId = Number(workflowIdAttr);
 | 
			
		||||
    if (Number.isNaN(workflowId)) {
 | 
			
		||||
        console.error(
 | 
			
		||||
            "WaitPostProcessWorkflow: data-workflow-id is not a valid number:",
 | 
			
		||||
            workflowIdAttr,
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const app = createApp(App, {
 | 
			
		||||
        workflowId,
 | 
			
		||||
        expectedStep,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    app.mount(el);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (document.readyState === "loading") {
 | 
			
		||||
    document.addEventListener("DOMContentLoaded", mountApp);
 | 
			
		||||
} else {
 | 
			
		||||
    mountApp();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, useTemplateRef } from "vue";
 | 
			
		||||
import type { WorkflowAttachment } from "ChillMainAssets/types";
 | 
			
		||||
import { computed, onMounted, ref, useTemplateRef } from "vue";
 | 
			
		||||
import type { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
 | 
			
		||||
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
 | 
			
		||||
import { GenericDoc } from "ChillDocStoreAssets/types";
 | 
			
		||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
 | 
			
		||||
 | 
			
		||||
interface AppConfig {
 | 
			
		||||
    workflowId: number;
 | 
			
		||||
@@ -34,6 +35,13 @@ const attachedGenericDoc = computed<GenericDocForAccompanyingPeriod[]>(
 | 
			
		||||
            ) as GenericDocForAccompanyingPeriod[],
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const workflow = ref<EntityWorkflow | null>(null);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    workflow.value = await fetchWorkflow(Number(props.workflowId));
 | 
			
		||||
    console.log("workflow", workflow.value);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const openModal = function () {
 | 
			
		||||
    pickDocModal.value?.openModal();
 | 
			
		||||
};
 | 
			
		||||
@@ -49,20 +57,30 @@ const onPickGenericDoc = ({
 | 
			
		||||
const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
 | 
			
		||||
    emit("removeAttachment", payload);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const canEditAttachement = computed<boolean>(() => {
 | 
			
		||||
    if (null === workflow.value) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return workflow.value._permissions.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <pick-generic-doc-modal
 | 
			
		||||
        :workflow="workflow"
 | 
			
		||||
        :accompanying-period-id="props.accompanyingPeriodId"
 | 
			
		||||
        :to-remove="attachedGenericDoc"
 | 
			
		||||
        ref="pickDocModal"
 | 
			
		||||
        @pickGenericDoc="onPickGenericDoc"
 | 
			
		||||
    ></pick-generic-doc-modal>
 | 
			
		||||
    <attachment-list
 | 
			
		||||
        :workflow="workflow"
 | 
			
		||||
        :attachments="props.attachments"
 | 
			
		||||
        @removeAttachment="onRemoveAttachment"
 | 
			
		||||
    ></attachment-list>
 | 
			
		||||
    <ul class="record_actions">
 | 
			
		||||
    <ul v-if="canEditAttachement" class="record_actions">
 | 
			
		||||
        <li>
 | 
			
		||||
            <button type="button" class="btn btn-create" @click="openModal">
 | 
			
		||||
                Ajouter une pièce jointe
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { 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";
 | 
			
		||||
 | 
			
		||||
interface AttachmentListProps {
 | 
			
		||||
    attachments: WorkflowAttachment[];
 | 
			
		||||
    workflow: EntityWorkflow | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
@@ -36,7 +37,12 @@ const props = defineProps<AttachmentListProps>();
 | 
			
		||||
                            :stored-object="a.genericDoc.storedObject"
 | 
			
		||||
                        ></document-action-buttons-group>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li>
 | 
			
		||||
                    <li
 | 
			
		||||
                        v-if="
 | 
			
		||||
                            !workflow?._permissions
 | 
			
		||||
                                .CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT
 | 
			
		||||
                        "
 | 
			
		||||
                    >
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            class="btn btn-delete"
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,10 @@ import {
 | 
			
		||||
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
 | 
			
		||||
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
 | 
			
		||||
import { computed, onMounted, ref } from "vue";
 | 
			
		||||
import { EntityWorkflow } from "ChillMainAssets/types";
 | 
			
		||||
 | 
			
		||||
interface PickGenericDocProps {
 | 
			
		||||
    workflow: EntityWorkflow | null;
 | 
			
		||||
    accompanyingPeriodId: number;
 | 
			
		||||
    pickedList: GenericDocForAccompanyingPeriod[];
 | 
			
		||||
    toRemove: GenericDocForAccompanyingPeriod[];
 | 
			
		||||
@@ -36,9 +38,21 @@ const isPicked = (genericDoc: GenericDocForAccompanyingPeriod): boolean =>
 | 
			
		||||
    ) !== -1;
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    genericDocs.value = await fetch_generic_docs_by_accompanying_period(
 | 
			
		||||
    const fetchedGenericDocs = await fetch_generic_docs_by_accompanying_period(
 | 
			
		||||
        props.accompanyingPeriodId,
 | 
			
		||||
    );
 | 
			
		||||
    const documentClasses = [
 | 
			
		||||
        "Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument",
 | 
			
		||||
        "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
 | 
			
		||||
        "Chill\\DocStoreBundle\\Entity\\PersonDocument",
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    genericDocs.value = fetchedGenericDocs.filter(
 | 
			
		||||
        (doc) =>
 | 
			
		||||
            !documentClasses.includes(
 | 
			
		||||
                props.workflow?.relatedEntityClass || "",
 | 
			
		||||
            ) || props.workflow?.relatedEntityId !== doc.identifiers.id,
 | 
			
		||||
    );
 | 
			
		||||
    loaded.value = true;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,10 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
 | 
			
		||||
import { computed, ref, useTemplateRef } from "vue";
 | 
			
		||||
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
 | 
			
		||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
 | 
			
		||||
import { EntityWorkflow } from "ChillMainAssets/types";
 | 
			
		||||
 | 
			
		||||
interface PickGenericDocModalProps {
 | 
			
		||||
    workflow: EntityWorkflow | null;
 | 
			
		||||
    accompanyingPeriodId: number;
 | 
			
		||||
    toRemove: GenericDocForAccompanyingPeriod[];
 | 
			
		||||
}
 | 
			
		||||
@@ -80,6 +82,7 @@ defineExpose({ openModal, closeModal });
 | 
			
		||||
        </template>
 | 
			
		||||
        <template v-slot:body>
 | 
			
		||||
            <pick-generic-doc
 | 
			
		||||
                :workflow="props.workflow"
 | 
			
		||||
                :accompanying-period-id="props.accompanyingPeriodId"
 | 
			
		||||
                :to-remove="props.toRemove"
 | 
			
		||||
                :picked-list="pickeds"
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { WaitingScreenState } from "ChillMainAssets/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
    state: WaitingScreenState;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<Props>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div id="waiting-screen">
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="props.state === 'pending' && !!$slots.pending"
 | 
			
		||||
            class="alert alert-danger text-center"
 | 
			
		||||
        >
 | 
			
		||||
            <div>
 | 
			
		||||
                <slot name="pending"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div>
 | 
			
		||||
                <i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
 | 
			
		||||
                <span class="sr-only">Loading...</span>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="props.state === 'stopped' && !!$slots.stopped"
 | 
			
		||||
            class="alert alert-info"
 | 
			
		||||
        >
 | 
			
		||||
            <div>
 | 
			
		||||
                <slot name="stopped"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="props.state === 'failure' && !!$slots.failure"
 | 
			
		||||
            class="alert alert-danger text-center"
 | 
			
		||||
        >
 | 
			
		||||
            <div>
 | 
			
		||||
                <slot name="failure"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="props.state === 'ready' && !!$slots.ready"
 | 
			
		||||
            class="alert alert-success text-center"
 | 
			
		||||
        >
 | 
			
		||||
            <div>
 | 
			
		||||
                <slot name="ready"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
#waiting-screen {
 | 
			
		||||
    > .alert {
 | 
			
		||||
        min-height: 350px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
        role="button"
 | 
			
		||||
        data-bs-toggle="dropdown"
 | 
			
		||||
        aria-expanded="false">
 | 
			
		||||
        <i class="fa fa-flash"></i>
 | 
			
		||||
        <i class="bi bi-lightning-fill"></i>
 | 
			
		||||
    </a>
 | 
			
		||||
    <div class="dropdown-menu">
 | 
			
		||||
        {% for menu in menus %}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,6 @@
 | 
			
		||||
    {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
 | 
			
		||||
    {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
 | 
			
		||||
 | 
			
		||||
    {{ form_row(form.addressesEmails) }}
 | 
			
		||||
 | 
			
		||||
    {% include handler.template(notification) with handler.templateData(notification) %}
 | 
			
		||||
 | 
			
		||||
    <div class="mb-3 row">
 | 
			
		||||
 
 | 
			
		||||
@@ -58,12 +58,14 @@
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
    {% if signatures|length > 0 %}
 | 
			
		||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
 | 
			
		||||
 | 
			
		||||
    <section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
 | 
			
		||||
    {% if signatures|length > 0 %}
 | 
			
		||||
        <section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
 | 
			
		||||
    {% elseif entity_workflow.currentStep.sends|length > 0 %}
 | 
			
		||||
    {% if entity_workflow.currentStep.sends|length > 0 %}
 | 
			
		||||
        <section class="step my-4">
 | 
			
		||||
            <h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
 | 
			
		||||
            {% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
{% extends '@ChillMain/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ 'workflow.signature.waiting_for'|trans }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
    {{ encore_entry_link_tags('page_workflow_waiting_post_process') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block js %}
 | 
			
		||||
    {{ encore_entry_script_tags('page_workflow_waiting_post_process') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <h1>{{ block('title') }}</h1>
 | 
			
		||||
 | 
			
		||||
    <div class="screen-wait" data-workflow-id="{{ workflow.id|e('html_attr') }}" data-expected-step="{{ expectedStep|e('html_attr') }}"></div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -0,0 +1,53 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 | 
			
		||||
use Symfony\Component\Workflow\Registry;
 | 
			
		||||
 | 
			
		||||
final class EntityWorkflowAttachmentVoter extends Voter
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly Registry $registry,
 | 
			
		||||
    ) {}
 | 
			
		||||
    public const EDIT = 'CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT';
 | 
			
		||||
 | 
			
		||||
    protected function supports(string $attribute, $subject): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $subject instanceof EntityWorkflow && self::EDIT === $attribute;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (!$subject instanceof EntityWorkflow) {
 | 
			
		||||
            throw new \UnexpectedValueException('Subject must be an instance of EntityWorkflow');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($subject->isFinal()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $workflow = $this->registry->get($subject, $subject->getWorkflowName());
 | 
			
		||||
 | 
			
		||||
        $marking = $workflow->getMarking($subject);
 | 
			
		||||
        foreach ($marking->getPlaces() as $place => $int) {
 | 
			
		||||
            $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
 | 
			
		||||
            if ($placeMetadata['isSentExternal'] ?? false) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,18 +12,25 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\MainBundle\Serializer\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowAttachmentVoter;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
use Symfony\Component\Workflow\Registry;
 | 
			
		||||
 | 
			
		||||
class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
 | 
			
		||||
final class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
 | 
			
		||||
{
 | 
			
		||||
    use NormalizerAwareTrait;
 | 
			
		||||
 | 
			
		||||
    public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly EntityWorkflowManager $entityWorkflowManager,
 | 
			
		||||
        private readonly MetadataExtractor $metadataExtractor,
 | 
			
		||||
        private readonly Registry $registry,
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param EntityWorkflow $object
 | 
			
		||||
@@ -46,6 +53,9 @@ class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareIn
 | 
			
		||||
            'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
 | 
			
		||||
            'title' => $handler->getEntityTitle($object),
 | 
			
		||||
            'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
 | 
			
		||||
            '_permissions' => [
 | 
			
		||||
                EntityWorkflowAttachmentVoter::EDIT => $this->security->isGranted(EntityWorkflowAttachmentVoter::EDIT, $object),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ final class UserControllerTest extends WebTestCase
 | 
			
		||||
        self::assertResponseIsSuccessful();
 | 
			
		||||
 | 
			
		||||
        $username = 'Test_user'.uniqid();
 | 
			
		||||
        $password = 'Password1234!';
 | 
			
		||||
        $password = 'Password_1234!';
 | 
			
		||||
 | 
			
		||||
        // Fill in the form and submit it
 | 
			
		||||
 | 
			
		||||
@@ -99,7 +99,7 @@ final class UserControllerTest extends WebTestCase
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticatedAsAdmin();
 | 
			
		||||
        $crawler = $client->request('GET', "/fr/admin/user/{$userId}/edit_password");
 | 
			
		||||
        $newPassword = '1234Password!';
 | 
			
		||||
        $newPassword = '1234_Password!';
 | 
			
		||||
 | 
			
		||||
        $form = $crawler->selectButton('Changer le mot de passe')->form([
 | 
			
		||||
            'chill_mainbundle_user_password[new_password][first]' => $newPassword,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,173 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Tests\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowAttachmentVoter;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
 | 
			
		||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
 | 
			
		||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
 | 
			
		||||
use Symfony\Component\Workflow\DefinitionBuilder;
 | 
			
		||||
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
 | 
			
		||||
use Symfony\Component\Workflow\Registry;
 | 
			
		||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
 | 
			
		||||
use Symfony\Component\Workflow\Transition;
 | 
			
		||||
use Symfony\Component\Workflow\Workflow;
 | 
			
		||||
use Symfony\Component\Workflow\WorkflowInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class EntityWorkflowAttachmentVoterTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider dataVoteOnAttribute
 | 
			
		||||
     */
 | 
			
		||||
    public function testVoteOnAttribute(EntityWorkflow $entityWorkflow, int $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $voter = new EntityWorkflowAttachmentVoter($this->buildRegistry());
 | 
			
		||||
        $actual = $voter->vote(
 | 
			
		||||
            new UsernamePasswordToken(new User(), 'default'),
 | 
			
		||||
            $entityWorkflow,
 | 
			
		||||
            ['CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT'],
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals($expected, $actual);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function dataVoteOnAttribute(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $entity = new EntityWorkflow();
 | 
			
		||||
        $entity->setWorkflowName('dummy');
 | 
			
		||||
 | 
			
		||||
        $workflow = static::buildRegistry()->get($entity, 'dummy');
 | 
			
		||||
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entity);
 | 
			
		||||
        $dto->futureDestUsers[] = new User();
 | 
			
		||||
        $workflow->apply(
 | 
			
		||||
            $entity,
 | 
			
		||||
            'to_final_positive',
 | 
			
		||||
            ['context' => $dto,
 | 
			
		||||
                'byUser' => new User(),
 | 
			
		||||
                'transition' => 'to_final_positive',
 | 
			
		||||
                'transitionAt' => new \DateTimeImmutable()],
 | 
			
		||||
        );
 | 
			
		||||
        // we need to mark manually as final, as the listener is not registered
 | 
			
		||||
        $entity->getCurrentStep()->setIsFinal(true);
 | 
			
		||||
 | 
			
		||||
        yield 'on final positive' => [
 | 
			
		||||
            $entity,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $entity = new EntityWorkflow();
 | 
			
		||||
        $entity->setWorkflowName('dummy');
 | 
			
		||||
 | 
			
		||||
        $workflow = static::buildRegistry()->get($entity, 'dummy');
 | 
			
		||||
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entity);
 | 
			
		||||
        $dto->futureDestUsers[] = new User();
 | 
			
		||||
        $workflow->apply(
 | 
			
		||||
            $entity,
 | 
			
		||||
            'to_final_negative',
 | 
			
		||||
            ['context' => $dto,
 | 
			
		||||
                'byUser' => new User(),
 | 
			
		||||
                'transition' => 'to_final_negative',
 | 
			
		||||
                'transitionAt' => new \DateTimeImmutable()],
 | 
			
		||||
        );
 | 
			
		||||
        // we need to mark manually as final, as the listener is not registered
 | 
			
		||||
        $entity->getCurrentStep()->setIsFinal(true);
 | 
			
		||||
 | 
			
		||||
        yield 'on final negative' => [
 | 
			
		||||
            $entity,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $entity = new EntityWorkflow();
 | 
			
		||||
        $entity->setWorkflowName('dummy');
 | 
			
		||||
 | 
			
		||||
        $workflow = static::buildRegistry()->get($entity, 'dummy');
 | 
			
		||||
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entity);
 | 
			
		||||
        $dto->futureDestUsers[] = new User();
 | 
			
		||||
        $workflow->apply(
 | 
			
		||||
            $entity,
 | 
			
		||||
            'to_sent_external',
 | 
			
		||||
            ['context' => $dto,
 | 
			
		||||
                'byUser' => new User(),
 | 
			
		||||
                'transition' => 'to_sent_external',
 | 
			
		||||
                'transitionAt' => new \DateTimeImmutable()],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        yield 'on sent_external' => [
 | 
			
		||||
            $entity,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $entity = new EntityWorkflow();
 | 
			
		||||
        $entity->setWorkflowName('dummy');
 | 
			
		||||
 | 
			
		||||
        yield 'on initial' => [
 | 
			
		||||
            $entity,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static function buildRegistry(): Registry
 | 
			
		||||
    {
 | 
			
		||||
        $builder = new DefinitionBuilder();
 | 
			
		||||
        $builder
 | 
			
		||||
            ->setInitialPlaces(['initial'])
 | 
			
		||||
            ->addPlaces(['initial', 'sent_external', 'final_positive', 'final_negative'])
 | 
			
		||||
            ->addTransitions([
 | 
			
		||||
                new Transition('to_final_positive', ['initial'], 'final_positive'),
 | 
			
		||||
                new Transition('to_sent_external', ['initial'], 'sent_external'),
 | 
			
		||||
                new Transition('to_final_negative', ['initial'], 'final_negative'),
 | 
			
		||||
 | 
			
		||||
            ])
 | 
			
		||||
            ->setMetadataStore(
 | 
			
		||||
                new InMemoryMetadataStore(
 | 
			
		||||
                    placesMetadata: [
 | 
			
		||||
                        'sent_external' => [
 | 
			
		||||
                            'isSentExternal' => true,
 | 
			
		||||
                        ],
 | 
			
		||||
                        'final_positive' => [
 | 
			
		||||
                            'isFinal' => true,
 | 
			
		||||
                            'isFinalPositive' => true,
 | 
			
		||||
                        ],
 | 
			
		||||
                        'final_negative' => [
 | 
			
		||||
                            'isFinal' => true,
 | 
			
		||||
                            'isFinalPositive' => false,
 | 
			
		||||
                        ],
 | 
			
		||||
                    ]
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        $workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
 | 
			
		||||
        $registry = new Registry();
 | 
			
		||||
        $registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
 | 
			
		||||
            public function supports(WorkflowInterface $workflow, object $subject): bool
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return $registry;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,6 +11,9 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Tests\Workflow\Helper;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
 | 
			
		||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
@@ -148,8 +151,11 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain because the user is not present as a dest user'];
 | 
			
		||||
        yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain because there is no workflow'];
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
 | 
			
		||||
            'force deny because the user is not present as a dest user'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
@@ -171,6 +177,9 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
			
		||||
            'force grant because the user was a previous user'];
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
 | 
			
		||||
            'force denied because the user was not a previous user'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
@@ -232,6 +241,13 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain: there is a signature on a canceled workflow'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('sent_external', $dto, 'to_sent_external', new \DateTimeImmutable(), $user);
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(), 'force denied: the workflow is sent to an external user'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testNoWorkflow(): void
 | 
			
		||||
@@ -253,7 +269,217 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
			
		||||
        $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
 | 
			
		||||
        $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
 | 
			
		||||
 | 
			
		||||
        return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
 | 
			
		||||
        $repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
 | 
			
		||||
        $repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn([]);
 | 
			
		||||
 | 
			
		||||
        return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideDataAllowedByWorkflowReadOperationByAttachment
 | 
			
		||||
     *
 | 
			
		||||
     * @param list<EntityWorkflow> $entityWorkflows
 | 
			
		||||
     */
 | 
			
		||||
    public function testAllowedByWorkflowReadByAttachment(
 | 
			
		||||
        array $entityWorkflows,
 | 
			
		||||
        User $user,
 | 
			
		||||
        string $expected,
 | 
			
		||||
        ?\DateTimeImmutable $atDate,
 | 
			
		||||
        string $message,
 | 
			
		||||
    ): void {
 | 
			
		||||
        // all entities must have this workflow name, so we are ok to set it here
 | 
			
		||||
        foreach ($entityWorkflows as $entityWorkflow) {
 | 
			
		||||
            $entityWorkflow->setWorkflowName('dummy');
 | 
			
		||||
        }
 | 
			
		||||
        $helper = $this->buildHelperForAttachment($entityWorkflows, $user, $atDate);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new StoredObject()), $message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function provideDataAllowedByWorkflowReadOperationByAttachment(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain because the user is not present as a dest user'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
			
		||||
            'force grant because the user is a current user'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
			
		||||
            'force grant because the user was a previous user'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futurePersonSignatures[] = new Person();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'Abstain: there is a signature for person, but the attachment is not concerned'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideDataAllowedByWorkflowWriteOperationByAttachment
 | 
			
		||||
     *
 | 
			
		||||
     * @param list<EntityWorkflow> $entityWorkflows
 | 
			
		||||
     */
 | 
			
		||||
    public function testAllowedByWorkflowWriteByAttachment(
 | 
			
		||||
        array $entityWorkflows,
 | 
			
		||||
        User $user,
 | 
			
		||||
        string $expected,
 | 
			
		||||
        ?\DateTimeImmutable $atDate,
 | 
			
		||||
        string $message,
 | 
			
		||||
    ): void {
 | 
			
		||||
        // all entities must have this workflow name, so we are ok to set it here
 | 
			
		||||
        foreach ($entityWorkflows as $entityWorkflow) {
 | 
			
		||||
            $entityWorkflow->setWorkflowName('dummy');
 | 
			
		||||
        }
 | 
			
		||||
        $helper = $this->buildHelperForAttachment($entityWorkflows, $user, $atDate);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new StoredObject()), $message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function provideDataAllowedByWorkflowWriteOperationByAttachment(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain because there is no workflow'];
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain because the user is not present as a dest user (and attachment)'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
			
		||||
            'force grant because the user is a current user'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
 | 
			
		||||
            'force grant because the user was a previous user'];
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain because the user was not a previous user'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $entityWorkflow->getCurrentStep()->setIsFinal(true);
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
 | 
			
		||||
            'force denied: user was a previous user, but it is finalized positive'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable());
 | 
			
		||||
        $entityWorkflow->getCurrentStep()->setIsFinal(true);
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain: user was a previous user, it is finalized, but finalized negative'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futurePersonSignatures[] = new Person();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
 | 
			
		||||
        $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain: there is a signature, but not on the attachment'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futurePersonSignatures[] = new Person();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain: there is a signature, but the signature is not on the attachment'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futurePersonSignatures[] = new Person();
 | 
			
		||||
        $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
 | 
			
		||||
        $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), new User());
 | 
			
		||||
        $entityWorkflow->getCurrentStep()->setIsFinal(true);
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
 | 
			
		||||
            'abstain: there is a signature on a canceled workflow'];
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = new EntityWorkflow();
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestUsers[] = $user = new User();
 | 
			
		||||
        $entityWorkflow->setStep('sent_external', $dto, 'to_sent_external', new \DateTimeImmutable(), $user);
 | 
			
		||||
 | 
			
		||||
        yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
 | 
			
		||||
            'force denied: the workflow is sent to an external user'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param list<EntityWorkflow> $entityWorkflows
 | 
			
		||||
     */
 | 
			
		||||
    private function buildHelperForAttachment(array $entityWorkflows, User $user, ?\DateTimeImmutable $atDateTime): WorkflowRelatedEntityPermissionHelper
 | 
			
		||||
    {
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->getUser()->willReturn($user);
 | 
			
		||||
 | 
			
		||||
        $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
 | 
			
		||||
        $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->shouldNotBeCalled();
 | 
			
		||||
 | 
			
		||||
        $repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
 | 
			
		||||
        $attachments = [];
 | 
			
		||||
        foreach ($entityWorkflows as $entityWorkflow) {
 | 
			
		||||
            $attachments[] = new EntityWorkflowAttachment('dummy', ['id' => 1], $entityWorkflow, new StoredObject());
 | 
			
		||||
        }
 | 
			
		||||
        $repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments);
 | 
			
		||||
 | 
			
		||||
        return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static function buildRegistry(): Registry
 | 
			
		||||
@@ -261,10 +487,13 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
 | 
			
		||||
        $builder = new DefinitionBuilder();
 | 
			
		||||
        $builder
 | 
			
		||||
            ->setInitialPlaces(['initial'])
 | 
			
		||||
            ->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
 | 
			
		||||
            ->addPlaces(['initial', 'test', 'sent_external', 'final_positive', 'final_negative'])
 | 
			
		||||
            ->setMetadataStore(
 | 
			
		||||
                new InMemoryMetadataStore(
 | 
			
		||||
                    placesMetadata: [
 | 
			
		||||
                        'sent_external' => [
 | 
			
		||||
                            'isSentExternal' => true,
 | 
			
		||||
                        ],
 | 
			
		||||
                        'final_positive' => [
 | 
			
		||||
                            'isFinal' => true,
 | 
			
		||||
                            'isFinalPositive' => true,
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,11 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Tests\Workflow\Messenger;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
@@ -23,6 +26,7 @@ use Chill\ThirdPartyBundle\Entity\ThirdParty;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 | 
			
		||||
use Symfony\Component\Mailer\MailerInterface;
 | 
			
		||||
use Symfony\Component\Mime\Address;
 | 
			
		||||
@@ -39,24 +43,54 @@ class PostSendExternalMessageHandlerTest extends TestCase
 | 
			
		||||
    public function testSendMessageHappyScenario(): void
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflow = $this->buildEntityWorkflow();
 | 
			
		||||
 | 
			
		||||
        // Prepare attachments (2 attachments)
 | 
			
		||||
        $attachmentStoredObject1 = new StoredObject();
 | 
			
		||||
        $attachmentStoredObject2 = new StoredObject();
 | 
			
		||||
        new EntityWorkflowAttachment('generic_doc', ['id' => 1], $entityWorkflow, $attachmentStoredObject1);
 | 
			
		||||
        new EntityWorkflowAttachment('generic_doc', ['id' => 2], $entityWorkflow, $attachmentStoredObject2);
 | 
			
		||||
 | 
			
		||||
        // Prepare transition DTO and sends
 | 
			
		||||
        $dto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $dto->futureDestineeEmails = ['external@example.com'];
 | 
			
		||||
        $dto->futureDestineeThirdParties = [(new ThirdParty())->setEmail('3party@example.com')];
 | 
			
		||||
        $entityWorkflow->setStep('send_external', $dto, 'to_send_external', new \DateTimeImmutable(), new User());
 | 
			
		||||
 | 
			
		||||
        // Repository returns our workflow
 | 
			
		||||
        $repository = $this->prophesize(EntityWorkflowRepository::class);
 | 
			
		||||
        $repository->find(1)->willReturn($entityWorkflow);
 | 
			
		||||
 | 
			
		||||
        // Mailer must send to both recipients
 | 
			
		||||
        $mailer = $this->prophesize(MailerInterface::class);
 | 
			
		||||
        $mailer->send(Argument::that($this->buildCheckAddressCallback('3party@example.com')))->shouldBeCalledOnce();
 | 
			
		||||
        $mailer->send(Argument::that($this->buildCheckAddressCallback('external@example.com')))->shouldBeCalledOnce();
 | 
			
		||||
 | 
			
		||||
        // Workflow manager and handler
 | 
			
		||||
        $workflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
 | 
			
		||||
        $workflowHandler->getEntityTitle($entityWorkflow, Argument::any())->willReturn('title');
 | 
			
		||||
        $workflowManager = $this->prophesize(EntityWorkflowManager::class);
 | 
			
		||||
        $workflowManager->getHandler($entityWorkflow)->willReturn($workflowHandler->reveal());
 | 
			
		||||
 | 
			
		||||
        $handler = new PostSendExternalMessageHandler($repository->reveal(), $mailer->reveal(), $workflowManager->reveal());
 | 
			
		||||
        // Associated stored object for the workflow
 | 
			
		||||
        $associatedStoredObject = new StoredObject();
 | 
			
		||||
        $workflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($associatedStoredObject);
 | 
			
		||||
 | 
			
		||||
        // Converter should be called for each attachment and the associated stored object
 | 
			
		||||
        $converter = $this->prophesize(StoredObjectToPdfConverter::class);
 | 
			
		||||
        $converter->addConvertedVersion($attachmentStoredObject1, 'fr')->shouldBeCalledOnce();
 | 
			
		||||
        $converter->addConvertedVersion($attachmentStoredObject2, 'fr')->shouldBeCalledOnce();
 | 
			
		||||
        $converter->addConvertedVersion($associatedStoredObject, 'fr')->shouldBeCalledOnce();
 | 
			
		||||
 | 
			
		||||
        // Logger (not used in happy path, but required by handler)
 | 
			
		||||
        $logger = $this->prophesize(LoggerInterface::class);
 | 
			
		||||
 | 
			
		||||
        $handler = new PostSendExternalMessageHandler(
 | 
			
		||||
            $repository->reveal(),
 | 
			
		||||
            $mailer->reveal(),
 | 
			
		||||
            $workflowManager->reveal(),
 | 
			
		||||
            $converter->reveal(),
 | 
			
		||||
            $logger->reveal(),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $handler(new PostSendExternalMessage(1, 'fr'));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,12 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Workflow\Helper;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
@@ -58,21 +61,39 @@ class WorkflowRelatedEntityPermissionHelper
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
        private readonly EntityWorkflowManager $entityWorkflowManager,
 | 
			
		||||
        private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
 | 
			
		||||
        private readonly Registry $registry,
 | 
			
		||||
        private readonly ClockInterface $clock,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param object $entity The entity may be an
 | 
			
		||||
     *
 | 
			
		||||
     * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
 | 
			
		||||
     */
 | 
			
		||||
    public function isAllowedByWorkflowForReadOperation(object $entity): string
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
			
		||||
        if ($entity instanceof StoredObject) {
 | 
			
		||||
            $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
 | 
			
		||||
            $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
 | 
			
		||||
            $isAttached = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
			
		||||
            $isAttached = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ([] === $entityWorkflows) {
 | 
			
		||||
            return self::ABSTAIN;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
 | 
			
		||||
            return self::FORCE_GRANT;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($isAttached) {
 | 
			
		||||
            return self::ABSTAIN;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // give a view permission if there is a Person signature pending, or in the 12 hours following
 | 
			
		||||
        // the signature last state
 | 
			
		||||
        foreach ($entityWorkflows as $workflow) {
 | 
			
		||||
@@ -100,33 +121,51 @@ class WorkflowRelatedEntityPermissionHelper
 | 
			
		||||
     */
 | 
			
		||||
    public function isAllowedByWorkflowForWriteOperation(object $entity): string
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
			
		||||
        $runningWorkflows = [];
 | 
			
		||||
        if ($entity instanceof StoredObject) {
 | 
			
		||||
            $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
 | 
			
		||||
            $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
 | 
			
		||||
            $isAttached = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
 | 
			
		||||
            $isAttached = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if a workflow is finalized positive, we are not allowed to edit to document any more
 | 
			
		||||
        if ([] === $entityWorkflows) {
 | 
			
		||||
            return self::ABSTAIN;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if a workflow is finalized positive, anyone is allowed to edit the document anymore
 | 
			
		||||
        foreach ($entityWorkflows as $entityWorkflow) {
 | 
			
		||||
            if ($entityWorkflow->isFinal()) {
 | 
			
		||||
                $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
			
		||||
                $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
 | 
			
		||||
                foreach ($marking->getPlaces() as $place => $int) {
 | 
			
		||||
                    $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
 | 
			
		||||
                    if (true === ($placeMetadata['isFinalPositive'] ?? false)) {
 | 
			
		||||
                        // the workflow is final, and final positive, so we stop here.
 | 
			
		||||
                        return self::FORCE_DENIED;
 | 
			
		||||
                    }
 | 
			
		||||
            $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
			
		||||
            $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
 | 
			
		||||
            foreach ($marking->getPlaces() as $place => $int) {
 | 
			
		||||
                $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
 | 
			
		||||
                if (
 | 
			
		||||
                    ($entityWorkflow->isFinal() && ($placeMetadata['isFinalPositive'] ?? false))
 | 
			
		||||
                    || ($placeMetadata['isSentExternal'] ?? false)
 | 
			
		||||
                ) {
 | 
			
		||||
                    // the workflow is final, and final positive, or is sentExternal, so we stop here.
 | 
			
		||||
                    return self::FORCE_DENIED;
 | 
			
		||||
                }
 | 
			
		||||
                if (
 | 
			
		||||
                    // if not finalized positive
 | 
			
		||||
                    $entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false)
 | 
			
		||||
                ) {
 | 
			
		||||
                    return self::ABSTAIN;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                $runningWorkflows[] = $entityWorkflow;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if there is a signature on a **running workflow**, no one can edit the workflow any more
 | 
			
		||||
        foreach ($runningWorkflows as $entityWorkflow) {
 | 
			
		||||
            foreach ($entityWorkflow->getSteps() as $step) {
 | 
			
		||||
                foreach ($step->getSignatures() as $signature) {
 | 
			
		||||
                    if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
 | 
			
		||||
                        return self::FORCE_DENIED;
 | 
			
		||||
        $runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal());
 | 
			
		||||
 | 
			
		||||
        // if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore
 | 
			
		||||
        if (!$isAttached) {
 | 
			
		||||
            foreach ($runningWorkflows as $entityWorkflow) {
 | 
			
		||||
                foreach ($entityWorkflow->getSteps() as $step) {
 | 
			
		||||
                    foreach ($step->getSignatures() as $signature) {
 | 
			
		||||
                        if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
 | 
			
		||||
                            return self::FORCE_DENIED;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -137,7 +176,11 @@ class WorkflowRelatedEntityPermissionHelper
 | 
			
		||||
            return self::FORCE_GRANT;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return self::ABSTAIN;
 | 
			
		||||
        if ($isAttached) {
 | 
			
		||||
            return self::ABSTAIN;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return self::FORCE_DENIED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,14 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Workflow\Messenger;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException;
 | 
			
		||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
 | 
			
		||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
 | 
			
		||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
 | 
			
		||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 | 
			
		||||
use Symfony\Component\Mailer\MailerInterface;
 | 
			
		||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
 | 
			
		||||
@@ -25,6 +30,8 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
 | 
			
		||||
        private EntityWorkflowRepository $entityWorkflowRepository,
 | 
			
		||||
        private MailerInterface $mailer,
 | 
			
		||||
        private EntityWorkflowManager $workflowManager,
 | 
			
		||||
        private StoredObjectToPdfConverter $storedObjectToPdfConverter,
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function __invoke(PostSendExternalMessage $message): void
 | 
			
		||||
@@ -35,11 +42,38 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
 | 
			
		||||
            throw new UnrecoverableMessageHandlingException(sprintf('Entity workflow with id %d not found', $message->entityWorkflowId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->convertToPdf($entityWorkflow, $message->lang);
 | 
			
		||||
 | 
			
		||||
        foreach ($entityWorkflow->getCurrentStep()->getSends() as $send) {
 | 
			
		||||
            $this->sendEmailToDestinee($send, $message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function convertToPdf(EntityWorkflow $entityWorkflow, string $locale): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($entityWorkflow->getAttachments() as $attachment) {
 | 
			
		||||
            try {
 | 
			
		||||
                $this->storedObjectToPdfConverter->addConvertedVersion($attachment->getProxyStoredObject(), $locale);
 | 
			
		||||
            } catch (StoredObjectManagerException $e) {
 | 
			
		||||
                $this->logger->error('Error converting attachment to PDF', ['backtrace' => $e->getTraceAsString(), 'attachment_id' => $attachment->getId()]);
 | 
			
		||||
            } catch (ConversionWithSameMimeTypeException $e) {
 | 
			
		||||
                $this->logger->error('Error converting attachment to PDF: already at the same MIME type', ['backtrace' => $e->getTraceAsString(), 'attachment_id' => $attachment->getId()]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $storedObject = $this->workflowManager->getAssociatedStoredObject($entityWorkflow);
 | 
			
		||||
 | 
			
		||||
        if (null !== $storedObject) {
 | 
			
		||||
            try {
 | 
			
		||||
                $this->storedObjectToPdfConverter->addConvertedVersion($storedObject, $locale);
 | 
			
		||||
            } catch (StoredObjectManagerException $e) {
 | 
			
		||||
                $this->logger->error('Error converting stored object to PDF', ['backtrace' => $e->getTraceAsString(), 'stored_object_id' => $storedObject->getId(), 'workflow_id' => $entityWorkflow->getId()]);
 | 
			
		||||
            } catch (ConversionWithSameMimeTypeException $e) {
 | 
			
		||||
                $this->logger->error('Error converting stored object to PDF: already at the same MIME type', ['backtrace' => $e->getTraceAsString(), 'stored_object_id' => $storedObject->getId(), 'workflow_id' => $entityWorkflow->getId()]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function sendEmailToDestinee(EntityWorkflowSend $send, PostSendExternalMessage $message): void
 | 
			
		||||
    {
 | 
			
		||||
        $entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Component\Messenger\MessageBusInterface;
 | 
			
		||||
use Symfony\Component\Workflow\Registry;
 | 
			
		||||
use Symfony\Component\Workflow\Transition;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handles state changes for signature steps within a workflow.
 | 
			
		||||
@@ -50,8 +51,10 @@ class SignatureStepStateChanger
 | 
			
		||||
     *
 | 
			
		||||
     * @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
 | 
			
		||||
     * @param int|null                    $atIndex   optional index position for the signature within the zone
 | 
			
		||||
     *
 | 
			
		||||
     * @return string The expected new workflow's step, after transition is applyied
 | 
			
		||||
     */
 | 
			
		||||
    public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): void
 | 
			
		||||
    public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): string
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
			
		||||
 | 
			
		||||
@@ -60,7 +63,14 @@ class SignatureStepStateChanger
 | 
			
		||||
            ->setZoneSignatureIndex($atIndex)
 | 
			
		||||
            ->setStateDate($this->clock->now());
 | 
			
		||||
        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as signed', ['signatureId' => $signature->getId(), 'index' => (string) $atIndex]);
 | 
			
		||||
        ['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
 | 
			
		||||
 | 
			
		||||
        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
			
		||||
        if (null === $transition) {
 | 
			
		||||
            return $signature->getStep()->getEntityWorkflow()->getStep();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $transition->getTos()[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -71,8 +81,10 @@ class SignatureStepStateChanger
 | 
			
		||||
     *
 | 
			
		||||
     * This method updates the signature state to 'canceled' and logs the action.
 | 
			
		||||
     * It also dispatches a message to notify about the state change.
 | 
			
		||||
     *
 | 
			
		||||
     * @return string The expected new workflow's step, after transition is applyied
 | 
			
		||||
     */
 | 
			
		||||
    public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): void
 | 
			
		||||
    public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): string
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
			
		||||
 | 
			
		||||
@@ -80,7 +92,15 @@ class SignatureStepStateChanger
 | 
			
		||||
            ->setState(EntityWorkflowSignatureStateEnum::CANCELED)
 | 
			
		||||
            ->setStateDate($this->clock->now());
 | 
			
		||||
        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as canceled', ['signatureId' => $signature->getId()]);
 | 
			
		||||
 | 
			
		||||
        ['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
 | 
			
		||||
 | 
			
		||||
        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
			
		||||
        if (null === $transition) {
 | 
			
		||||
            return $signature->getStep()->getEntityWorkflow()->getStep();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $transition->getTos()[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -93,8 +113,10 @@ class SignatureStepStateChanger
 | 
			
		||||
     * a state change has occurred.
 | 
			
		||||
     *
 | 
			
		||||
     * @param EntityWorkflowStepSignature $signature the signature entity to be marked as rejected
 | 
			
		||||
     *
 | 
			
		||||
     * @return string The expected new workflow's step, after transition is applyied
 | 
			
		||||
     */
 | 
			
		||||
    public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): void
 | 
			
		||||
    public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): string
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
 | 
			
		||||
 | 
			
		||||
@@ -102,7 +124,16 @@ class SignatureStepStateChanger
 | 
			
		||||
            ->setState(EntityWorkflowSignatureStateEnum::REJECTED)
 | 
			
		||||
            ->setStateDate($this->clock->now());
 | 
			
		||||
        $this->logger->info(self::LOG_PREFIX.'Mark signature entity as rejected', ['signatureId' => $signature->getId()]);
 | 
			
		||||
 | 
			
		||||
        ['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
 | 
			
		||||
 | 
			
		||||
        $this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
 | 
			
		||||
 | 
			
		||||
        if (null === $transition) {
 | 
			
		||||
            return $signature->getStep()->getEntityWorkflow()->getStep();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $transition->getTos()[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -117,10 +148,35 @@ class SignatureStepStateChanger
 | 
			
		||||
    {
 | 
			
		||||
        $this->entityManager->refresh($signature, LockMode::PESSIMISTIC_READ);
 | 
			
		||||
 | 
			
		||||
        ['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
 | 
			
		||||
 | 
			
		||||
        if (null === $transition) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $entityWorkflow = $signature->getStep()->getEntityWorkflow();
 | 
			
		||||
        $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
 | 
			
		||||
        $transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $transitionDto->futureDestUsers[] = $futureUser;
 | 
			
		||||
 | 
			
		||||
        $workflow->apply($entityWorkflow, $transition->getName(), [
 | 
			
		||||
            'context' => $transitionDto,
 | 
			
		||||
            'transitionAt' => $this->clock->now(),
 | 
			
		||||
            'transition' => $transition->getName(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array{transition: Transition|null, futureUser: User|null}
 | 
			
		||||
     */
 | 
			
		||||
    private function decideTransition(EntityWorkflowStepSignature $signature): array
 | 
			
		||||
    {
 | 
			
		||||
        if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
 | 
			
		||||
            $this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
            return ['transition' => null, 'futureUser' => null];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
 | 
			
		||||
@@ -144,7 +200,7 @@ class SignatureStepStateChanger
 | 
			
		||||
        if (null === $transition) {
 | 
			
		||||
            $this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
            return ['transition' => null, 'futureUser' => null];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ('person' === $signature->getSignerKind()) {
 | 
			
		||||
@@ -156,19 +212,16 @@ class SignatureStepStateChanger
 | 
			
		||||
        if (null === $futureUser) {
 | 
			
		||||
            $this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
            return ['transition' => null, 'futureUser' => null];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
 | 
			
		||||
        $transitionDto->futureDestUsers[] = $futureUser;
 | 
			
		||||
        foreach ($workflow->getDefinition()->getTransitions() as $transitionObj) {
 | 
			
		||||
            if ($transitionObj->getName() === $transition) {
 | 
			
		||||
                return ['transition' => $transitionObj, 'futureUser' => $futureUser];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $workflow->apply($entityWorkflow, $transition, [
 | 
			
		||||
            'context' => $transitionDto,
 | 
			
		||||
            'transitionAt' => $this->clock->now(),
 | 
			
		||||
            'transition' => $transition,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
 | 
			
		||||
        throw new \RuntimeException('Transition not found');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
 | 
			
		||||
 
 | 
			
		||||
@@ -965,6 +965,31 @@ paths:
 | 
			
		||||
                        application/json:
 | 
			
		||||
                            schema:
 | 
			
		||||
                                $ref: "#/components/schemas/UserJob"
 | 
			
		||||
    /1.0/main/workflow/{id}.json:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
                - workflow
 | 
			
		||||
            summary: Return a workflow
 | 
			
		||||
            parameters:
 | 
			
		||||
                -   name: id
 | 
			
		||||
                    in: path
 | 
			
		||||
                    required: true
 | 
			
		||||
                    description: The workflow id
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
                        format: integer
 | 
			
		||||
                        minimum: 1
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "ok"
 | 
			
		||||
                    content:
 | 
			
		||||
                        application/json:
 | 
			
		||||
                            schema:
 | 
			
		||||
                                $ref: "#/components/schemas/Workflow"
 | 
			
		||||
                404:
 | 
			
		||||
                    description: "not found"
 | 
			
		||||
                401:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
    /1.0/main/workflow/my:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
 
 | 
			
		||||
@@ -120,5 +120,8 @@ module.exports = function (encore, entries) {
 | 
			
		||||
    "vue_onthefly",
 | 
			
		||||
    __dirname + "/Resources/public/vuejs/OnTheFly/index.js",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  encore.addEntry(
 | 
			
		||||
    "page_workflow_waiting_post_process",
 | 
			
		||||
    __dirname + "/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts"
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -666,10 +666,17 @@ workflow:
 | 
			
		||||
        cancel_are_you_sure: Êtes-vous sûr de vouloir annuler la signature de %signer%
 | 
			
		||||
        reject_signature_of: Rejet de la signature de %signer%
 | 
			
		||||
        reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer%
 | 
			
		||||
        waiting_for: En attente de modification de l'état de la signature
 | 
			
		||||
 | 
			
		||||
    attachments:
 | 
			
		||||
        title: Pièces jointes
 | 
			
		||||
 | 
			
		||||
    wait:
 | 
			
		||||
        title: En attente de traitement
 | 
			
		||||
        error_while_waiting: Le traitement a échoué
 | 
			
		||||
        success: Traitement terminé. Redirection en cours...
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Subscribe final: Recevoir une notification à l'étape finale
 | 
			
		||||
Subscribe all steps: Recevoir une notification à chaque étape
 | 
			
		||||
CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
            :id="evaluation.id"
 | 
			
		||||
            :templates="templates"
 | 
			
		||||
            :preventDefaultMoveToGenerate="true"
 | 
			
		||||
            @go-to-generate-document="$emit('submitBeforeGenerate', $event)"
 | 
			
		||||
            @go-to-generate-document="submitBeforeGenerate"
 | 
			
		||||
        >
 | 
			
		||||
            <template v-slot:title>
 | 
			
		||||
                <label class="col-form-label">{{
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
                <li>
 | 
			
		||||
                    <drop-file-modal
 | 
			
		||||
                        :allow-remove="false"
 | 
			
		||||
                        @add-document="$emit('addDocument', $event)"
 | 
			
		||||
                        @add-document="emit('addDocument', $event)"
 | 
			
		||||
                    ></drop-file-modal>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
@@ -39,9 +39,34 @@ import {
 | 
			
		||||
    EVALUATION_GENERATE_A_DOCUMENT,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
 | 
			
		||||
defineProps(["evaluation", "templates"]);
 | 
			
		||||
defineEmits(["addDocument", "submitBeforeGenerate"]);
 | 
			
		||||
const store = useStore();
 | 
			
		||||
 | 
			
		||||
const props = defineProps(["evaluation", "templates"]);
 | 
			
		||||
const emit = defineEmits(["addDocument"]);
 | 
			
		||||
 | 
			
		||||
async function submitBeforeGenerate({ template }) {
 | 
			
		||||
    const callback = (data) => {
 | 
			
		||||
        let evaluationId = data.accompanyingPeriodWorkEvaluations.find(
 | 
			
		||||
            (e) => e.key === props.evaluation.key,
 | 
			
		||||
        ).id;
 | 
			
		||||
 | 
			
		||||
        window.location.assign(
 | 
			
		||||
            buildLink(
 | 
			
		||||
                template,
 | 
			
		||||
                evaluationId,
 | 
			
		||||
                "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return store.dispatch("submit", callback).catch((e) => {
 | 
			
		||||
        console.log(e);
 | 
			
		||||
        throw e;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@
 | 
			
		||||
                                    :preventDefaultMoveToGenerate="true"
 | 
			
		||||
                                    :goToGenerateWorkflowPayload="{ doc: d }"
 | 
			
		||||
                                    @go-to-generate-workflow="
 | 
			
		||||
                                        $emit('goToGenerateWorkflow', $event)
 | 
			
		||||
                                        goToGenerateWorkflowEvaluationDocument
 | 
			
		||||
                                    "
 | 
			
		||||
                                ></list-workflow-modal>
 | 
			
		||||
                            </li>
 | 
			
		||||
@@ -95,10 +95,9 @@
 | 
			
		||||
                                            <a
 | 
			
		||||
                                                class="dropdown-item"
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    $emit(
 | 
			
		||||
                                                        'goToGenerateNotification',
 | 
			
		||||
                                                    goToGenerateDocumentNotification(
 | 
			
		||||
                                                        d,
 | 
			
		||||
                                                        true,
 | 
			
		||||
                                                        false,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                "
 | 
			
		||||
                                            >
 | 
			
		||||
@@ -113,8 +112,7 @@
 | 
			
		||||
                                            <a
 | 
			
		||||
                                                class="dropdown-item"
 | 
			
		||||
                                                @click="
 | 
			
		||||
                                                    $emit(
 | 
			
		||||
                                                        'goToGenerateNotification',
 | 
			
		||||
                                                    goToGenerateDocumentNotification(
 | 
			
		||||
                                                        d,
 | 
			
		||||
                                                        false,
 | 
			
		||||
                                                    )
 | 
			
		||||
@@ -150,15 +148,35 @@
 | 
			
		||||
                                    "
 | 
			
		||||
                                ></document-action-buttons-group>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <!--replace document-->
 | 
			
		||||
                            <li
 | 
			
		||||
                                v-if="
 | 
			
		||||
                                    Number.isInteger(d.id) &&
 | 
			
		||||
                                    d.storedObject._permissions.canEdit
 | 
			
		||||
                                "
 | 
			
		||||
                            >
 | 
			
		||||
                                <drop-file-modal
 | 
			
		||||
                                    :existing-doc="d.storedObject"
 | 
			
		||||
                                    :allow-remove="false"
 | 
			
		||||
                                    @add-document="
 | 
			
		||||
                                        (arg) =>
 | 
			
		||||
                                            replaceDocument(
 | 
			
		||||
                                                d,
 | 
			
		||||
                                                arg.stored_object,
 | 
			
		||||
                                                arg.stored_object_version,
 | 
			
		||||
                                            )
 | 
			
		||||
                                    "
 | 
			
		||||
                                ></drop-file-modal>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <li v-if="Number.isInteger(d.id)">
 | 
			
		||||
                                <div class="duplicate-dropdown">
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        class="btn btn-edit dropdown-toggle"
 | 
			
		||||
                                        class="btn btn-outline-primary dropdown-toggle"
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        data-bs-toggle="dropdown"
 | 
			
		||||
                                        aria-expanded="false"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {{ trans(EVALUATION_DOCUMENT_EDIT) }}
 | 
			
		||||
                                        <i class="bi bi-lightning-fill"></i>
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <ul class="dropdown-menu">
 | 
			
		||||
                                        <!--delete-->
 | 
			
		||||
@@ -180,27 +198,6 @@
 | 
			
		||||
                                                }}
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <!--replace document-->
 | 
			
		||||
                                        <li
 | 
			
		||||
                                            v-if="
 | 
			
		||||
                                                d.storedObject._permissions
 | 
			
		||||
                                                    .canEdit
 | 
			
		||||
                                            "
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <drop-file-modal
 | 
			
		||||
                                                :existing-doc="d.storedObject"
 | 
			
		||||
                                                :allow-remove="false"
 | 
			
		||||
                                                @add-document="
 | 
			
		||||
                                                    (arg) =>
 | 
			
		||||
                                                        $emit(
 | 
			
		||||
                                                            'replaceDocument',
 | 
			
		||||
                                                            d,
 | 
			
		||||
                                                            arg.stored_object,
 | 
			
		||||
                                                            arg.stored_object_version,
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                "
 | 
			
		||||
                                            ></drop-file-modal>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <!--duplicate document-->
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            <a
 | 
			
		||||
@@ -300,35 +297,45 @@ import {
 | 
			
		||||
    EVALUATION_DOCUMENTS,
 | 
			
		||||
    EVALUATION_DOCUMENT_MOVE,
 | 
			
		||||
    EVALUATION_DOCUMENT_DELETE,
 | 
			
		||||
    EVALUATION_DOCUMENT_EDIT,
 | 
			
		||||
    EVALUATION_DOCUMENT_DUPLICATE_HERE,
 | 
			
		||||
    EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
import { ref, watch } from "vue";
 | 
			
		||||
import { computed, ref, watch } from "vue";
 | 
			
		||||
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue";
 | 
			
		||||
import { buildLinkCreate } from "ChillMainAssets/lib/entity-workflow/api";
 | 
			
		||||
import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
 | 
			
		||||
defineProps([
 | 
			
		||||
const props = defineProps([
 | 
			
		||||
    "documents",
 | 
			
		||||
    "docAnchorId",
 | 
			
		||||
    "accompanyingPeriodId",
 | 
			
		||||
    "accompanyingPeriodWorkId",
 | 
			
		||||
    "evaluation",
 | 
			
		||||
]);
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
    "inputDocumentTitle",
 | 
			
		||||
    "removeDocument",
 | 
			
		||||
    "duplicateDocument",
 | 
			
		||||
    "statusDocumentChanged",
 | 
			
		||||
    "goToGenerateWorkflow",
 | 
			
		||||
    "goToGenerateNotification",
 | 
			
		||||
    "duplicateDocumentToWork",
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
const store = useStore();
 | 
			
		||||
 | 
			
		||||
const showAccompanyingPeriodSelector = ref(false);
 | 
			
		||||
const selectedEvaluation = ref(null);
 | 
			
		||||
const selectedDocumentToDuplicate = ref(null);
 | 
			
		||||
const selectedDocumentToMove = ref(null);
 | 
			
		||||
 | 
			
		||||
const AmIRefferer = computed(() => {
 | 
			
		||||
    return !(
 | 
			
		||||
        store.state.work.accompanyingPeriod.user &&
 | 
			
		||||
        store.state.me &&
 | 
			
		||||
        store.state.work.accompanyingPeriod.user.id !== store.state.me.id
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const prepareDocumentDuplicationToWork = (d) => {
 | 
			
		||||
    selectedDocumentToDuplicate.value = d;
 | 
			
		||||
    /** ensure selectedDocumentToMove is null */
 | 
			
		||||
@@ -358,4 +365,91 @@ watch(selectedEvaluation, (val) => {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function goToGenerateWorkflowEvaluationDocument({
 | 
			
		||||
    workflowName,
 | 
			
		||||
    payload,
 | 
			
		||||
}) {
 | 
			
		||||
    const callback = (data) => {
 | 
			
		||||
        let evaluation = data.accompanyingPeriodWorkEvaluations.find(
 | 
			
		||||
            (e) => e.key === props.evaluation.key,
 | 
			
		||||
        );
 | 
			
		||||
        let updatedDocument = evaluation.documents.find(
 | 
			
		||||
            (d) => d.key === payload.doc.key,
 | 
			
		||||
        );
 | 
			
		||||
        window.location.assign(
 | 
			
		||||
            buildLinkCreate(
 | 
			
		||||
                workflowName,
 | 
			
		||||
                "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
 | 
			
		||||
                updatedDocument.id,
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return store.dispatch("submit", callback).catch((e) => {
 | 
			
		||||
        console.log(e);
 | 
			
		||||
        throw e;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Replaces a document in the store with a new document.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Object} oldDocument - The document to be replaced.
 | 
			
		||||
 * @param {StoredObject} storedObject - The stored object of the new document.
 | 
			
		||||
 * @param {StoredObjectVersion} storedObjectVersion - The new version of the document
 | 
			
		||||
 * @return {void}
 | 
			
		||||
 */
 | 
			
		||||
async function replaceDocument(oldDocument, storedObject, storedObjectVersion) {
 | 
			
		||||
    let document = {
 | 
			
		||||
        type: "accompanying_period_work_evaluation_document",
 | 
			
		||||
        storedObject: storedObject,
 | 
			
		||||
        title: oldDocument.title,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return store.commit("replaceDocument", {
 | 
			
		||||
        key: props.evaluation.key,
 | 
			
		||||
        document,
 | 
			
		||||
        oldDocument: oldDocument,
 | 
			
		||||
        stored_object_version: storedObjectVersion,
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function goToGenerateDocumentNotification(document, tos) {
 | 
			
		||||
    const callback = (data) => {
 | 
			
		||||
        let evaluation = data.accompanyingPeriodWorkEvaluations.find(
 | 
			
		||||
            (e) => e.key === props.evaluation.key,
 | 
			
		||||
        );
 | 
			
		||||
        let updatedDocument = evaluation.documents.find(
 | 
			
		||||
            (d) => d.key === document.key,
 | 
			
		||||
        );
 | 
			
		||||
        window.location.assign(
 | 
			
		||||
            buildLinkCreateNotification(
 | 
			
		||||
                "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
 | 
			
		||||
                updatedDocument.id,
 | 
			
		||||
                tos === true
 | 
			
		||||
                    ? store.state.work.accompanyingPeriod.user?.id
 | 
			
		||||
                    : null,
 | 
			
		||||
                window.location.pathname +
 | 
			
		||||
                    window.location.search +
 | 
			
		||||
                    window.location.hash,
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return store.dispatch("submit", callback).catch((e) => {
 | 
			
		||||
        console.log(e);
 | 
			
		||||
        throw e;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function submitBeforeLeaveToEditor() {
 | 
			
		||||
    console.log("submit beore edit 2");
 | 
			
		||||
    // empty callback
 | 
			
		||||
    const callback = () => null;
 | 
			
		||||
    return store.dispatch("submit", callback).catch((e) => {
 | 
			
		||||
        console.log(e);
 | 
			
		||||
        throw e;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,8 @@
 | 
			
		||||
                v-if="evaluation.documents.length > 0"
 | 
			
		||||
                :documents="evaluation.documents"
 | 
			
		||||
                :docAnchorId="docAnchorId"
 | 
			
		||||
                :evaluation="evaluation"
 | 
			
		||||
                :accompanyingPeriodId="store.state.work.accompanyingPeriod.id"
 | 
			
		||||
                :accompanying-period-work-id="store.state.work.id"
 | 
			
		||||
                @inputDocumentTitle="onInputDocumentTitle"
 | 
			
		||||
                @removeDocument="removeDocument"
 | 
			
		||||
                @duplicateDocument="duplicateDocument"
 | 
			
		||||
@@ -34,7 +34,6 @@
 | 
			
		||||
                "
 | 
			
		||||
                @move-document-to-evaluation="moveDocumentToEvaluation"
 | 
			
		||||
                @statusDocumentChanged="onStatusDocumentChanged"
 | 
			
		||||
                @goToGenerateWorkflow="goToGenerateWorkflowEvaluationDocument"
 | 
			
		||||
                @goToGenerateNotification="goToGenerateDocumentNotification"
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +41,6 @@
 | 
			
		||||
                :evaluation="evaluation"
 | 
			
		||||
                :templates="getTemplatesAvailables"
 | 
			
		||||
                @addDocument="addDocument"
 | 
			
		||||
                @submitBeforeGenerate="submitBeforeGenerate"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -290,29 +288,6 @@ function onStatusDocumentChanged(newStatus) {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function goToGenerateWorkflowEvaluationDocument({ workflowName, payload }) {
 | 
			
		||||
    const callback = (data) => {
 | 
			
		||||
        let evaluation = data.accompanyingPeriodWorkEvaluations.find(
 | 
			
		||||
            (e) => e.key === props.evaluation.key,
 | 
			
		||||
        );
 | 
			
		||||
        let updatedDocument = evaluation.documents.find(
 | 
			
		||||
            (d) => d.key === payload.doc.key,
 | 
			
		||||
        );
 | 
			
		||||
        window.location.assign(
 | 
			
		||||
            buildLinkCreate(
 | 
			
		||||
                workflowName,
 | 
			
		||||
                "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
 | 
			
		||||
                updatedDocument.id,
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    store.dispatch("submit", callback).catch((e) => {
 | 
			
		||||
        console.log(e);
 | 
			
		||||
        throw e;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function goToGenerateDocumentNotification(document, tos) {
 | 
			
		||||
    const callback = (data) => {
 | 
			
		||||
        let evaluation = data.accompanyingPeriodWorkEvaluations.find(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user